Spaces:
Running
Running
Update backend/lens_core.py
Browse files- backend/lens_core.py +138 -112
backend/lens_core.py
CHANGED
|
@@ -187,94 +187,115 @@ AI_MODEL_ALIASES = {
|
|
| 187 |
}
|
| 188 |
}
|
| 189 |
|
| 190 |
-
AI_PROMPT_SYSTEM_BASE =
|
| 191 |
-
"You are a professional manga translator and dialogue localizer.\n"
|
| 192 |
-
"Rewrite each paragraph as natural dialogue in the target language while preserving meaning, tone, intent, and character voice.\n"
|
| 193 |
-
"Keep lines concise for speech bubbles. Do not add new information. Do not omit meaning. Do not explain.\n"
|
| 194 |
-
"Preserve emphasis (… ! ?). Avoid excessive punctuation.\n"
|
| 195 |
-
"If the input is already in the target language, improve it (dialogue polish) without changing meaning."
|
| 196 |
-
)
|
| 197 |
|
| 198 |
-
AI_LANG_STYLE = {
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
"
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
-
AI_PROMPT_USER_BY_LANG = {
|
| 228 |
-
"th": """Thai manga translation guidelines (OCR input)
|
| 229 |
-
|
| 230 |
-
Goal: Produce Thai text that reads like a skilled Thai manga translator: natural, concise, and faithful to tone/intent, without guessing wildly.
|
| 231 |
-
|
| 232 |
-
A) Identify the type of text and translate accordingly
|
| 233 |
-
- Narration / inner monologue: smooth Thai narration, natural flow.
|
| 234 |
-
- Spoken dialogue: real spoken Thai, short and punchy for speech bubbles.
|
| 235 |
-
- Labels / status / announcements / UI text: short, clear, list-like formatting when appropriate.
|
| 236 |
-
|
| 237 |
-
B) Character voice & register
|
| 238 |
-
- Match intensity (calm / angry / teasing / rude) but do not add extra rudeness that is not present.
|
| 239 |
-
- Use particles/interjections only when they help the voice; do not overuse.
|
| 240 |
-
- Keep SFX / elongated sounds manga-like (elongation, repetition) but not excessively long.
|
| 241 |
-
|
| 242 |
-
C) Addressing, pronouns, and gendered endings
|
| 243 |
-
- Default: omit pronouns and omit gendered polite sentence-final particles unless the source line clearly requires them.
|
| 244 |
-
- Never use a male-coded second-person pronoun.
|
| 245 |
-
- When a line addresses someone by name, keep the name and write the sentence without inserting a second-person pronoun after the name. Prefer: NAME + sentence.
|
| 246 |
-
- If a second-person reference is truly needed for readability, pick a neutral/casual option appropriate to tone, and keep it gender-neutral; do not guess gender from the name alone.
|
| 247 |
-
- Do not guess speaker gender. Only use clearly gendered first-person forms or gendered sentence endings when the same source line strongly signals them. Keep consistency within the line and never mix conflicting forms.
|
| 248 |
-
|
| 249 |
-
D) OCR noise / incomplete words (be conservative)
|
| 250 |
-
- OCR may drop/swap letters or insert duplicates. Fix ONLY when it is high-confidence and obvious (1–2 characters off and the intended word is clear).
|
| 251 |
-
- Do not “correct” words that already look valid. Do not over-correct names, terms, or stylistic spellings.
|
| 252 |
-
- If uncertain, keep the original token or transliterate; do not invent a different word.
|
| 253 |
-
|
| 254 |
-
E) Proper nouns & recurring terms
|
| 255 |
-
- Keep character names, places, skills, and key terms consistent across the page.
|
| 256 |
-
- Preserve honorifics only when present and meaningful.
|
| 257 |
-
|
| 258 |
-
Do not add explanations. Return only the translated Thai text, preserving paragraph boundaries and order.""".strip(),
|
| 259 |
-
"en": """Style preferences:
|
| 260 |
-
- Keep English dialogue concise and conversational.
|
| 261 |
-
- Keep lines short for speech bubbles.
|
| 262 |
-
- Keep names and recurring terms consistent.
|
| 263 |
-
- Keep SFX short; avoid very long repeated characters.
|
| 264 |
-
""".strip(),
|
| 265 |
-
"ja": """Style preferences:
|
| 266 |
-
- Keep Japanese dialogue concise and natural for manga.
|
| 267 |
-
- Keep lines short for speech bubbles.
|
| 268 |
-
- Keep names and recurring terms consistent.
|
| 269 |
-
- Keep SFX short; avoid very long repeated characters.
|
| 270 |
-
""".strip(),
|
| 271 |
-
"default": """Style preferences:
|
| 272 |
-
- Keep dialogue concise, spoken, and faithful to tone.
|
| 273 |
-
- Keep lines short for speech bubbles.
|
| 274 |
-
- Keep names and recurring terms consistent.
|
| 275 |
-
- Keep SFX short; avoid very long repeated characters.
|
| 276 |
-
""".strip(),
|
| 277 |
-
}
|
| 278 |
|
| 279 |
AI_PROMPT_RESPONSE_CONTRACT_JSON = (
|
| 280 |
"Return ONLY valid JSON (no markdown, no extra text).\n"
|
|
@@ -312,9 +333,10 @@ _FONT_PAIR_CACHE = {}
|
|
| 312 |
_TP_HTML_EPS_PX = 0.0
|
| 313 |
ZWSP = "\u200b"
|
| 314 |
|
| 315 |
-
def ai_prompt_user_default(lang: str) -> str:
|
| 316 |
l = _normalize_lang(lang)
|
| 317 |
-
|
|
|
|
| 318 |
|
| 319 |
def _active_ai_contract() -> str:
|
| 320 |
return AI_PROMPT_RESPONSE_CONTRACT_JSON if DO_AI_JSON else AI_PROMPT_RESPONSE_CONTRACT_TEXT
|
|
@@ -421,22 +443,22 @@ def _save_ai_cache(path: str, cache: dict):
|
|
| 421 |
json.dump(cache, f, ensure_ascii=False)
|
| 422 |
os.replace(tmp, path)
|
| 423 |
|
| 424 |
-
def _build_ai_prompt_packet(target_lang: str, original_text_full: str):
|
| 425 |
lang = _normalize_lang(target_lang)
|
| 426 |
-
|
| 427 |
-
|
| 428 |
output_schema = json.dumps({"aiTextFull": "..."}, ensure_ascii=False)
|
| 429 |
data_template = _active_ai_data_template()
|
| 430 |
if DO_AI_JSON:
|
| 431 |
-
data_text = data_template.format(
|
| 432 |
-
input_json=input_json, output_schema=output_schema)
|
| 433 |
else:
|
| 434 |
data_text = data_template.format(input_json=input_json)
|
| 435 |
|
| 436 |
-
|
| 437 |
-
|
|
|
|
| 438 |
|
| 439 |
-
system_parts = [
|
| 440 |
if style:
|
| 441 |
system_parts.append(style)
|
| 442 |
system_parts.append(_active_ai_contract())
|
|
@@ -448,6 +470,7 @@ def _build_ai_prompt_packet(target_lang: str, original_text_full: str):
|
|
| 448 |
user_parts.append(data_text)
|
| 449 |
return system_text, user_parts
|
| 450 |
|
|
|
|
| 451 |
def _gemini_generate_json(api_key: str, model: str, system_text: str, user_parts: list[str]):
|
| 452 |
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
|
| 453 |
parts = [{"text": p} for p in user_parts if (p or "").strip()]
|
|
@@ -479,6 +502,7 @@ def _gemini_generate_json(api_key: str, model: str, system_text: str, user_parts
|
|
| 479 |
raise Exception("Gemini returned empty text")
|
| 480 |
return txt
|
| 481 |
|
|
|
|
| 482 |
def _read_first_env(*names: str) -> str:
|
| 483 |
for n in names:
|
| 484 |
v = (os.environ.get(n) or "").strip()
|
|
@@ -1522,15 +1546,20 @@ def ai_translate_original_text(original_text_full: str, target_lang: str):
|
|
| 1522 |
if not api_key:
|
| 1523 |
raise Exception("AI_API_KEY is required for AI translation")
|
| 1524 |
|
|
|
|
| 1525 |
lang = _normalize_lang(target_lang)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1526 |
prompt_sig = _sha1(
|
| 1527 |
json.dumps(
|
| 1528 |
{
|
| 1529 |
-
"sys":
|
| 1530 |
-
"edit":
|
| 1531 |
"contract": _active_ai_contract(),
|
| 1532 |
"data": _active_ai_data_template(),
|
| 1533 |
-
"style":
|
| 1534 |
},
|
| 1535 |
ensure_ascii=False,
|
| 1536 |
)
|
|
@@ -1542,8 +1571,7 @@ def ai_translate_original_text(original_text_full: str, target_lang: str):
|
|
| 1542 |
cache = _load_ai_cache(AI_CACHE_PATH)
|
| 1543 |
cache_key = _sha1(
|
| 1544 |
json.dumps(
|
| 1545 |
-
{"provider": provider, "m": model, "u": base_url,
|
| 1546 |
-
"l": lang, "p": prompt_sig, "t": original_text_full},
|
| 1547 |
ensure_ascii=False,
|
| 1548 |
)
|
| 1549 |
)
|
|
@@ -1552,9 +1580,9 @@ def ai_translate_original_text(original_text_full: str, target_lang: str):
|
|
| 1552 |
if lang == "th" and cached:
|
| 1553 |
t = str(cached.get("aiTextFull") or "")
|
| 1554 |
if t:
|
| 1555 |
-
t2 = re.sub(r"(?:(?<=^)|(?<=[\s\"'“”‘’()\[\]{}<>]))
|
| 1556 |
-
t2 = re.sub(r"[
|
| 1557 |
-
t2 = re.sub(r"^[
|
| 1558 |
if t2 != t:
|
| 1559 |
cached = dict(cached)
|
| 1560 |
cached["aiTextFull"] = t2
|
|
@@ -1562,7 +1590,7 @@ def ai_translate_original_text(original_text_full: str, target_lang: str):
|
|
| 1562 |
_save_ai_cache(AI_CACHE_PATH, cache)
|
| 1563 |
return cached
|
| 1564 |
|
| 1565 |
-
system_text, user_parts = _build_ai_prompt_packet(lang, original_text_full)
|
| 1566 |
|
| 1567 |
started = time.time()
|
| 1568 |
used_model = model
|
|
@@ -1571,16 +1599,14 @@ def ai_translate_original_text(original_text_full: str, target_lang: str):
|
|
| 1571 |
elif provider == "anthropic":
|
| 1572 |
raw = _anthropic_generate_json(api_key, model, system_text, user_parts)
|
| 1573 |
else:
|
| 1574 |
-
raw, used_model = _openai_compat_generate_json(
|
| 1575 |
-
api_key, base_url, model, system_text, user_parts)
|
| 1576 |
|
| 1577 |
-
ai_text_full = _parse_ai_textfull_only(
|
| 1578 |
-
raw) if DO_AI_JSON else _parse_ai_textfull_text_only(raw)
|
| 1579 |
|
| 1580 |
if lang == "th" and ai_text_full:
|
| 1581 |
-
ai_text_full = re.sub(r"(?:(?<=^)|(?<=[\s\"'“”‘’()\[\]{}<>]))
|
| 1582 |
-
ai_text_full = re.sub(r"[
|
| 1583 |
-
ai_text_full = re.sub(r"^[
|
| 1584 |
|
| 1585 |
result = {
|
| 1586 |
"aiTextFull": ai_text_full,
|
|
|
|
| 187 |
}
|
| 188 |
}
|
| 189 |
|
| 190 |
+
AI_PROMPT_SYSTEM_BASE = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
+
AI_LANG_STYLE = {"default": ""}
|
| 193 |
+
|
| 194 |
+
AI_PROMPT_USER_BY_LANG = {"default": ""}
|
| 195 |
+
|
| 196 |
+
TP_REMOTE_DEFAULTS_URL = (
|
| 197 |
+
os.environ.get("TP_REMOTE_DEFAULTS_URL")
|
| 198 |
+
or "https://raw.githubusercontent.com/Kuju29/TextPhantomOCR_Overlay/refs/heads/main/defaults_api.json"
|
| 199 |
+
).strip()
|
| 200 |
+
TP_REMOTE_DEFAULTS_TIMEOUT_SEC = float(os.environ.get("TP_REMOTE_DEFAULTS_TIMEOUT_SEC", "2"))
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def _remote_defaults() -> dict:
|
| 204 |
+
url = TP_REMOTE_DEFAULTS_URL
|
| 205 |
+
if not url:
|
| 206 |
+
raise RuntimeError("TP_REMOTE_DEFAULTS_URL is required")
|
| 207 |
+
|
| 208 |
+
if url.startswith("file://"):
|
| 209 |
+
with open(url[len("file://"):], "r", encoding="utf-8") as f:
|
| 210 |
+
raw = f.read()
|
| 211 |
+
else:
|
| 212 |
+
with httpx.Client(timeout=TP_REMOTE_DEFAULTS_TIMEOUT_SEC) as client:
|
| 213 |
+
r = client.get(
|
| 214 |
+
url,
|
| 215 |
+
headers={"accept": "application/json"},
|
| 216 |
+
follow_redirects=True,
|
| 217 |
+
)
|
| 218 |
+
r.raise_for_status()
|
| 219 |
+
raw = r.text
|
| 220 |
+
|
| 221 |
+
data = json.loads((raw or "").strip() or "{}")
|
| 222 |
+
if not isinstance(data, dict) or not data:
|
| 223 |
+
raise RuntimeError("Remote defaults is empty")
|
| 224 |
+
return data
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _remote_first_str(data: dict, *keys: str) -> str:
|
| 228 |
+
if not data:
|
| 229 |
+
return ""
|
| 230 |
+
for k in keys:
|
| 231 |
+
v = data.get(k)
|
| 232 |
+
if isinstance(v, str) and v.strip():
|
| 233 |
+
return v.strip()
|
| 234 |
+
return ""
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def _remote_first_map(data: dict, *keys: str) -> dict:
|
| 238 |
+
if not data:
|
| 239 |
+
return {}
|
| 240 |
+
for k in keys:
|
| 241 |
+
v = data.get(k)
|
| 242 |
+
if isinstance(v, dict) and v:
|
| 243 |
+
return v
|
| 244 |
+
return {}
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def ai_prompt_system_base(data: dict | None = None) -> str:
|
| 248 |
+
d = data if isinstance(data, dict) else _remote_defaults()
|
| 249 |
+
v = _remote_first_str(
|
| 250 |
+
d,
|
| 251 |
+
"AI_PROMPT_SYSTEM_BASE",
|
| 252 |
+
"aiPromptSystemBase",
|
| 253 |
+
"promptSystemBase",
|
| 254 |
+
"systemBase",
|
| 255 |
+
)
|
| 256 |
+
if not v:
|
| 257 |
+
raise RuntimeError("Missing AI_PROMPT_SYSTEM_BASE in remote defaults")
|
| 258 |
+
return v
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def ai_lang_style_map(data: dict | None = None) -> dict[str, str]:
|
| 262 |
+
d = data if isinstance(data, dict) else _remote_defaults()
|
| 263 |
+
remote = _remote_first_map(d, "AI_LANG_STYLE", "aiLangStyle", "langStyle")
|
| 264 |
+
if not remote:
|
| 265 |
+
raise RuntimeError("Missing AI_LANG_STYLE in remote defaults")
|
| 266 |
+
out: dict[str, str] = {}
|
| 267 |
+
for k, v in remote.items():
|
| 268 |
+
if not isinstance(k, str) or not isinstance(v, str):
|
| 269 |
+
continue
|
| 270 |
+
kk = _normalize_lang(k)
|
| 271 |
+
if not kk:
|
| 272 |
+
continue
|
| 273 |
+
out[kk] = v.strip()
|
| 274 |
+
out.setdefault("default", "")
|
| 275 |
+
return out
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def ai_prompt_user_by_lang_map(data: dict | None = None) -> dict[str, str]:
|
| 279 |
+
d = data if isinstance(data, dict) else _remote_defaults()
|
| 280 |
+
remote = _remote_first_map(
|
| 281 |
+
d,
|
| 282 |
+
"AI_PROMPT_USER_BY_LANG",
|
| 283 |
+
"aiPromptUserByLang",
|
| 284 |
+
"promptUserByLang",
|
| 285 |
+
)
|
| 286 |
+
if not remote:
|
| 287 |
+
raise RuntimeError("Missing AI_PROMPT_USER_BY_LANG in remote defaults")
|
| 288 |
+
out: dict[str, str] = {}
|
| 289 |
+
for k, v in remote.items():
|
| 290 |
+
if not isinstance(k, str) or not isinstance(v, str):
|
| 291 |
+
continue
|
| 292 |
+
kk = _normalize_lang(k)
|
| 293 |
+
if not kk:
|
| 294 |
+
continue
|
| 295 |
+
out[kk] = v.strip()
|
| 296 |
+
out.setdefault("default", "")
|
| 297 |
+
return out
|
| 298 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
|
| 300 |
AI_PROMPT_RESPONSE_CONTRACT_JSON = (
|
| 301 |
"Return ONLY valid JSON (no markdown, no extra text).\n"
|
|
|
|
| 333 |
_TP_HTML_EPS_PX = 0.0
|
| 334 |
ZWSP = "\u200b"
|
| 335 |
|
| 336 |
+
def ai_prompt_user_default(lang: str, data: dict | None = None) -> str:
|
| 337 |
l = _normalize_lang(lang)
|
| 338 |
+
m = ai_prompt_user_by_lang_map(data)
|
| 339 |
+
return (m.get(l) or m.get("default") or "").strip()
|
| 340 |
|
| 341 |
def _active_ai_contract() -> str:
|
| 342 |
return AI_PROMPT_RESPONSE_CONTRACT_JSON if DO_AI_JSON else AI_PROMPT_RESPONSE_CONTRACT_TEXT
|
|
|
|
| 443 |
json.dump(cache, f, ensure_ascii=False)
|
| 444 |
os.replace(tmp, path)
|
| 445 |
|
| 446 |
+
def _build_ai_prompt_packet(target_lang: str, original_text_full: str, defaults: dict | None = None):
|
| 447 |
lang = _normalize_lang(target_lang)
|
| 448 |
+
d = defaults if isinstance(defaults, dict) else _remote_defaults()
|
| 449 |
+
input_json = json.dumps({"target_lang": lang, "originalTextFull": original_text_full}, ensure_ascii=False)
|
| 450 |
output_schema = json.dumps({"aiTextFull": "..."}, ensure_ascii=False)
|
| 451 |
data_template = _active_ai_data_template()
|
| 452 |
if DO_AI_JSON:
|
| 453 |
+
data_text = data_template.format(input_json=input_json, output_schema=output_schema)
|
|
|
|
| 454 |
else:
|
| 455 |
data_text = data_template.format(input_json=input_json)
|
| 456 |
|
| 457 |
+
styles = ai_lang_style_map(d)
|
| 458 |
+
style = styles.get(lang) or styles.get("default") or ""
|
| 459 |
+
editable = (ai_prompt_user_default(lang, d) or "").strip()
|
| 460 |
|
| 461 |
+
system_parts = [ai_prompt_system_base(d)]
|
| 462 |
if style:
|
| 463 |
system_parts.append(style)
|
| 464 |
system_parts.append(_active_ai_contract())
|
|
|
|
| 470 |
user_parts.append(data_text)
|
| 471 |
return system_text, user_parts
|
| 472 |
|
| 473 |
+
|
| 474 |
def _gemini_generate_json(api_key: str, model: str, system_text: str, user_parts: list[str]):
|
| 475 |
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
|
| 476 |
parts = [{"text": p} for p in user_parts if (p or "").strip()]
|
|
|
|
| 502 |
raise Exception("Gemini returned empty text")
|
| 503 |
return txt
|
| 504 |
|
| 505 |
+
|
| 506 |
def _read_first_env(*names: str) -> str:
|
| 507 |
for n in names:
|
| 508 |
v = (os.environ.get(n) or "").strip()
|
|
|
|
| 1546 |
if not api_key:
|
| 1547 |
raise Exception("AI_API_KEY is required for AI translation")
|
| 1548 |
|
| 1549 |
+
defaults = _remote_defaults()
|
| 1550 |
lang = _normalize_lang(target_lang)
|
| 1551 |
+
styles = ai_lang_style_map(defaults)
|
| 1552 |
+
edit_map = ai_prompt_user_by_lang_map(defaults)
|
| 1553 |
+
sys_base = ai_prompt_system_base(defaults)
|
| 1554 |
+
|
| 1555 |
prompt_sig = _sha1(
|
| 1556 |
json.dumps(
|
| 1557 |
{
|
| 1558 |
+
"sys": sys_base,
|
| 1559 |
+
"edit": edit_map,
|
| 1560 |
"contract": _active_ai_contract(),
|
| 1561 |
"data": _active_ai_data_template(),
|
| 1562 |
+
"style": styles.get(lang) or styles.get("default") or "",
|
| 1563 |
},
|
| 1564 |
ensure_ascii=False,
|
| 1565 |
)
|
|
|
|
| 1571 |
cache = _load_ai_cache(AI_CACHE_PATH)
|
| 1572 |
cache_key = _sha1(
|
| 1573 |
json.dumps(
|
| 1574 |
+
{"provider": provider, "m": model, "u": base_url, "l": lang, "p": prompt_sig, "t": original_text_full},
|
|
|
|
| 1575 |
ensure_ascii=False,
|
| 1576 |
)
|
| 1577 |
)
|
|
|
|
| 1580 |
if lang == "th" and cached:
|
| 1581 |
t = str(cached.get("aiTextFull") or "")
|
| 1582 |
if t:
|
| 1583 |
+
t2 = re.sub(r"(?:(?<=^)|(?<=[\s\"'“”‘’()\[\]{}<>]))นาย(?=(?:\s|$))", "", t)
|
| 1584 |
+
t2 = re.sub(r"[ ]{2,}", " ", t2)
|
| 1585 |
+
t2 = re.sub(r"^[ ]+", "", t2, flags=re.MULTILINE)
|
| 1586 |
if t2 != t:
|
| 1587 |
cached = dict(cached)
|
| 1588 |
cached["aiTextFull"] = t2
|
|
|
|
| 1590 |
_save_ai_cache(AI_CACHE_PATH, cache)
|
| 1591 |
return cached
|
| 1592 |
|
| 1593 |
+
system_text, user_parts = _build_ai_prompt_packet(lang, original_text_full, defaults)
|
| 1594 |
|
| 1595 |
started = time.time()
|
| 1596 |
used_model = model
|
|
|
|
| 1599 |
elif provider == "anthropic":
|
| 1600 |
raw = _anthropic_generate_json(api_key, model, system_text, user_parts)
|
| 1601 |
else:
|
| 1602 |
+
raw, used_model = _openai_compat_generate_json(api_key, base_url, model, system_text, user_parts)
|
|
|
|
| 1603 |
|
| 1604 |
+
ai_text_full = _parse_ai_textfull_only(raw) if DO_AI_JSON else _parse_ai_textfull_text_only(raw)
|
|
|
|
| 1605 |
|
| 1606 |
if lang == "th" and ai_text_full:
|
| 1607 |
+
ai_text_full = re.sub(r"(?:(?<=^)|(?<=[\s\"'“”‘’()\[\]{}<>]))นาย(?=(?:\s|$))", "", ai_text_full)
|
| 1608 |
+
ai_text_full = re.sub(r"[ ]{2,}", " ", ai_text_full)
|
| 1609 |
+
ai_text_full = re.sub(r"^[ ]+", "", ai_text_full, flags=re.MULTILINE)
|
| 1610 |
|
| 1611 |
result = {
|
| 1612 |
"aiTextFull": ai_text_full,
|