Better Gemini Support + Fixed gemini Inconsistency with regex retry

This commit is contained in:
zero64801 2025-10-22 15:38:44 -03:00
parent 4c4341fc62
commit 4677078f96
5 changed files with 193 additions and 119 deletions

View file

@ -1,3 +1,17 @@
# --------------------------------------------------
# Gemini-Specific Settings (Optional)
# --------------------------------------------------
# Sets the thinking budget for Gemini models.
# - To turn on dynamic thinking, set to -1.
# - To disable thinking (on compatible models like 2.5 Flash), set to 0.
# - To set a specific token budget, use a positive integer (e.g., 8192).
# - Leave this variable out to use the model's default setting.
GEMINI_THINKING_BUDGET=
# Set to "gemini" to use the Gemini API or "openai" for OpenAI (If empty it will default to openai.)
API_PROVIDER=openai
#API link, leave blank to use OpenAI API #API link, leave blank to use OpenAI API
api="" api=""

View file

@ -239,4 +239,4 @@ def deleteFolderFiles(folderPath):
for filename in os.listdir(folderPath): for filename in os.listdir(folderPath):
file_path = os.path.join(folderPath, filename) file_path = os.path.join(folderPath, filename)
if file_path.endswith((".json", ".yaml", ".ks")): if file_path.endswith((".json", ".yaml", ".ks")):
os.remove(file_path) os.remove(file_path)

View file

@ -18,9 +18,14 @@ import tempfile
# Open AI # Open AI
load_dotenv() load_dotenv()
if os.getenv("api").replace(" ", "") != "": if os.getenv("API_PROVIDER") == "gemini":
openai.base_url = os.getenv("api") openai.base_url = "https://generativelanguage.googleapis.com/v1beta/openai/"
openai.organization = os.getenv("org") openai.organization = None
else:
api_url = os.getenv("api")
if api_url and api_url.replace(" ", "") != "":
openai.base_url = api_url
openai.organization = os.getenv("org")
openai.api_key = os.getenv("key") openai.api_key = os.getenv("key")
# Globals # Globals
@ -429,4 +434,4 @@ def translateAI(text, history, fullPromptFlag):
pbar=PBAR, pbar=PBAR,
lock=LOCK, lock=LOCK,
mismatchList=MISMATCH mismatchList=MISMATCH
) )

View file

@ -18,9 +18,14 @@ import tempfile
# Open AI # Open AI
load_dotenv() load_dotenv()
if os.getenv("api").replace(" ", "") != "": if os.getenv("API_PROVIDER") == "gemini":
openai.base_url = os.getenv("api") openai.base_url = "https://generativelanguage.googleapis.com/v1beta/openai/"
openai.organization = os.getenv("org") openai.organization = None
else:
api_url = os.getenv("api")
if api_url and api_url.replace(" ", "") != "":
openai.base_url = api_url
openai.organization = os.getenv("org")
openai.api_key = os.getenv("key") openai.api_key = os.getenv("key")
# Globals # Globals
@ -370,4 +375,4 @@ def translateAI(text, history, fullPromptFlag):
pbar=PBAR, pbar=PBAR,
lock=LOCK, lock=LOCK,
mismatchList=MISMATCH mismatchList=MISMATCH
) )

View file

