Translates an XML exam file from German to English.
This function searches for ... tags
and translates their content using an LLM, preserving tags and entities.
Parameters:
| Name |
Type |
Description |
Default |
input_path
|
Union[str, Path]
|
Path to the German XML exam file.
|
required
|
llm_client
|
Optional[LLMClient]
|
LLMClient instance. If None, a new one is created.
|
None
|
output_path
|
Optional[Union[str, Path]]
|
Path for the English output. If None, a suffix "_engl" is added.
|
None
|
verbose
|
bool
|
If True, prints detailed translation progress.
|
False
|
Returns:
| Type |
Description |
str
|
Path to the saved English XML file.
|
Source code in src/academic_doc_generator/exam_translator/xml_translator.py
| def translate_xml_exam(
input_path: Union[str, Path],
llm_client: Optional[LLMClient] = None,
output_path: Optional[Union[str, Path]] = None,
verbose: bool = False,
) -> str:
"""Translates an XML exam file from German to English.
This function searches for <mattext texttype="text/xhtml">...</mattext> tags
and translates their content using an LLM, preserving tags and entities.
Args:
input_path: Path to the German XML exam file.
llm_client: LLMClient instance. If None, a new one is created.
output_path: Path for the English output. If None, a suffix "_engl" is added.
verbose: If True, prints detailed translation progress.
Returns:
Path to the saved English XML file.
"""
logger = setup_exam_translator_logger()
logger.info("=== Starte Übersetzung von XML-Klausur ===")
logger.info(f"Input: {input_path}")
if llm_client is None:
llm_client = LLMClient(api_choice="kiconnect", llm="openai-gpt-oss-120b")
print(f"✓ LLM: {llm_client.api_choice} / {llm_client.llm}")
input_path = Path(input_path)
if not input_path.exists():
logger.error(f"File not found: {input_path}")
raise FileNotFoundError(f"File not found: {input_path}")
if output_path is None:
stem = input_path.stem
suffix = input_path.suffix
output_path = input_path.parent / f"{stem}_engl{suffix}"
else:
output_path = Path(output_path)
logger.info(f"Output: {output_path}")
print(f"\n📄 Reading XML file: {input_path}")
with open(input_path, encoding="utf-8") as f:
content = f.read()
# Regex to find mattext tags with xhtml texttype
# We use a non-greedy match for the content
pattern = re.compile(r'(<mattext texttype="text/xhtml">)(.*?)(</mattext>)', re.DOTALL)
matches = list(pattern.finditer(content))
print(f"🔍 Found {len(matches)} sections to translate.")
logger.info(f"Anzahl gefundener XML-Abschnitte: {len(matches)}")
# We process in reverse to not mess up indices if we were doing string slicing,
# but here we'll use re.sub with a callback for safety and simplicity.
count = 0
def replace_match(match):
nonlocal count
count += 1
prefix = match.group(1)
inner_text = match.group(2)
suffix = match.group(3)
if not inner_text.strip():
return match.group(0)
print(f" [{count}/{len(matches)}] Translating section...")
logger.info(f"--- Starte Übersetzung von XML-Abschnitt {count}/{len(matches)} ---")
logger.info(f"Original XML-Abschnitt:\n{inner_text}")
prompt = build_prompt(PromptTemplate.TRANSLATE_XML_XHTML, text=inner_text)
messages = [{"role": "user", "content": prompt}]
translated_text = llm_client.chat_completion(messages).strip()
logger.info(f"Übersetzter XML-Abschnitt:\n{translated_text}")
logger.info(f"--- Ende Übersetzung von XML-Abschnitt {count}/{len(matches)} ---\n")
if verbose:
print(f"--- Original ---\n{inner_text}\n--- Translated ---\n{translated_text}\n")
return f"{prefix}{translated_text}{suffix}"
final_content = pattern.sub(replace_match, content)
print(f"\n💾 Saving English version: {output_path}")
with open(output_path, "w", encoding="utf-8") as f:
f.write(final_content)
print("✅ Translation completed!")
logger.info("✅ XML-Übersetzung erfolgreich abgeschlossen!")
logger.info(f"Gespeichert unter: {output_path}")
logger.info("============================================\n")
return str(output_path)
|