File size: 7,049 Bytes
ba8abef | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | #!/usr/bin/env python3
"""Prompting + decode settings for Qwen3-4B translation."""
from __future__ import annotations
import re
import time
from openai import OpenAI
SYSTEM = (
"You are a professional translator for invoices, notices, medical instructions, "
"and news. Translate faithfully into the requested target language.\n"
"Rules:\n"
"- Output only the translation. No quotes, labels, romanization notes, or commentary.\n"
"- Copy the listed preserve tokens exactly (IDs, emails, amounts, Latin brand names, acronyms).\n"
"- If a preserve token is already Latin, keep it in Latin letters; do not transliterate it.\n"
"- Do not replace a proper name with a similar-sounding different name.\n"
"- receipt ≠ invoice. receipt → Arabic إيصال, French reçu, German Quittung, Spanish recibo. "
"invoice → Arabic فاتورة, French facture, German Rechnung, Spanish factura, Italian fattura, "
"Portuguese fatura, Turkish fatura, Greek τιμολόγιο, Russian счёт.\n"
"- digital identity → Arabic هوية رقمية (not اعتماد).\n"
"- In a banking context, Arabic حوّل / حول المبلغ إلى الحساب means transfer (wire) money, "
"not convert a currency, unless the source is clearly about an exchange rate.\n"
"- Keep technical acronyms GPU, OCR, CPU, CUDA, API in Latin letters when the source uses them.\n"
"- Modern Standard Arabic; keep the definite article on city names (الرياض، الدوحة، أبوظبي).\n"
"- Translate meaning, but never drop a number, date, or identifier."
)
_ID = re.compile(r"\b[A-Z]{2,}[A-Z0-9._/-]*\d[A-Z0-9._/-]*\b")
_EMAIL = re.compile(r"\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b")
_ACRONYM = re.compile(
r"\b(?:GPU|OCR|CPU|CUDA|API|AWB|LLC|USD|AED|EUR|VAT|TTC|IBAN|SWIFT|SKU|PO)\b"
)
_HYPHEN_NAME = re.compile(r"\b[A-Z][a-z]+-[A-Z][a-z]+\b")
_NAME_RUN = re.compile(
r"\b[A-Z][a-z]+(?:\s+(?:and|&|of|de|del|al|the)\s+[A-Z][a-z]+)*(?:\s+[A-Z][a-z]+){1,3}\b"
)
_SKIP_NAMES = {
"The Total", "The Patient", "Please Reset", "Please Send", "Please Pay",
"On August", "Each Page", "This Stainless", "Dear Mr",
}
def copy_spans(source: str) -> list[str]:
found: list[str] = []
for rx in (_EMAIL, _ID, _ACRONYM, _HYPHEN_NAME):
found.extend(rx.findall(source))
for m in _NAME_RUN.finditer(source):
span = m.group(0)
if span in _SKIP_NAMES:
continue
if span.lower().startswith(("the ", "this ", "please ", "dear ")):
continue
found.append(span)
out: list[str] = []
for tok in found:
if tok not in out:
out.append(tok)
return out[:12]
def term_hints(source: str, src_lang: str, tgt_lang: str) -> list[str]:
hints: list[str] = []
sl, tl = src_lang.lower(), tgt_lang.lower()
low = source.lower()
if tl == "arabic":
if re.search(r"\breceipts?\b", low):
hints.append("receipt → إيصال")
if re.search(r"\binvoices?\b", low):
hints.append("invoice → فاتورة")
if "identity" in low:
hints.append("digital identity → هوية رقمية")
if re.search(r"\b(gpu|ocr|cuda)\b", low):
hints.append("keep GPU / OCR / CUDA as Latin acronyms")
if "cafe" in low or "café" in low:
hints.append("Cafe in a brand name stays Latin (Harbor and Rye Cafe)")
if "tablet" in low:
hints.append("tablet → قرص")
if "customs" in low:
hints.append("customs officer → موظف الجمارك")
if re.search(r"\bgoods\b", low):
hints.append("goods → البضائع")
if "finance" in low:
hints.append("Finance department → قسم المالية")
if "flight" in low:
hints.append("flight → رحلة")
if sl == "arabic" and tl == "english":
if re.search(r"حو[ّ]?ل", source) and "حساب" in source:
hints.append("حوّل المبلغ إلى الحساب = transfer (not convert)")
if "إيصال" in source:
hints.append("إيصال = receipt")
if "فاتورة" in source:
hints.append("فاتورة = invoice")
if "هوية" in source:
hints.append("هوية = identity")
if "رحلة" in source and "مطار" in source:
hints.append("الرحلة from an airport = flight (not trip)")
if tl == "greek" and "invoice" in low:
hints.append("invoice → τιμολόγιο")
if tl == "russian" and "invoice" in low:
hints.append("invoice → счёт")
return hints
def prompt_for(case: dict) -> tuple[str, str]:
src, tgt = case["src_lang"], case["tgt_lang"]
preserve = list(case.get("preserve") or [])
for span in copy_spans(case["source"]):
if span not in preserve:
preserve.append(span)
hints = term_hints(case["source"], src, tgt)
lines = [
f"Translate from {src} to {tgt}.",
"Output only the translation.",
]
if preserve:
lines.append(
"Your translation MUST contain each of these strings verbatim: "
+ ", ".join(preserve)
+ "."
)
if hints:
lines.append("Term hints: " + "; ".join(hints) + ".")
lines.append("")
lines.append(case["source"])
return SYSTEM, "\n".join(lines)
def max_tokens_for(source: str) -> int:
return min(512, max(96, 48 + 4 * max(1, len(source.split()))))
def strip_fences(text: str) -> str:
text = (text or "").strip()
text = re.sub(r"^```(?:\w+)?\s*", "", text)
text = re.sub(r"\s*```$", "", text)
text = re.sub(
r"^(?:translation|arabic|english|french|german|spanish|italian|portuguese|"
r"turkish|russian|greek)\s*:\s*",
"",
text,
flags=re.I,
)
return text.strip().strip('"“”«»')
def translate_text(
client: OpenAI,
source: str,
src_lang: str,
tgt_lang: str,
model: str = "Qwen/Qwen3-4B-Instruct-2507",
preserve: list[str] | None = None,
) -> tuple[str, float]:
return translate(
client,
model,
{
"source": source,
"src_lang": src_lang,
"tgt_lang": tgt_lang,
"preserve": preserve or [],
},
)
def translate(client: OpenAI, model: str, case: dict) -> tuple[str, float]:
system, user = prompt_for(case)
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user},
]
kwargs = dict(
model=model,
temperature=0.0,
top_p=1.0,
max_tokens=max_tokens_for(case["source"]),
messages=messages,
)
t0 = time.perf_counter()
try:
resp = client.chat.completions.create(**kwargs, seed=0)
except TypeError:
resp = client.chat.completions.create(**kwargs)
dt = time.perf_counter() - t0
return strip_fences(resp.choices[0].message.content or ""), dt
|