Placeholders for Speaker codes and similar script

This commit is contained in:
dazedanon 2025-11-04 11:21:16 -06:00
parent c0e80c2d6c
commit 14821210db
2 changed files with 184 additions and 21 deletions

View file

@ -26,18 +26,29 @@ You will be translating erotic and sexual content. I will provide you with lines
- `\\cself` is a variable for a string or number.
- Translate 'コイツ' as 'this bastard' or 'this bitch' depending on gender.
- Instead of translating a lone '回' as 'times' use 'x' instead so its shorter. Same with a lone '人'
- Do not translate text inside brackets for sound effects `\\SE`
**IMPORTANT - Placeholder Preservation:**
- Some text may contain special placeholders like `__PROTECTED_0__`, `__PROTECTED_1__`, etc.
- These placeholders represent game script codes (sound effects, music, etc.) that must NOT be modified.
- You MUST preserve these placeholders EXACTLY as they appear in the input:
* Do NOT translate them
* Do NOT remove them
* Do NOT modify them in any way
* Keep them in the same position in your translation
- Example: If input contains `__PROTECTED_0__`, your output must also contain `__PROTECTED_0__` in the appropriate position.
**Output Examples**
Input:
Input (with protected placeholders):
{
"Line1": "「\\SE[タイプライター]こいつからは幼女の匂いが しないでござる」",
"Line2": "「えぇ…\\c[7]こんなに可愛いのに…」"
"Line1": "「音楽が__PROTECTED_0__流れています」",
"Line2": "「そして__PROTECTED_1__効果音も鳴ります」"
}
Output:
Output (placeholders preserved exactly):
{
"Line1": "\"\\SE[タイプライター]This one doesn't smell like a little girl.\"",
"Line2": "\"Eh... \\c[7]but she's so cute...\""
"Line1": "\"The music __PROTECTED_0__ is playing.\"",
"Line2": "\"And the __PROTECTED_1__ sound effect is also playing.\""
}
Input:

View file