@ -130,6 +130,27 @@ def getPricingConfig(model):
"batchSize": 30, "batchSize": 30,
"frequencyPenalty": 0.05 "frequencyPenalty": 0.05
} }
elif "gemini-1.5-flash" in model:
return {
"inputAPICost": 0.35,
"outputAPICost": 1.05,
"batchSize": 30,
"frequencyPenalty": 0.0
}
elif "gemini-1.5-pro" in model:
return {
"inputAPICost": 3.50,
"outputAPICost": 10.50,
"batchSize": 30,
"frequencyPenalty": 0.0
}
elif "gemini" in model:
return {
"inputAPICost": 0.50,
"outputAPICost": 1.50,
"batchSize": 30,
"frequencyPenalty": 0.0
}
else: else:
# Fallback to environment variables # Fallback to environment variables
return { return {
@ -285,7 +306,7 @@ def createTranslationSchema(numLines):
def translateText(system, user, history, penalty, formatType, model, numLines=None): def translateText(system, user, history, penalty, formatType, model, numLines=None):
"""Send translation request to OpenAI API""" """Send translation request to the selected API"""
# Ensure system content is not empty # Ensure system content is not empty
if not system or not str(system).strip(): if not system or not str(system).strip():
raise ValueError("System content cannot be empty") raise ValueError("System content cannot be empty")
@ -330,42 +351,53 @@ def translateText(system, user, history, penalty, formatType, model, numLines=No
if not message.get("content") or not str(message.get("content")).strip(): if not message.get("content") or not str(message.get("content")).strip():
raise ValueError(f"Message {i} has empty content: {message}") raise ValueError(f"Message {i} has empty content: {message}")
# Call OpenAI API # --- API Call Logic ---
try: api_provider = os.getenv("API_PROVIDER", "openai").lower()
# Base parameters for the API call
params = {
"model": model,
"response_format": responseFormat,
"messages": msg,
}
# Provider-specific parameters
if api_provider == "gemini":
params["temperature"] = 0
# Handle thinking budget for Gemini
thinking_budget_str = os.getenv("GEMINI_THINKING_BUDGET")
if thinking_budget_str:
try:
thinking_budget = int(thinking_budget_str)
params["extra_body"] = {
'google': {
'thinking_config': {
'thinking_budget': thinking_budget
}
}
}
except (ValueError, TypeError):
# Ignore if the value is not a valid integer
pass
# frequency_penalty is not supported via the OpenAI compatibility layer for Gemini
else: # Default to OpenAI behavior
if "gpt-5" in model: if "gpt-5" in model:
response = openai.chat.completions.create( params["reasoning_effort"] = "minimal"
model=model,
response_format=responseFormat,
messages=msg,
reasoning_effort="minimal"
)
else: else:
response = openai.chat.completions.create( params["temperature"] = 0
model=model, params["frequency_penalty"] = penalty
response_format=responseFormat,
messages=msg, # Call API
temperature=0, try:
frequency_penalty=penalty response = openai.chat.completions.create(**params)
)
except Exception as e: except Exception as e:
# If structured output fails, fallback to json_object # If structured output fails, fallback to json_object
if formatType == "json" and "json_schema" in str(responseFormat): if formatType == "json" and "json_schema" in str(responseFormat):
responseFormat = {"type": "json_object"} responseFormat = {"type": "json_object"}
if "gpt-5" in model: params["response_format"] = responseFormat
response = openai.chat.completions.create( response = openai.chat.completions.create(**params)
model=model,
response_format=responseFormat,
messages=msg,
reasoning_effort="minimal"
)
else:
response = openai.chat.completions.create(
model=model,
response_format=responseFormat,
messages=msg,
temperature=0,
frequency_penalty=penalty
)
else: else:
raise e raise e
@ -567,7 +599,6 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
return [text, [0, 0]] return [text, [0, 0]]
with open(config.logFilePath, "a+", encoding="utf-8") as logFile: with open(config.logFilePath, "a+", encoding="utf-8") as logFile:
mismatch = False
totalTokens = [0, 0] totalTokens = [0, 0]
if isinstance(text, list): if isinstance(text, list):
@ -612,97 +643,116 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
totalTokens[1] += estimate[1] totalTokens[1] += estimate[1]
continue continue
# Translate # --- Translation and Validation Retry Block ---
max_retries = 2 # 1 initial attempt + 2 retries
final_translations = None
last_raw_translation = ""
numLines = len(tItem) if isinstance(tItem, list) else None numLines = len(tItem) if isinstance(tItem, list) else None
response = translateText(system, user, history, 0.05, formatType, config.model, numLines)
translatedText = response.choices[0].message.content
# Retry if AI refused for attempt in range(max_retries + 1):
if not translatedText or '"error":' in translatedText: is_valid = True
response = translateText(
f"{system}\n You translate ALL content.", # On retries, add a note to the system prompt
user, history, 0.1, formatType, "gpt-4.1" if config.model == "gpt-5" else "gpt-4o", numLines current_system = system
) if attempt > 0:
current_system += f"\n\nIMPORTANT: Your previous attempt was incorrect or incomplete. Please ensure the entire output is translated to {config.language} and contains no untranslated characters. Translate the following text again, ensuring the JSON structure is correct."
if pbar:
pbar.write(f"Retrying translation... (Attempt {attempt + 1}/{max_retries + 1})")
# Translate
response = translateText(current_system, user, history, 0.05, formatType, config.model, numLines)
translatedText = response.choices[0].message.content translatedText = response.choices[0].message.content
last_raw_translation = translatedText
# Update token count # Update token count for this attempt
totalTokens[0] += response.usage.prompt_tokens totalTokens[0] += response.usage.prompt_tokens
totalTokens[1] += response.usage.completion_tokens totalTokens[1] += response.usage.completion_tokens
# Clean the translation first for consistency # Clean the translation first for consistency
translatedText = cleanTranslatedText(translatedText, config.language) cleaned_text = cleanTranslatedText(translatedText, config.language)
# Process translation result # Process and validate translation result
if translatedText: if cleaned_text:
if isinstance(tItem, list): if isinstance(tItem, list):
extractedTranslations = extractTranslation(translatedText, True, pbar) extracted = extractTranslation(cleaned_text, True, pbar)
if extractedTranslations is None or len(tItem) != len(extractedTranslations):
# Mismatch, try again # Check 1: Mismatch in length
response = translateText(system, user, history, 0.05, formatType, config.model, numLines) if extracted is None or len(tItem) != len(extracted):
translatedText = response.choices[0].message.content is_valid = False
totalTokens[0] += response.usage.prompt_tokens
totalTokens[1] += response.usage.completion_tokens # Check 2: Untranslated content
else:
# Clean and extract again for line in extracted:
translatedText = cleanTranslatedText(translatedText, config.language) if re.search(config.langRegex, str(line)):
extractedTranslations = extractTranslation(translatedText, True, pbar) is_valid = False
if extractedTranslations is None or len(tItem) != len(extractedTranslations): break
# Format the JSON output for consistent mismatch logging
formatted_mismatch_output = translatedText if is_valid:
try: final_translations = extracted
parsed_json = json.loads(translatedText)
formatted_mismatch_output = json.dumps(parsed_json, indent=4, ensure_ascii=False)
except (json.JSONDecodeError, ValueError):
pass
# Log mismatch
with open(config.mismatchLogPath, "a+", encoding="utf-8") as mismatchFile:
mismatchFile.write(f"Mismatch: {filename}\n")
mismatchFile.write(f"Input:\n{subbedT}\n")
mismatchFile.write(f"Output:\n{formatted_mismatch_output}\n")
mismatch = True
# Format the JSON output for consistent logging
formatted_output = translatedText
try:
# Try to parse and reformat the JSON for consistent formatting
parsed_json = json.loads(translatedText)
formatted_output = json.dumps(parsed_json, indent=4, ensure_ascii=False)
except (json.JSONDecodeError, ValueError):
# If it's not valid JSON, keep the original
pass
logFile.write(f"Input:\n{subbedT}\n")
logFile.write(f"Output:\n{formatted_output}\n")
# Set results if no mismatch
if not mismatch:
tList[index] = extractedTranslations
history = extractedTranslations[-config.maxHistory:]
else: else:
history = text[-config.maxHistory:] if isinstance(text, list) else text # Check for untranslated content in single string
mismatch = False if re.search(config.langRegex, cleaned_text):
if filename and mismatchList is not None and filename not in mismatchList: is_valid = False
mismatchList.append(filename) else:
final_translations = cleaned_text.replace("Placeholder Text", "")
# Update progress bar
if lock and pbar is not None:
with lock:
pbar.update(len(tItem))
else: else:
# Single string translation - clean after getting the raw response is_valid = False
cleanedText = cleanTranslatedText(translatedText, config.language) if pbar: pbar.write(f"AI Refused: {tItem}\n")
tList[index] = cleanedText.replace("Placeholder Text", "")
else: # If translation is valid, break the retry loop
if pbar: if is_valid:
pbar.write(f"AI Refused: {tItem}\n") break
# --- End of Retry Block ---
# After the loop, handle the final result
if final_translations is not None: # Success case
formatted_output = last_raw_translation
try:
parsed_json = json.loads(last_raw_translation)
formatted_output = json.dumps(parsed_json, indent=4, ensure_ascii=False)
except (json.JSONDecodeError, ValueError):
pass
logFile.write(f"Input:\n{subbedT}\n")
logFile.write(f"Output:\n{formatted_output}\n")
if isinstance(tItem, list):
tList[index] = final_translations
history = final_translations[-config.maxHistory:]
else:
tList[index] = final_translations
history = final_translations
if lock and pbar is not None:
with lock:
pbar.update(len(tItem) if isinstance(tItem, list) else 1)
else: # Failure case after all retries
if pbar: pbar.write(f"Translation failed after {max_retries + 1} attempts. Check mismatch log.")
formatted_mismatch_output = last_raw_translation
try:
parsed_json = json.loads(last_raw_translation)
formatted_mismatch_output = json.dumps(parsed_json, indent=4, ensure_ascii=False)
except (json.JSONDecodeError, ValueError):
pass
with open(config.mismatchLogPath, "a+", encoding="utf-8") as mismatchFile:
mismatchFile.write(f"Failed after retries: {filename}\n")
mismatchFile.write(f"Input:\n{subbedT}\n")
mismatchFile.write(f"Final Output:\n{formatted_mismatch_output}\n")
if filename and mismatchList is not None and filename not in mismatchList:
mismatchList.append(filename)
tList[index] = tItem
history = text[-config.maxHistory:] if isinstance(text, list) else text
# Combine if multilist # Combine if multilist
if isinstance(tList[0], list): if tList and isinstance(tList[0], list):
tList = [t for sublist in tList for t in sublist] tList = [t for sublist in tList for t in sublist]
# Return result # Return result
if formatType == "json": if formatType == "json":
return [tList, totalTokens] return [tList, totalTokens]
else: else:
return [tList[0], totalTokens] return [tList[0], totalTokens]