Spaces:
Running
Running
R3.2.8.6-R1.3C-RU2 Russian Contamination Adjudication and Target Preview State
#31
by GuXiaopeng - opened
- core/generation_runner.py +36 -5
- core/pipeline_agent.py +170 -7
- smartpaper_restore_bridge/frontend/index.html +36 -2
core/generation_runner.py
CHANGED
|
@@ -8,6 +8,7 @@ connections. Checkpoints are task-scoped, immutable-by-sequence JSON snapshots.
|
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import hashlib
|
|
|
|
| 11 |
import json
|
| 12 |
import logging
|
| 13 |
import math
|
|
@@ -33,7 +34,7 @@ ProgressCallback = Callable[..., None]
|
|
| 33 |
MIN_FINAL_REFERENCES = 50
|
| 34 |
LITERATURE_EVIDENCE_TARGET = 75
|
| 35 |
CITATION_PLAN_TARGET = 70
|
| 36 |
-
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-
|
| 37 |
|
| 38 |
|
| 39 |
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
|
@@ -299,10 +300,31 @@ class GenerationRunner:
|
|
| 299 |
partial = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=include_front)
|
| 300 |
self._publish_target_preview(partial, f'translating:{unit_name}', force=True, complete=False)
|
| 301 |
updated_sections = dict(translation_sections or {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
try:
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
except PipelineCancelledError:
|
| 307 |
raise
|
| 308 |
except Exception as error:
|
|
@@ -326,6 +348,9 @@ class GenerationRunner:
|
|
| 326 |
'Translation deferred until final gate | unit=%s code=%s error=%s',
|
| 327 |
unit_name, error_code, type(error).__name__,
|
| 328 |
)
|
|
|
|
|
|
|
|
|
|
| 329 |
return updated_sections, translation_glossary
|
| 330 |
updated_glossary = dict(result.get('glossary') or translation_glossary or {})
|
| 331 |
updated_sections[unit_key] = {
|
|
@@ -720,7 +745,13 @@ class GenerationRunner:
|
|
| 720 |
if working_abstract and not state.get("english_finalized"):
|
| 721 |
section_order_for_front = self._unique_top_level_sections(adaptive_structure)
|
| 722 |
if section_order_for_front:
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
translation_sections, translation_glossary = self._translate_frozen_unit(
|
| 725 |
agent, unit_key='__front_matter__', unit_name='Working Front Matter',
|
| 726 |
source_text=working_front_source, target_lang=target_lang,
|
|
|
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import hashlib
|
| 11 |
+
import inspect
|
| 12 |
import json
|
| 13 |
import logging
|
| 14 |
import math
|
|
|
|
| 34 |
MIN_FINAL_REFERENCES = 50
|
| 35 |
LITERATURE_EVIDENCE_TARGET = 75
|
| 36 |
CITATION_PLAN_TARGET = 70
|
| 37 |
+
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v2'
|
| 38 |
|
| 39 |
|
| 40 |
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
|
|
|
| 300 |
partial = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=include_front)
|
| 301 |
self._publish_target_preview(partial, f'translating:{unit_name}', force=True, complete=False)
|
| 302 |
updated_sections = dict(translation_sections or {})
|
| 303 |
+
|
| 304 |
+
def publish_translation_stage(stage):
|
| 305 |
+
value = str(stage or '').strip().casefold()
|
| 306 |
+
if value in {'qa', 'repairing'}:
|
| 307 |
+
preview_stage = f'translation_qa:{unit_name}'
|
| 308 |
+
else:
|
| 309 |
+
preview_stage = f'translating:{unit_name}'
|
| 310 |
+
self._publish_target_preview(partial, preview_stage, force=True, complete=False)
|
| 311 |
+
|
| 312 |
try:
|
| 313 |
+
translate_method = agent.translate_frozen_section
|
| 314 |
+
call_kwargs = {
|
| 315 |
+
'glossary': translation_glossary, 'section_name': unit_name, 'max_attempts': 2,
|
| 316 |
+
}
|
| 317 |
+
try:
|
| 318 |
+
parameters = inspect.signature(translate_method).parameters.values()
|
| 319 |
+
supports_stage_callback = any(
|
| 320 |
+
item.name == 'stage_callback' or item.kind == inspect.Parameter.VAR_KEYWORD
|
| 321 |
+
for item in parameters
|
| 322 |
+
)
|
| 323 |
+
except (TypeError, ValueError):
|
| 324 |
+
supports_stage_callback = False
|
| 325 |
+
if supports_stage_callback:
|
| 326 |
+
call_kwargs['stage_callback'] = publish_translation_stage
|
| 327 |
+
result = translate_method(source, target_lang, **call_kwargs)
|
| 328 |
except PipelineCancelledError:
|
| 329 |
raise
|
| 330 |
except Exception as error:
|
|
|
|
| 348 |
'Translation deferred until final gate | unit=%s code=%s error=%s',
|
| 349 |
unit_name, error_code, type(error).__name__,
|
| 350 |
)
|
| 351 |
+
self._publish_target_preview(
|
| 352 |
+
partial, f'translation_repair_pending:{unit_name}', force=True, complete=False
|
| 353 |
+
)
|
| 354 |
return updated_sections, translation_glossary
|
| 355 |
updated_glossary = dict(result.get('glossary') or translation_glossary or {})
|
| 356 |
updated_sections[unit_key] = {
|
|
|
|
| 745 |
if working_abstract and not state.get("english_finalized"):
|
| 746 |
section_order_for_front = self._unique_top_level_sections(adaptive_structure)
|
| 747 |
if section_order_for_front:
|
| 748 |
+
first_section_title = section_order_for_front[0][1]
|
| 749 |
+
working_front_source = self._extract_front_matter(full_draft_en, first_section_title)
|
| 750 |
+
if not working_front_source and not completed_sections:
|
| 751 |
+
# Fresh tasks contain only Title/Abstract/Keywords here; the
|
| 752 |
+
# first body heading does not exist yet. Preserve legacy
|
| 753 |
+
# resume behavior when completed body sections are present.
|
| 754 |
+
working_front_source = str(full_draft_en or '').strip()
|
| 755 |
translation_sections, translation_glossary = self._translate_frozen_unit(
|
| 756 |
agent, unit_key='__front_matter__', unit_name='Working Front Matter',
|
| 757 |
source_text=working_front_source, target_lang=target_lang,
|
core/pipeline_agent.py
CHANGED
|
@@ -61,7 +61,7 @@ class CitationAuditError(DeepSeekRequestError):
|
|
| 61 |
|
| 62 |
|
| 63 |
class SSciPipelineAgent:
|
| 64 |
-
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-
|
| 65 |
|
| 66 |
def __init__(self, ds_key, s2_key=""):
|
| 67 |
self.ds_key = str(ds_key or "").strip()
|
|
@@ -3459,6 +3459,123 @@ Output only the repaired section."""
|
|
| 3459 |
core = 'Use formal academic Simplified Chinese with explicit concepts, natural Chinese syntax, clear hierarchy, and logically connected claims. Avoid carrying English nominalization and clause order into Chinese mechanically.'
|
| 3460 |
return core + ' ' + profile['term_policy'] + ' ' + profile['contamination_policy']
|
| 3461 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3462 |
@classmethod
|
| 3463 |
def _compile_translation_ir(cls, source):
|
| 3464 |
"""Compile a private, language-neutral translation intermediate representation."""
|
|
@@ -4302,7 +4419,7 @@ SECTION: {section_name}
|
|
| 4302 |
system_prompt = f"""You are a bilingual academic translation QA classifier for {target_lang}.
|
| 4303 |
Review EACH claim independently. Do not rewrite and do not provide free-form explanations. Check semantic equivalence and language quality: omissions/additions, negation, uncertainty, association-versus-causation strength, terminology, grammar, language contamination, citation attachment, and acronym attachment.
|
| 4304 |
LANGUAGE ADAPTER: {profile['adapter_id']}; morphology={profile['morphology']}. {profile['term_policy']} {profile['contamination_policy']}
|
| 4305 |
-
For Russian, do not flag a terminology error merely because a translated term is inflected for grammatical case/number/gender. For Kazakh, do not flag a terminology error merely because productive suffixes attach to the correct term stem. For Kazakh language identity, absence of Kazakh-specific letters in one short claim is not sufficient evidence of Russian contamination.
|
| 4306 |
Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[]}}]}}.
|
| 4307 |
If ok=false, codes MUST contain only values from allowed_issue_codes. Never return numbered prose, examples, quotes, or corrective wording. Return every supplied id exactly once."""
|
| 4308 |
response = self.call_deepseek(
|
|
@@ -4319,15 +4436,42 @@ If ok=false, codes MUST contain only values from allowed_issue_codes. Never retu
|
|
| 4319 |
f'Translation claim QA id mismatch: expected={expected_row_ids}, got={sorted(by_id)}.'
|
| 4320 |
)
|
| 4321 |
row_lookup = {row['id']: row for row in rows}
|
|
|
|
| 4322 |
for row_id in expected_row_ids:
|
| 4323 |
returned_row = by_id[row_id]
|
| 4324 |
-
|
| 4325 |
-
continue
|
| 4326 |
raw_codes = [str(code or '').strip().upper() for code in (returned_row.get('codes') or [])]
|
| 4327 |
codes = [code for code in raw_codes if code in allowed_codes]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4328 |
if not codes:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4329 |
codes = ['SEMANTIC_DRIFT']
|
| 4330 |
-
meta = row_lookup[row_id]
|
| 4331 |
issues.setdefault(meta['block_id'], {})[meta['claim_id']] = list(dict.fromkeys(codes))[:5]
|
| 4332 |
return issues
|
| 4333 |
|
|
@@ -4685,7 +4829,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4685 |
)
|
| 4686 |
return {}
|
| 4687 |
|
| 4688 |
-
def translate_frozen_section(self, section_text, target_lang, glossary=None, section_name='', max_attempts=2):
|
| 4689 |
"""Translate one frozen English unit and publish only after all QA passes."""
|
| 4690 |
value = str(section_text or '').strip()
|
| 4691 |
if not value:
|
|
@@ -4716,8 +4860,21 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4716 |
)
|
| 4717 |
attempts = max(1, int(max_attempts or 1))
|
| 4718 |
last_error = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4719 |
for attempt in range(1, attempts + 1):
|
| 4720 |
self._check_cancelled()
|
|
|
|
| 4721 |
translations = {}
|
| 4722 |
try:
|
| 4723 |
for batch in batches:
|
|
@@ -4727,6 +4884,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4727 |
for batch in batches
|
| 4728 |
for block_id, source in batch
|
| 4729 |
}
|
|
|
|
| 4730 |
quality_issues = self._audit_translation_blocks_with_retry(
|
| 4731 |
source_blocks, translations, target_lang, frozen_glossary,
|
| 4732 |
section_name=section_name or 'frozen section', max_attempts=2,
|
|
@@ -4742,8 +4900,12 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4742 |
logging.warning(
|
| 4743 |
'Translation claim-local QA recovery: section=%s round=%s failed=%s',
|
| 4744 |
section_name or 'section', local_round,
|
| 4745 |
-
[
|
|
|
|
|
|
|
|
|
|
| 4746 |
)
|
|
|
|
| 4747 |
touched_blocks = set()
|
| 4748 |
for block_id, claim_id, codes in failed_pairs:
|
| 4749 |
source_claims = {
|
|
@@ -4760,6 +4922,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4760 |
)
|
| 4761 |
translations[block_id] = self._compose_translation_claim_segments(claim_order, segments)
|
| 4762 |
touched_blocks.add(block_id)
|
|
|
|
| 4763 |
quality_issues = self._audit_translation_blocks_with_retry(
|
| 4764 |
{block_id: source_blocks[block_id] for block_id in sorted(touched_blocks)},
|
| 4765 |
{block_id: translations[block_id] for block_id in sorted(touched_blocks)},
|
|
|
|
| 61 |
|
| 62 |
|
| 63 |
class SSciPipelineAgent:
|
| 64 |
+
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v2'
|
| 65 |
|
| 66 |
def __init__(self, ds_key, s2_key=""):
|
| 67 |
self.ds_key = str(ds_key or "").strip()
|
|
|
|
| 3459 |
core = 'Use formal academic Simplified Chinese with explicit concepts, natural Chinese syntax, clear hierarchy, and logically connected claims. Avoid carrying English nominalization and clause order into Chinese mechanically.'
|
| 3460 |
return core + ' ' + profile['term_policy'] + ' ' + profile['contamination_policy']
|
| 3461 |
|
| 3462 |
+
@classmethod
|
| 3463 |
+
def _russian_contamination_evidence(cls, source, target, glossary=None):
|
| 3464 |
+
"""Deterministically confirm only sustained, unprotected Latin prose in Russian.
|
| 3465 |
+
|
| 3466 |
+
LLM QA may propose LANGUAGE_CONTAMINATION, but a fatal/repair signal is
|
| 3467 |
+
accepted only when this evidence check finds a multi-word Latin clause
|
| 3468 |
+
after removing immutable research objects and explicit glossary terms.
|
| 3469 |
+
This prevents AI/LLM/ChatGPT/SEM/model names from becoming lexical
|
| 3470 |
+
blacklists while preserving fail-closed behavior for real English prose.
|
| 3471 |
+
"""
|
| 3472 |
+
source_value = str(source or '')
|
| 3473 |
+
text = cls._strip_translation_claim_markers(str(target or ''))
|
| 3474 |
+
# A verbatim quoted span is protected evidence only when the same quote
|
| 3475 |
+
# exists in the locked English source. Invented/untranslated English
|
| 3476 |
+
# quotes are not silently exempted. Research-object protection also
|
| 3477 |
+
# removes acronyms, model/tool names, citations, URLs, DOI strings,
|
| 3478 |
+
# statistics and numeric anchors.
|
| 3479 |
+
quote_pattern = re.compile(r'“[^”\n]{1,500}”|"[^"\n]{1,500}"')
|
| 3480 |
+
text = quote_pattern.sub(lambda match: ' ' if match.group(0) in source_value else match.group(0), text)
|
| 3481 |
+
text = cls._translation_protected_pattern().sub(' ', text)
|
| 3482 |
+
text = re.sub(r'\[\[SP-(?:P\d{6}|C\d{6}|T\d{6})\]\]', ' ', text)
|
| 3483 |
+
|
| 3484 |
+
# A glossary target may legitimately remain Latin (for example a
|
| 3485 |
+
# standardized construct/tool name). Remove only exact glossary targets;
|
| 3486 |
+
# do not whitelist arbitrary English source prose.
|
| 3487 |
+
glossary_terms = []
|
| 3488 |
+
english_function_words = {
|
| 3489 |
+
'a','an','and','as','at','by','for','from','in','of','on','or','the','to','with',
|
| 3490 |
+
}
|
| 3491 |
+
for value in dict(glossary or {}).values():
|
| 3492 |
+
term = str(value or '').strip()
|
| 3493 |
+
words = re.findall(r'[A-Za-z][A-Za-z0-9+.#-]*', term)
|
| 3494 |
+
title_like = bool(words) and len(words) <= 5 and all(
|
| 3495 |
+
word.casefold() in english_function_words or word[:1].isupper() for word in words
|
| 3496 |
+
)
|
| 3497 |
+
technical_shape = bool(words) and any(
|
| 3498 |
+
any(ch.isdigit() for ch in word)
|
| 3499 |
+
or word.isupper()
|
| 3500 |
+
or (len(word) > 2 and any(ch.isupper() for ch in word[1:]))
|
| 3501 |
+
for word in words
|
| 3502 |
+
)
|
| 3503 |
+
if term and re.search(r'[A-Za-z]', term) and (title_like or technical_shape):
|
| 3504 |
+
glossary_terms.append(term)
|
| 3505 |
+
for term in sorted(set(glossary_terms), key=len, reverse=True):
|
| 3506 |
+
text = re.sub(re.escape(term), ' ', text, flags=re.I)
|
| 3507 |
+
|
| 3508 |
+
# Mixed-case technical names such as OpenAI/TensorFlow are not covered by
|
| 3509 |
+
# the all-caps acronym rule. Allow them only when they are present in the
|
| 3510 |
+
# locked English source; ordinary source prose is never whitelisted.
|
| 3511 |
+
source_tokens = set(re.findall(r'\b[A-Za-z][A-Za-z0-9+.#-]*\b', str(source or '')))
|
| 3512 |
+
technical = {
|
| 3513 |
+
token for token in source_tokens
|
| 3514 |
+
if any(ch.isdigit() for ch in token)
|
| 3515 |
+
or (len(token) > 2 and any(ch.isupper() for ch in token[1:]))
|
| 3516 |
+
}
|
| 3517 |
+
for token in sorted(technical, key=len, reverse=True):
|
| 3518 |
+
text = re.sub(rf'(?<![A-Za-z0-9_]){re.escape(token)}(?![A-Za-z0-9_])', ' ', text)
|
| 3519 |
+
|
| 3520 |
+
latin = list(re.finditer(r"[A-Za-z][A-Za-z'’-]*", text))
|
| 3521 |
+
allowed_gap = re.compile(r"^[\s,;:()\[\]{}.!?\"'“”‘’—–/\\-]*$")
|
| 3522 |
+
function_words = {
|
| 3523 |
+
'a','an','and','are','as','at','be','been','being','by','for','from','had','has','have',
|
| 3524 |
+
'in','into','is','it','of','on','or','that','the','these','this','those','to','was','were',
|
| 3525 |
+
'which','with','within','without','would','could','should','than','then','their','its',
|
| 3526 |
+
}
|
| 3527 |
+
max_run = 0
|
| 3528 |
+
max_run_has_function = False
|
| 3529 |
+
run = []
|
| 3530 |
+
previous_end = None
|
| 3531 |
+
normalized_source = re.sub(r'\s+', ' ', source_value).casefold()
|
| 3532 |
+
|
| 3533 |
+
def evaluate(items):
|
| 3534 |
+
nonlocal max_run, max_run_has_function
|
| 3535 |
+
if not items:
|
| 3536 |
+
return False
|
| 3537 |
+
original_words = [item.group(0) for item in items]
|
| 3538 |
+
words = [word.casefold() for word in original_words]
|
| 3539 |
+
has_function = any(word in function_words for word in words)
|
| 3540 |
+
if len(words) > max_run:
|
| 3541 |
+
max_run = len(words)
|
| 3542 |
+
max_run_has_function = has_function
|
| 3543 |
+
# Preserve a bounded source-matched proper name/title such as
|
| 3544 |
+
# "University of California at Berkeley". Ordinary copied English
|
| 3545 |
+
# prose is not title-like and therefore remains detectable.
|
| 3546 |
+
title_like = len(words) <= 6 and all(
|
| 3547 |
+
word in function_words or original[:1].isupper()
|
| 3548 |
+
for word, original in zip(words, original_words)
|
| 3549 |
+
)
|
| 3550 |
+
candidate = ' '.join(original_words).casefold()
|
| 3551 |
+
if title_like and candidate and candidate in normalized_source:
|
| 3552 |
+
return False
|
| 3553 |
+
# Four-word clause with an English function word is strong evidence;
|
| 3554 |
+
# seven Latin words is strong even without one. Short titles/terms
|
| 3555 |
+
# and isolated Latin research objects remain legal.
|
| 3556 |
+
return (len(words) >= 4 and has_function) or len(words) >= 7
|
| 3557 |
+
|
| 3558 |
+
confirmed = False
|
| 3559 |
+
for match in latin:
|
| 3560 |
+
if previous_end is None:
|
| 3561 |
+
run = [match]
|
| 3562 |
+
else:
|
| 3563 |
+
gap = text[previous_end:match.start()]
|
| 3564 |
+
if allowed_gap.fullmatch(gap or ''):
|
| 3565 |
+
run.append(match)
|
| 3566 |
+
else:
|
| 3567 |
+
confirmed = confirmed or evaluate(run)
|
| 3568 |
+
run = [match]
|
| 3569 |
+
previous_end = match.end()
|
| 3570 |
+
confirmed = confirmed or evaluate(run)
|
| 3571 |
+
return {
|
| 3572 |
+
'confirmed': bool(confirmed),
|
| 3573 |
+
'max_latin_run_words': int(max_run),
|
| 3574 |
+
'max_run_has_function_word': bool(max_run_has_function),
|
| 3575 |
+
'glossary_latin_terms': int(len(set(glossary_terms))),
|
| 3576 |
+
'source_technical_tokens': int(len(technical)),
|
| 3577 |
+
}
|
| 3578 |
+
|
| 3579 |
@classmethod
|
| 3580 |
def _compile_translation_ir(cls, source):
|
| 3581 |
"""Compile a private, language-neutral translation intermediate representation."""
|
|
|
|
| 4419 |
system_prompt = f"""You are a bilingual academic translation QA classifier for {target_lang}.
|
| 4420 |
Review EACH claim independently. Do not rewrite and do not provide free-form explanations. Check semantic equivalence and language quality: omissions/additions, negation, uncertainty, association-versus-causation strength, terminology, grammar, language contamination, citation attachment, and acronym attachment.
|
| 4421 |
LANGUAGE ADAPTER: {profile['adapter_id']}; morphology={profile['morphology']}. {profile['term_policy']} {profile['contamination_policy']}
|
| 4422 |
+
For Russian, do not flag a terminology error merely because a translated term is inflected for grammatical case/number/gender. For Russian LANGUAGE_CONTAMINATION, require sustained unprotected non-Russian prose such as a multi-word English clause or sentence. Never use LANGUAGE_CONTAMINATION merely for protected acronyms, AI/LLM/SEM, model or tool names, variable names, citations, glossary terms, source-matched proper names/titles, or isolated Latin terminology. For Kazakh, do not flag a terminology error merely because productive suffixes attach to the correct term stem. For Kazakh language identity, absence of Kazakh-specific letters in one short claim is not sufficient evidence of Russian contamination.
|
| 4423 |
Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[]}}]}}.
|
| 4424 |
If ok=false, codes MUST contain only values from allowed_issue_codes. Never return numbered prose, examples, quotes, or corrective wording. Return every supplied id exactly once."""
|
| 4425 |
response = self.call_deepseek(
|
|
|
|
| 4436 |
f'Translation claim QA id mismatch: expected={expected_row_ids}, got={sorted(by_id)}.'
|
| 4437 |
)
|
| 4438 |
row_lookup = {row['id']: row for row in rows}
|
| 4439 |
+
language_code = profile.get('language_code')
|
| 4440 |
for row_id in expected_row_ids:
|
| 4441 |
returned_row = by_id[row_id]
|
| 4442 |
+
meta = row_lookup[row_id]
|
|
|
|
| 4443 |
raw_codes = [str(code or '').strip().upper() for code in (returned_row.get('codes') or [])]
|
| 4444 |
codes = [code for code in raw_codes if code in allowed_codes]
|
| 4445 |
+
model_contamination = 'LANGUAGE_CONTAMINATION' in codes
|
| 4446 |
+
contamination = None
|
| 4447 |
+
if language_code == 'ru':
|
| 4448 |
+
contamination = self._russian_contamination_evidence(
|
| 4449 |
+
meta['english'], meta['target'], relevant_glossary
|
| 4450 |
+
)
|
| 4451 |
+
if contamination.get('confirmed'):
|
| 4452 |
+
if 'LANGUAGE_CONTAMINATION' not in codes:
|
| 4453 |
+
codes.append('LANGUAGE_CONTAMINATION')
|
| 4454 |
+
elif model_contamination:
|
| 4455 |
+
codes = [code for code in codes if code != 'LANGUAGE_CONTAMINATION']
|
| 4456 |
+
self._record_translation_assurance(
|
| 4457 |
+
'RU_LANGUAGE_CONTAMINATION_UNCONFIRMED', target_lang=target_lang, block_id=row_id,
|
| 4458 |
+
severity='diagnostic', decision='PASS_WITH_WARNING', detail=contamination,
|
| 4459 |
+
)
|
| 4460 |
+
|
| 4461 |
+
# A deterministic confirmed Russian contamination signal can
|
| 4462 |
+
# override an LLM false negative. Conversely, a sole Russian
|
| 4463 |
+
# contamination code without deterministic evidence is suppressed.
|
| 4464 |
+
if returned_row.get('ok') is True and not (contamination and contamination.get('confirmed')):
|
| 4465 |
+
continue
|
| 4466 |
if not codes:
|
| 4467 |
+
only_suppressed_contamination = (
|
| 4468 |
+
language_code == 'ru' and model_contamination
|
| 4469 |
+
and not (contamination or {}).get('confirmed')
|
| 4470 |
+
and bool(raw_codes) and all(code == 'LANGUAGE_CONTAMINATION' for code in raw_codes)
|
| 4471 |
+
)
|
| 4472 |
+
if only_suppressed_contamination:
|
| 4473 |
+
continue
|
| 4474 |
codes = ['SEMANTIC_DRIFT']
|
|
|
|
| 4475 |
issues.setdefault(meta['block_id'], {})[meta['claim_id']] = list(dict.fromkeys(codes))[:5]
|
| 4476 |
return issues
|
| 4477 |
|
|
|
|
| 4829 |
)
|
| 4830 |
return {}
|
| 4831 |
|
| 4832 |
+
def translate_frozen_section(self, section_text, target_lang, glossary=None, section_name='', max_attempts=2, stage_callback=None):
|
| 4833 |
"""Translate one frozen English unit and publish only after all QA passes."""
|
| 4834 |
value = str(section_text or '').strip()
|
| 4835 |
if not value:
|
|
|
|
| 4860 |
)
|
| 4861 |
attempts = max(1, int(max_attempts or 1))
|
| 4862 |
last_error = None
|
| 4863 |
+
|
| 4864 |
+
def emit_stage(stage):
|
| 4865 |
+
if not callable(stage_callback):
|
| 4866 |
+
return
|
| 4867 |
+
try:
|
| 4868 |
+
stage_callback(str(stage or ''))
|
| 4869 |
+
except Exception as error:
|
| 4870 |
+
logging.warning(
|
| 4871 |
+
'Translation stage callback ignored: section=%s stage=%s error=%s',
|
| 4872 |
+
section_name or 'section', stage, type(error).__name__,
|
| 4873 |
+
)
|
| 4874 |
+
|
| 4875 |
for attempt in range(1, attempts + 1):
|
| 4876 |
self._check_cancelled()
|
| 4877 |
+
emit_stage('translating')
|
| 4878 |
translations = {}
|
| 4879 |
try:
|
| 4880 |
for batch in batches:
|
|
|
|
| 4884 |
for batch in batches
|
| 4885 |
for block_id, source in batch
|
| 4886 |
}
|
| 4887 |
+
emit_stage('qa')
|
| 4888 |
quality_issues = self._audit_translation_blocks_with_retry(
|
| 4889 |
source_blocks, translations, target_lang, frozen_glossary,
|
| 4890 |
section_name=section_name or 'frozen section', max_attempts=2,
|
|
|
|
| 4900 |
logging.warning(
|
| 4901 |
'Translation claim-local QA recovery: section=%s round=%s failed=%s',
|
| 4902 |
section_name or 'section', local_round,
|
| 4903 |
+
[
|
| 4904 |
+
{'id': f'SP-P{block_id}-{claim_id}', 'codes': list(codes)}
|
| 4905 |
+
for block_id, claim_id, codes in failed_pairs
|
| 4906 |
+
],
|
| 4907 |
)
|
| 4908 |
+
emit_stage('repairing')
|
| 4909 |
touched_blocks = set()
|
| 4910 |
for block_id, claim_id, codes in failed_pairs:
|
| 4911 |
source_claims = {
|
|
|
|
| 4922 |
)
|
| 4923 |
translations[block_id] = self._compose_translation_claim_segments(claim_order, segments)
|
| 4924 |
touched_blocks.add(block_id)
|
| 4925 |
+
emit_stage('qa')
|
| 4926 |
quality_issues = self._audit_translation_blocks_with_retry(
|
| 4927 |
{block_id: source_blocks[block_id] for block_id in sorted(touched_blocks)},
|
| 4928 |
{block_id: translations[block_id] for block_id in sorted(touched_blocks)},
|
smartpaper_restore_bridge/frontend/index.html
CHANGED
|
@@ -335,10 +335,33 @@
|
|
| 335 |
target.textContent = value;
|
| 336 |
}
|
| 337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
function updateTargetPreparation(parentDocument, payload, labels) {
|
| 339 |
const target = parentDocument.getElementById("sp-live-translation-preview");
|
| 340 |
const channel = renderChannels.target;
|
| 341 |
-
if (!target
|
| 342 |
const map = labels && typeof labels === "object" ? labels : {};
|
| 343 |
const stage = String(payload && payload.target_stage || "");
|
| 344 |
let value = String(map.waiting || "");
|
|
@@ -346,12 +369,23 @@
|
|
| 346 |
value = formatTemplate(map.translating || value, {name:stage.slice("translating:".length)});
|
| 347 |
} else if (stage.startsWith("translation_qa:")) {
|
| 348 |
value = formatTemplate(map.qa || value, {name:stage.slice("translation_qa:".length)});
|
|
|
|
|
|
|
| 349 |
} else if (stage.startsWith("translation_ready:")) {
|
| 350 |
value = formatTemplate(map.ready || value, {name:stage.slice("translation_ready:".length)});
|
| 351 |
} else if (stage === "translation_complete") {
|
| 352 |
value = String(map.complete || value);
|
| 353 |
}
|
| 354 |
if (!value) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
target.dataset.spPlaceholder = "1";
|
| 356 |
target.textContent = value;
|
| 357 |
}
|
|
@@ -512,7 +546,7 @@
|
|
| 512 |
updateOnePreview(parentDocument, parentWindow, "sp-live-translation-preview", "sp-live-translation-shell", targetText, renderChannels.target);
|
| 513 |
lastTargetPreviewText = targetText;
|
| 514 |
}
|
| 515 |
-
|
| 516 |
}
|
| 517 |
|
| 518 |
async function fetchPreview(watch) {
|
|
|
|
| 335 |
target.textContent = value;
|
| 336 |
}
|
| 337 |
|
| 338 |
+
function ensureTargetStatusNode(parentDocument) {
|
| 339 |
+
const shell = parentDocument.getElementById("sp-live-translation-shell");
|
| 340 |
+
const target = parentDocument.getElementById("sp-live-translation-preview");
|
| 341 |
+
if (!shell || !target) return null;
|
| 342 |
+
let status = parentDocument.getElementById("sp-live-translation-status");
|
| 343 |
+
if (!status) {
|
| 344 |
+
status = parentDocument.createElement("div");
|
| 345 |
+
status.id = "sp-live-translation-status";
|
| 346 |
+
status.style.whiteSpace = "pre-wrap";
|
| 347 |
+
status.style.fontSize = "0.88em";
|
| 348 |
+
status.style.opacity = "0.72";
|
| 349 |
+
status.style.marginBottom = "8px";
|
| 350 |
+
status.style.paddingBottom = "4px";
|
| 351 |
+
status.style.position = "sticky";
|
| 352 |
+
status.style.top = "0";
|
| 353 |
+
status.style.zIndex = "1";
|
| 354 |
+
status.style.background = "#ffffff";
|
| 355 |
+
status.style.display = "none";
|
| 356 |
+
shell.insertBefore(status, target);
|
| 357 |
+
}
|
| 358 |
+
return status;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
function updateTargetPreparation(parentDocument, payload, labels) {
|
| 362 |
const target = parentDocument.getElementById("sp-live-translation-preview");
|
| 363 |
const channel = renderChannels.target;
|
| 364 |
+
if (!target) return;
|
| 365 |
const map = labels && typeof labels === "object" ? labels : {};
|
| 366 |
const stage = String(payload && payload.target_stage || "");
|
| 367 |
let value = String(map.waiting || "");
|
|
|
|
| 369 |
value = formatTemplate(map.translating || value, {name:stage.slice("translating:".length)});
|
| 370 |
} else if (stage.startsWith("translation_qa:")) {
|
| 371 |
value = formatTemplate(map.qa || value, {name:stage.slice("translation_qa:".length)});
|
| 372 |
+
} else if (stage.startsWith("translation_repair_pending:")) {
|
| 373 |
+
value = formatTemplate(map.qa || value, {name:stage.slice("translation_repair_pending:".length)});
|
| 374 |
} else if (stage.startsWith("translation_ready:")) {
|
| 375 |
value = formatTemplate(map.ready || value, {name:stage.slice("translation_ready:".length)});
|
| 376 |
} else if (stage === "translation_complete") {
|
| 377 |
value = String(map.complete || value);
|
| 378 |
}
|
| 379 |
if (!value) return;
|
| 380 |
+
const hasTargetText = Boolean(String(payload && payload.target_text || "").trim()) || Boolean(channel.authoritative);
|
| 381 |
+
if (hasTargetText) {
|
| 382 |
+
const status = ensureTargetStatusNode(parentDocument);
|
| 383 |
+
if (!status) return;
|
| 384 |
+
status.textContent = value;
|
| 385 |
+
status.style.display = "block";
|
| 386 |
+
return;
|
| 387 |
+
}
|
| 388 |
+
if (target.querySelector("[data-sp-live-stream='1']")) return;
|
| 389 |
target.dataset.spPlaceholder = "1";
|
| 390 |
target.textContent = value;
|
| 391 |
}
|
|
|
|
| 546 |
updateOnePreview(parentDocument, parentWindow, "sp-live-translation-preview", "sp-live-translation-shell", targetText, renderChannels.target);
|
| 547 |
lastTargetPreviewText = targetText;
|
| 548 |
}
|
| 549 |
+
updateTargetPreparation(parentDocument, payload, watch && watch.targetStatusLabels);
|
| 550 |
}
|
| 551 |
|
| 552 |
async function fetchPreview(watch) {
|