@ -15,6 +15,106 @@ from dotenv import load_dotenv
from pathlib import Path
from retry import retry
# ===== Placeholder Protection System =====
# Patterns to protect from translation (sound effects, control codes, etc.)
PROTECTED_PATTERNS = [
r'\\SE\[[^\]]+\]', # \SE[sound_effect_name]
r'\\ME\[[^\]]+\]', # \ME[music_effect_name]
r'\\BGM\[[^\]]+\]', # \BGM[background_music_name]
r'\\BGS\[[^\]]+\]', # \BGS[background_sound_name]
]
def protect_script_codes(text):
"""
Replace script codes (like \\SE[タイプライター]) with unique placeholders before translation.
Returns: (protected_text, replacements_dict)
"""
if not text or not isinstance(text, str):
return text, {}
replacements = {}
protected_text = text
counter = 0
# Combine all patterns
combined_pattern = '|'.join(f'({pattern})' for pattern in PROTECTED_PATTERNS)
def replace_match(match):
nonlocal counter
original = match.group(0)
# Create a unique placeholder that won't be translated
placeholder = f"__PROTECTED_{counter}__"
replacements[placeholder] = original
counter += 1
return placeholder
if combined_pattern:
protected_text = re.sub(combined_pattern, replace_match, protected_text)
return protected_text, replacements
def restore_script_codes(text, replacements):
"""
Restore protected script codes from placeholders after translation.
"""
if not text or not replacements:
return text
if isinstance(text, str):
result = text
for placeholder, original in replacements.items():
result = result.replace(placeholder, original)
return result
elif isinstance(text, list):
return [restore_script_codes(item, replacements) for item in text]
else:
return text
def validate_placeholders(original_text, translated_text, replacements):
"""
Validate that all placeholders from the original text appear in the translation.
Returns: (is_valid, missing_placeholders, extra_placeholders)
"""
if not replacements:
return True, [], []
# Get all placeholders
all_placeholders = set(replacements.keys())
# Count placeholders in original
original_counts = {}
for placeholder in all_placeholders:
if isinstance(original_text, str):
original_counts[placeholder] = original_text.count(placeholder)
elif isinstance(original_text, list):
original_counts[placeholder] = sum(str(item).count(placeholder) for item in original_text)
# Count placeholders in translation
translated_counts = {}
for placeholder in all_placeholders:
if isinstance(translated_text, str):
translated_counts[placeholder] = translated_text.count(placeholder)
elif isinstance(translated_text, list):
translated_counts[placeholder] = sum(str(item).count(placeholder) for item in translated_text)
# Find mismatches
missing = []
extra = []
for placeholder in all_placeholders:
orig_count = original_counts.get(placeholder, 0)
trans_count = translated_counts.get(placeholder, 0)
if trans_count < orig_count:
missing.append(f"{placeholder} (expected {orig_count}, found {trans_count})")
elif trans_count > orig_count:
extra.append(f"{placeholder} (expected {orig_count}, found {trans_count})")
is_valid = len(missing) == 0 and len(extra) == 0
return is_valid, missing, extra
# (from .env if present), strip accidental whitespace, and set the base URL,
# organization, and API key. It also handles the Gemini compatibility layer.
load_dotenv()
@ -752,20 +852,33 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
history = tItem[-config.maxHistory:] if isinstance(tItem, list) else tItem
continue
# Format for translation
# Protect script codes before translation
protected_items = []
all_replacements = {}
if isinstance(tItem, list):
for j in range(len(tItem)):
if not tItem[j] or not str(tItem[j]).strip():
tItem[j] = "Placeholder Text"
payload = {f"Line{i+1}": string for i, string in enumerate(tItem)}
protected_items.append("Placeholder Text")
all_replacements[j] = {}
else:
protected_text, replacements = protect_script_codes(tItem[j])
protected_items.append(protected_text)
all_replacements[j] = replacements
else:
if not tItem or not str(tItem).strip():
protected_items = "Placeholder Text"
all_replacements[0] = {}
else:
protected_items, all_replacements[0] = protect_script_codes(tItem)
# Format for translation
if isinstance(tItem, list):
payload = {f"Line{i+1}": string for i, string in enumerate(protected_items)}
payload = json.dumps(payload, indent=4, ensure_ascii=False)
subbedT = payload
else:
# Check for empty/whitespace strings in non-list items
if not tItem or not str(tItem).strip():
subbedT = "Placeholder Text"
else:
subbedT = tItem
subbedT = protected_items
# Check cache for this exact payload
cached_result = get_cached_translation(subbedT, config.language)
@ -812,7 +925,12 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
# On retries, add a note to the system prompt
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."
current_system += f"\n\nIMPORTANT: Your previous attempt was incorrect or incomplete. Please ensure:\n"
current_system += f"1. The entire output is translated to {config.language} with no untranslated characters\n"
current_system += f"2. The JSON structure is correct\n"
current_system += f"3. ALL placeholders (like __PROTECTED_0__, __PROTECTED_1__, etc.) are preserved EXACTLY as they appear in the input\n"
current_system += f" - Do not modify, translate, or remove any __PROTECTED_N__ placeholders\n"
current_system += f" - Keep them in the exact same position in your translation"
if pbar:
pbar.write(f"Retrying translation... (Attempt {attempt + 1}/{max_retries + 1})")
@ -836,13 +954,39 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
# Check 1: Mismatch in length -> still a hard failure
if extracted is None or len(tItem) != len(extracted):
is_valid = False
if pbar:
pbar.write(f"Length mismatch: expected {len(tItem)}, got {len(extracted) if extracted else 0}")
else:
# Set translations (line count matches)
final_translations = extracted
# Check 2: Validate placeholders are preserved
# Flatten all_replacements for batch validation
all_protected_text = protected_items # The list we sent
placeholder_valid, missing, extra = validate_placeholders(all_protected_text, extracted,
{k: v for replacements in all_replacements.values() for k, v in replacements.items()})
if not placeholder_valid:
is_valid = False
if pbar:
if missing:
pbar.write(f"Missing placeholders: {', '.join(missing)}")
if extra:
pbar.write(f"Extra placeholders: {', '.join(extra)}")
else:
# Set translations (line count matches and placeholders valid)
final_translations = extracted
else:
# Single string: accept output even if it contains characters
# matching langRegex (allow names or untranslated tokens).
final_translations = cleaned_text.replace("Placeholder Text", "")
# Single string: validate placeholders
placeholder_valid, missing, extra = validate_placeholders(protected_items, cleaned_text, all_replacements[0])
if not placeholder_valid:
is_valid = False
if pbar:
if missing:
pbar.write(f"Missing placeholders: {', '.join(missing)}")
if extra:
pbar.write(f"Extra placeholders: {', '.join(extra)}")
else:
# Accept output even if it contains characters matching langRegex
final_translations = cleaned_text.replace("Placeholder Text", "")
else:
is_valid = False
if pbar: pbar.write(f"AI Refused: {tItem}\n")
@ -855,6 +999,14 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None,
# After the loop, handle the final result
if final_translations is not None: # Success case
# Restore protected script codes
if isinstance(tItem, list):
for j in range(len(final_translations)):
if j in all_replacements:
final_translations[j] = restore_script_codes(final_translations[j], all_replacements[j])
else:
final_translations = restore_script_codes(final_translations, all_replacements[0])
formatted_output = last_raw_translation
try:
parsed_json = json.loads(last_raw_translation)