Spaces:
Running
Running
R6A-R4-R12 canonical translation identity and evidence-grounded QA adjudication
#47
by GuXiaopeng - opened
- core/generation_runner.py +62 -7
- core/pipeline_agent.py +157 -2
core/generation_runner.py
CHANGED
|
@@ -37,7 +37,7 @@ ProgressCallback = Callable[..., None]
|
|
| 37 |
MIN_FINAL_REFERENCES = 50
|
| 38 |
LITERATURE_EVIDENCE_TARGET = 75
|
| 39 |
CITATION_PLAN_TARGET = 70
|
| 40 |
-
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-
|
| 41 |
|
| 42 |
|
| 43 |
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
|
@@ -866,6 +866,27 @@ class GenerationRunner:
|
|
| 866 |
# Test doubles / legacy adapters still get deterministic invalidation.
|
| 867 |
return hashlib.sha256(str(source or '').encode('utf-8')).hexdigest()
|
| 868 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
@staticmethod
|
| 870 |
def _assemble_partial_translation(translation_sections, adaptive_structure, *, include_front=False) -> str:
|
| 871 |
mapping = translation_sections if isinstance(translation_sections, dict) else {}
|
|
@@ -939,8 +960,10 @@ class GenerationRunner:
|
|
| 939 |
source = str(source_text or '').strip()
|
| 940 |
if not source or self._translation_worker is None:
|
| 941 |
return False
|
| 942 |
-
|
| 943 |
-
|
|
|
|
|
|
|
| 944 |
existing = dict((translation_sections or {}).get(unit_key) or {})
|
| 945 |
if not isinstance(getattr(self, '_translation_progress_queued', None), set):
|
| 946 |
self._translation_progress_queued = set()
|
|
@@ -953,13 +976,23 @@ class GenerationRunner:
|
|
| 953 |
if (
|
| 954 |
existing.get('qa_status') in {'passed', 'passed_with_warning'}
|
| 955 |
and existing.get('source_hash') == source_hash
|
|
|
|
| 956 |
and existing.get('target_lang') == str(self._translation_worker_context.get('target_lang') or '')
|
| 957 |
and existing.get('translation_contract_version') == TRANSLATION_CONTRACT_VERSION
|
| 958 |
and existing.get('fact_ledger_hash') == fact_ledger_hash
|
| 959 |
and existing.get('translated_text')
|
| 960 |
):
|
| 961 |
return False
|
| 962 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 963 |
if pending_key in self._translation_worker_pending:
|
| 964 |
return False
|
| 965 |
self._translation_progress_inflight.add(str(unit_key))
|
|
@@ -968,6 +1001,7 @@ class GenerationRunner:
|
|
| 968 |
'unit_name': str(unit_name),
|
| 969 |
'source_text': source,
|
| 970 |
'source_hash': source_hash,
|
|
|
|
| 971 |
'fact_ledger_hash': fact_ledger_hash,
|
| 972 |
'english_commit_text': str(english_commit_text or ''),
|
| 973 |
})
|
|
@@ -1010,9 +1044,12 @@ class GenerationRunner:
|
|
| 1010 |
source = str(source_text or '').strip()
|
| 1011 |
if not source:
|
| 1012 |
return translation_sections, translation_glossary
|
| 1013 |
-
|
|
|
|
|
|
|
|
|
|
| 1014 |
contract_version = TRANSLATION_CONTRACT_VERSION
|
| 1015 |
-
fact_ledger_hash =
|
| 1016 |
existing = dict((translation_sections or {}).get(unit_key) or {})
|
| 1017 |
if not isinstance(getattr(self, '_translation_progress_queued', None), set):
|
| 1018 |
self._translation_progress_queued = set()
|
|
@@ -1023,6 +1060,7 @@ class GenerationRunner:
|
|
| 1023 |
if (
|
| 1024 |
existing.get('qa_status') in {'passed', 'passed_with_warning'}
|
| 1025 |
and existing.get('source_hash') == source_hash
|
|
|
|
| 1026 |
and existing.get('target_lang') == target_lang
|
| 1027 |
and existing.get('translation_contract_version') == contract_version
|
| 1028 |
and existing.get('fact_ledger_hash') == fact_ledger_hash
|
|
@@ -1030,6 +1068,15 @@ class GenerationRunner:
|
|
| 1030 |
):
|
| 1031 |
self._translation_progress_inflight.discard(str(unit_key))
|
| 1032 |
return translation_sections, translation_glossary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1033 |
self._translation_progress_inflight.add(str(unit_key))
|
| 1034 |
partial = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=include_front)
|
| 1035 |
self._publish_target_preview(
|
|
@@ -1069,6 +1116,9 @@ class GenerationRunner:
|
|
| 1069 |
translation_source, pending_quote_tags, reused_quote_tags = self._prepare_qualitative_translation_source(
|
| 1070 |
source, getattr(self, "_qualitative_quote_sources", {}), quote_ledger
|
| 1071 |
)
|
|
|
|
|
|
|
|
|
|
| 1072 |
|
| 1073 |
def publish_translation_stage(stage):
|
| 1074 |
value = str(stage or '').strip().casefold()
|
|
@@ -1164,6 +1214,7 @@ class GenerationRunner:
|
|
| 1164 |
error_code = str(code_method(error) if callable(code_method) else 'TRANSLATION_PENDING')
|
| 1165 |
updated_sections[unit_key] = {
|
| 1166 |
'source_hash': source_hash,
|
|
|
|
| 1167 |
'target_lang': target_lang,
|
| 1168 |
'glossary_version': self._glossary_version(translation_glossary),
|
| 1169 |
'translation_contract_version': contract_version,
|
|
@@ -1186,12 +1237,16 @@ class GenerationRunner:
|
|
| 1186 |
)
|
| 1187 |
return updated_sections, translation_glossary
|
| 1188 |
updated_glossary = dict(result.get('glossary') or translation_glossary or {})
|
|
|
|
|
|
|
|
|
|
| 1189 |
updated_sections[unit_key] = {
|
| 1190 |
'source_hash': source_hash,
|
|
|
|
| 1191 |
'target_lang': target_lang,
|
| 1192 |
'glossary_version': self._glossary_version(updated_glossary),
|
| 1193 |
'translation_contract_version': str(result.get('translation_contract_version') or contract_version),
|
| 1194 |
-
'fact_ledger_hash':
|
| 1195 |
'translated_text': str(result.get('text') or '').strip(),
|
| 1196 |
'qa_status': str(result.get('qa_status') or 'passed'),
|
| 1197 |
'language_adapter': str(result.get('language_adapter') or ''),
|
|
|
|
| 37 |
MIN_FINAL_REFERENCES = 50
|
| 38 |
LITERATURE_EVIDENCE_TARGET = 75
|
| 39 |
CITATION_PLAN_TARGET = 70
|
| 40 |
+
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v4'
|
| 41 |
|
| 42 |
|
| 43 |
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
|
|
|
| 866 |
# Test doubles / legacy adapters still get deterministic invalidation.
|
| 867 |
return hashlib.sha256(str(source or '').encode('utf-8')).hexdigest()
|
| 868 |
|
| 869 |
+
def _translation_identity_bundle(self, agent, source: str) -> dict[str, str]:
|
| 870 |
+
"""Return one canonical identity domain for translation reuse.
|
| 871 |
+
|
| 872 |
+
The frozen English source remains the manuscript authority, while the
|
| 873 |
+
Translation Compiler may consume a canonicalized representation (for
|
| 874 |
+
example qualitative quote literals become QUOTE semantic slots). Cache
|
| 875 |
+
identity must therefore persist both hashes and derive the private Fact
|
| 876 |
+
Ledger only from the exact compiled source sent to the translator.
|
| 877 |
+
"""
|
| 878 |
+
raw_source = str(source or '').strip()
|
| 879 |
+
compiled_source, _pending, _reused = self._prepare_qualitative_translation_source(
|
| 880 |
+
raw_source, getattr(self, '_qualitative_quote_sources', {}), {}
|
| 881 |
+
)
|
| 882 |
+
compiled_source = str(compiled_source or '').strip()
|
| 883 |
+
return {
|
| 884 |
+
'source_hash': self._text_hash(raw_source),
|
| 885 |
+
'compiled_source': compiled_source,
|
| 886 |
+
'compiled_source_hash': self._text_hash(compiled_source),
|
| 887 |
+
'fact_ledger_hash': self._translation_ledger_hash(agent, compiled_source),
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
@staticmethod
|
| 891 |
def _assemble_partial_translation(translation_sections, adaptive_structure, *, include_front=False) -> str:
|
| 892 |
mapping = translation_sections if isinstance(translation_sections, dict) else {}
|
|
|
|
| 960 |
source = str(source_text or '').strip()
|
| 961 |
if not source or self._translation_worker is None:
|
| 962 |
return False
|
| 963 |
+
identity = self._translation_identity_bundle(agent, source)
|
| 964 |
+
source_hash = identity['source_hash']
|
| 965 |
+
compiled_source_hash = identity['compiled_source_hash']
|
| 966 |
+
fact_ledger_hash = identity['fact_ledger_hash']
|
| 967 |
existing = dict((translation_sections or {}).get(unit_key) or {})
|
| 968 |
if not isinstance(getattr(self, '_translation_progress_queued', None), set):
|
| 969 |
self._translation_progress_queued = set()
|
|
|
|
| 976 |
if (
|
| 977 |
existing.get('qa_status') in {'passed', 'passed_with_warning'}
|
| 978 |
and existing.get('source_hash') == source_hash
|
| 979 |
+
and existing.get('compiled_source_hash') == compiled_source_hash
|
| 980 |
and existing.get('target_lang') == str(self._translation_worker_context.get('target_lang') or '')
|
| 981 |
and existing.get('translation_contract_version') == TRANSLATION_CONTRACT_VERSION
|
| 982 |
and existing.get('fact_ledger_hash') == fact_ledger_hash
|
| 983 |
and existing.get('translated_text')
|
| 984 |
):
|
| 985 |
return False
|
| 986 |
+
if existing and existing.get('translated_text'):
|
| 987 |
+
reasons = []
|
| 988 |
+
if existing.get('source_hash') != source_hash: reasons.append('english_source_hash')
|
| 989 |
+
if existing.get('compiled_source_hash') != compiled_source_hash: reasons.append('compiled_source_hash')
|
| 990 |
+
if existing.get('fact_ledger_hash') != fact_ledger_hash: reasons.append('fact_ledger_hash')
|
| 991 |
+
if existing.get('translation_contract_version') != TRANSLATION_CONTRACT_VERSION: reasons.append('contract_version')
|
| 992 |
+
if existing.get('target_lang') != str(self._translation_worker_context.get('target_lang') or ''): reasons.append('target_lang')
|
| 993 |
+
if existing.get('qa_status') not in {'passed', 'passed_with_warning'}: reasons.append('qa_status')
|
| 994 |
+
logging.info('Translation reuse miss | unit=%s phase=enqueue reasons=%s', unit_name, ','.join(reasons) or 'unknown')
|
| 995 |
+
pending_key = (str(unit_key), source_hash, compiled_source_hash, fact_ledger_hash)
|
| 996 |
if pending_key in self._translation_worker_pending:
|
| 997 |
return False
|
| 998 |
self._translation_progress_inflight.add(str(unit_key))
|
|
|
|
| 1001 |
'unit_name': str(unit_name),
|
| 1002 |
'source_text': source,
|
| 1003 |
'source_hash': source_hash,
|
| 1004 |
+
'compiled_source_hash': compiled_source_hash,
|
| 1005 |
'fact_ledger_hash': fact_ledger_hash,
|
| 1006 |
'english_commit_text': str(english_commit_text or ''),
|
| 1007 |
})
|
|
|
|
| 1044 |
source = str(source_text or '').strip()
|
| 1045 |
if not source:
|
| 1046 |
return translation_sections, translation_glossary
|
| 1047 |
+
identity = self._translation_identity_bundle(agent, source)
|
| 1048 |
+
source_hash = identity['source_hash']
|
| 1049 |
+
compiled_source_hash = identity['compiled_source_hash']
|
| 1050 |
+
canonical_translation_source = identity['compiled_source']
|
| 1051 |
contract_version = TRANSLATION_CONTRACT_VERSION
|
| 1052 |
+
fact_ledger_hash = identity['fact_ledger_hash']
|
| 1053 |
existing = dict((translation_sections or {}).get(unit_key) or {})
|
| 1054 |
if not isinstance(getattr(self, '_translation_progress_queued', None), set):
|
| 1055 |
self._translation_progress_queued = set()
|
|
|
|
| 1060 |
if (
|
| 1061 |
existing.get('qa_status') in {'passed', 'passed_with_warning'}
|
| 1062 |
and existing.get('source_hash') == source_hash
|
| 1063 |
+
and existing.get('compiled_source_hash') == compiled_source_hash
|
| 1064 |
and existing.get('target_lang') == target_lang
|
| 1065 |
and existing.get('translation_contract_version') == contract_version
|
| 1066 |
and existing.get('fact_ledger_hash') == fact_ledger_hash
|
|
|
|
| 1068 |
):
|
| 1069 |
self._translation_progress_inflight.discard(str(unit_key))
|
| 1070 |
return translation_sections, translation_glossary
|
| 1071 |
+
if existing and existing.get('translated_text'):
|
| 1072 |
+
reasons = []
|
| 1073 |
+
if existing.get('source_hash') != source_hash: reasons.append('english_source_hash')
|
| 1074 |
+
if existing.get('compiled_source_hash') != compiled_source_hash: reasons.append('compiled_source_hash')
|
| 1075 |
+
if existing.get('fact_ledger_hash') != fact_ledger_hash: reasons.append('fact_ledger_hash')
|
| 1076 |
+
if existing.get('translation_contract_version') != contract_version: reasons.append('contract_version')
|
| 1077 |
+
if existing.get('target_lang') != target_lang: reasons.append('target_lang')
|
| 1078 |
+
if existing.get('qa_status') not in {'passed', 'passed_with_warning'}: reasons.append('qa_status')
|
| 1079 |
+
logging.info('Translation reuse miss | unit=%s phase=final_or_direct reasons=%s', unit_name, ','.join(reasons) or 'unknown')
|
| 1080 |
self._translation_progress_inflight.add(str(unit_key))
|
| 1081 |
partial = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=include_front)
|
| 1082 |
self._publish_target_preview(
|
|
|
|
| 1116 |
translation_source, pending_quote_tags, reused_quote_tags = self._prepare_qualitative_translation_source(
|
| 1117 |
source, getattr(self, "_qualitative_quote_sources", {}), quote_ledger
|
| 1118 |
)
|
| 1119 |
+
translation_source = str(translation_source or '').strip()
|
| 1120 |
+
if translation_source != canonical_translation_source:
|
| 1121 |
+
raise RuntimeError('Canonical translation source changed with Quote Ledger state.')
|
| 1122 |
|
| 1123 |
def publish_translation_stage(stage):
|
| 1124 |
value = str(stage or '').strip().casefold()
|
|
|
|
| 1214 |
error_code = str(code_method(error) if callable(code_method) else 'TRANSLATION_PENDING')
|
| 1215 |
updated_sections[unit_key] = {
|
| 1216 |
'source_hash': source_hash,
|
| 1217 |
+
'compiled_source_hash': compiled_source_hash,
|
| 1218 |
'target_lang': target_lang,
|
| 1219 |
'glossary_version': self._glossary_version(translation_glossary),
|
| 1220 |
'translation_contract_version': contract_version,
|
|
|
|
| 1237 |
)
|
| 1238 |
return updated_sections, translation_glossary
|
| 1239 |
updated_glossary = dict(result.get('glossary') or translation_glossary or {})
|
| 1240 |
+
result_fact_ledger_hash = str(result.get('fact_ledger_hash') or fact_ledger_hash)
|
| 1241 |
+
if result_fact_ledger_hash != fact_ledger_hash:
|
| 1242 |
+
raise RuntimeError('Translation result Fact Ledger identity differs from canonical compiled source.')
|
| 1243 |
updated_sections[unit_key] = {
|
| 1244 |
'source_hash': source_hash,
|
| 1245 |
+
'compiled_source_hash': compiled_source_hash,
|
| 1246 |
'target_lang': target_lang,
|
| 1247 |
'glossary_version': self._glossary_version(updated_glossary),
|
| 1248 |
'translation_contract_version': str(result.get('translation_contract_version') or contract_version),
|
| 1249 |
+
'fact_ledger_hash': result_fact_ledger_hash,
|
| 1250 |
'translated_text': str(result.get('text') or '').strip(),
|
| 1251 |
'qa_status': str(result.get('qa_status') or 'passed'),
|
| 1252 |
'language_adapter': str(result.get('language_adapter') or ''),
|
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()
|
|
@@ -5300,6 +5300,154 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 5300 |
_order, segments = self._parse_translation_claim_segments(restored, [claim_id])
|
| 5301 |
return segments[claim_id]
|
| 5302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5303 |
def _audit_translation_blocks_with_retry(
|
| 5304 |
self, sources, translations, target_lang, glossary=None, section_name='', max_attempts=2
|
| 5305 |
):
|
|
@@ -5431,6 +5579,12 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 5431 |
{block_id: translations[block_id] for block_id in sorted(touched_blocks)},
|
| 5432 |
target_lang, frozen_glossary, section_name=section_name or 'frozen section', max_attempts=2,
|
| 5433 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5434 |
if quality_issues:
|
| 5435 |
detail = '; '.join(
|
| 5436 |
f'SP-P{block_id}-{claim_id}: {",".join(codes)}'
|
|
@@ -5438,7 +5592,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 5438 |
for claim_id, codes in sorted((claim_map or {}).items())
|
| 5439 |
)
|
| 5440 |
raise DeepSeekRequestError(
|
| 5441 |
-
f'Translation claim-local QA
|
| 5442 |
)
|
| 5443 |
|
| 5444 |
# Rebuild after any localized repairs, then apply deterministic
|
|
@@ -5475,6 +5629,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 5475 |
message = str(error)
|
| 5476 |
if (
|
| 5477 |
message.startswith('Translation claim-local QA failed')
|
|
|
|
| 5478 |
or message.startswith('Translation introduced unowned factual content')
|
| 5479 |
or message.startswith('Translation order-sensitive relation changed')
|
| 5480 |
or message.startswith('Translation fact order changed')
|
|
|
|
| 61 |
|
| 62 |
|
| 63 |
class SSciPipelineAgent:
|
| 64 |
+
TRANSLATION_CONTRACT_VERSION = 'hierarchical-multilingual-v4'
|
| 65 |
|
| 66 |
def __init__(self, ds_key, s2_key=""):
|
| 67 |
self.ds_key = str(ds_key or "").strip()
|
|
|
|
| 5300 |
_order, segments = self._parse_translation_claim_segments(restored, [claim_id])
|
| 5301 |
return segments[claim_id]
|
| 5302 |
|
| 5303 |
+
def _adjudicate_persistent_translation_issues(
|
| 5304 |
+
self, sources, translations, issues, target_lang, glossary=None, section_name=''
|
| 5305 |
+
):
|
| 5306 |
+
"""Independently verify persistent stochastic QA findings before fatality.
|
| 5307 |
+
|
| 5308 |
+
Structural/factual invariants remain owned by deterministic validators.
|
| 5309 |
+
This verifier only decides whether an LLM-raised semantic/linguistic
|
| 5310 |
+
issue that survived bounded claim repair has blocking evidence in the
|
| 5311 |
+
source/target pair. It cannot rewrite text and returns bounded codes.
|
| 5312 |
+
"""
|
| 5313 |
+
issue_map = {
|
| 5314 |
+
str(block_id): {
|
| 5315 |
+
str(claim_id): [str(code or '').strip().upper() for code in (codes or []) if str(code or '').strip()]
|
| 5316 |
+
for claim_id, codes in dict(claim_map or {}).items()
|
| 5317 |
+
}
|
| 5318 |
+
for block_id, claim_map in dict(issues or {}).items()
|
| 5319 |
+
}
|
| 5320 |
+
if not issue_map:
|
| 5321 |
+
return {}
|
| 5322 |
+
|
| 5323 |
+
source_map = {str(key): str(value or '').strip() for key, value in dict(sources or {}).items()}
|
| 5324 |
+
target_map = {str(key): str(value or '').strip() for key, value in dict(translations or {}).items()}
|
| 5325 |
+
profile = self._translation_language_profile(target_lang)
|
| 5326 |
+
rows = []
|
| 5327 |
+
for block_id, claim_map in sorted(issue_map.items()):
|
| 5328 |
+
if block_id not in source_map or block_id not in target_map:
|
| 5329 |
+
raise DeepSeekRequestError(f'Translation QA adjudication block identity mismatch: SP-P{block_id}.')
|
| 5330 |
+
source_claims = {item['claim_id']: item['text'] for item in self._translation_claim_units(source_map[block_id])}
|
| 5331 |
+
claim_order = list(source_claims)
|
| 5332 |
+
_order, target_claims = self._parse_translation_claim_segments(target_map[block_id], claim_order)
|
| 5333 |
+
for claim_id, codes in sorted(claim_map.items()):
|
| 5334 |
+
if claim_id not in source_claims or claim_id not in target_claims:
|
| 5335 |
+
raise DeepSeekRequestError(f'Translation QA adjudication claim identity mismatch: SP-P{block_id}-{claim_id}.')
|
| 5336 |
+
for code in codes:
|
| 5337 |
+
# Russian sustained Latin prose already has deterministic
|
| 5338 |
+
# evidence authority inside audit_translation_blocks.
|
| 5339 |
+
if code == 'LANGUAGE_CONTAMINATION' and profile.get('language_code') == 'ru':
|
| 5340 |
+
evidence = self._russian_contamination_evidence(
|
| 5341 |
+
source_claims[claim_id], target_claims[claim_id], glossary
|
| 5342 |
+
)
|
| 5343 |
+
if evidence.get('confirmed'):
|
| 5344 |
+
rows.append({
|
| 5345 |
+
'id': f'SP-P{block_id}-{claim_id}', 'block_id': block_id,
|
| 5346 |
+
'claim_id': claim_id, 'code': code, 'english': source_claims[claim_id],
|
| 5347 |
+
'target': target_claims[claim_id], 'deterministic_confirmed': True,
|
| 5348 |
+
})
|
| 5349 |
+
continue
|
| 5350 |
+
rows.append({
|
| 5351 |
+
'id': f'SP-P{block_id}-{claim_id}', 'block_id': block_id,
|
| 5352 |
+
'claim_id': claim_id, 'code': code, 'english': source_claims[claim_id],
|
| 5353 |
+
'target': target_claims[claim_id], 'deterministic_confirmed': False,
|
| 5354 |
+
})
|
| 5355 |
+
|
| 5356 |
+
confirmed = {}
|
| 5357 |
+
model_rows = [row for row in rows if not row.get('deterministic_confirmed')]
|
| 5358 |
+
for row in rows:
|
| 5359 |
+
if row.get('deterministic_confirmed'):
|
| 5360 |
+
confirmed.setdefault(row['block_id'], {}).setdefault(row['claim_id'], []).append(row['code'])
|
| 5361 |
+
self._record_translation_assurance(
|
| 5362 |
+
'QA_ADJUDICATION_CONFIRMED', target_lang=target_lang, block_id=row['id'],
|
| 5363 |
+
severity='critical', decision='HOLD_FINAL',
|
| 5364 |
+
detail={'issue_code': row['code'], 'evidence_code': 'UNTRANSLATED_PROSE'},
|
| 5365 |
+
)
|
| 5366 |
+
|
| 5367 |
+
if not model_rows:
|
| 5368 |
+
return confirmed
|
| 5369 |
+
|
| 5370 |
+
allowed_evidence = {
|
| 5371 |
+
'CLAIM_MISSING','CLAIM_ADDED','POLARITY_CHANGED','CAUSALITY_CHANGED',
|
| 5372 |
+
'UNCERTAINTY_CHANGED','MEANING_CHANGED','CITATION_MOVED','ACRONYM_MISATTACHED',
|
| 5373 |
+
'GRAMMATICAL_VIOLATION','GLOSSARY_CONCEPT_MISMATCH','UNTRANSLATED_PROSE',
|
| 5374 |
+
'ACCEPTABLE_VARIANT','MORPHOLOGICAL_VARIANT','STYLISTIC_PREFERENCE',
|
| 5375 |
+
'QA_DISAGREEMENT','NO_BLOCKING_EVIDENCE',
|
| 5376 |
+
}
|
| 5377 |
+
payload = {
|
| 5378 |
+
'section': str(section_name or 'section'),
|
| 5379 |
+
'target_language': target_lang,
|
| 5380 |
+
'language_adapter': profile.get('adapter_id'),
|
| 5381 |
+
'morphology': profile.get('morphology'),
|
| 5382 |
+
'term_policy': profile.get('term_policy'),
|
| 5383 |
+
'items': [
|
| 5384 |
+
{'id': row['id'], 'issue_code': row['code'], 'english': row['english'], 'target': row['target']}
|
| 5385 |
+
for row in model_rows
|
| 5386 |
+
],
|
| 5387 |
+
}
|
| 5388 |
+
system_prompt = f"""You are an independent bilingual translation QA adjudicator for {target_lang}.
|
| 5389 |
+
This is verification-only: do not rewrite, suggest wording, or emit free-form explanations. Each supplied issue already survived two bounded claim-local repair rounds. Confirm a BLOCKING defect only when the English/target pair itself provides clear evidence. Do not treat stylistic preference or a legitimate grammatical/morphological variant as blocking.
|
| 5390 |
+
For Russian, allow required case/number/gender inflection. For Kazakh, allow productive case, possessive, person/plural and vowel-harmony suffixes on the correct term stem. For Chinese, allow natural reordering and sentence reshaping inside the same claim while preserving meaning.
|
| 5391 |
+
Return valid JSON only: {{"items":[{{"id":"SP-P000001-C000001","issue_code":"GRAMMAR","decision":"CONFIRMED_BLOCKING","evidence_code":"GRAMMATICAL_VIOLATION"}}]}}.
|
| 5392 |
+
Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one of: {sorted(allowed_evidence)}. Return every supplied id+issue_code pair exactly once."""
|
| 5393 |
+
try:
|
| 5394 |
+
response = self.call_deepseek(
|
| 5395 |
+
system_prompt, json.dumps(payload, ensure_ascii=False),
|
| 5396 |
+
max_tokens=max(1200, min(3200, 600 + 180 * len(model_rows))),
|
| 5397 |
+
temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
|
| 5398 |
+
)
|
| 5399 |
+
parsed = self._extract_json_from_text(response)
|
| 5400 |
+
returned = parsed.get('items', []) if isinstance(parsed, dict) else []
|
| 5401 |
+
by_key = {}
|
| 5402 |
+
for item in returned:
|
| 5403 |
+
if not isinstance(item, dict):
|
| 5404 |
+
continue
|
| 5405 |
+
key = (str(item.get('id') or ''), str(item.get('issue_code') or '').strip().upper())
|
| 5406 |
+
by_key[key] = item
|
| 5407 |
+
expected = {(row['id'], row['code']) for row in model_rows}
|
| 5408 |
+
if set(by_key) != expected:
|
| 5409 |
+
raise DeepSeekRequestError(
|
| 5410 |
+
f'Translation QA adjudication identity mismatch: expected={sorted(expected)}, got={sorted(by_key)}.'
|
| 5411 |
+
)
|
| 5412 |
+
for row in model_rows:
|
| 5413 |
+
item = by_key[(row['id'], row['code'])]
|
| 5414 |
+
decision = str(item.get('decision') or '').strip().upper()
|
| 5415 |
+
evidence_code = str(item.get('evidence_code') or '').strip().upper()
|
| 5416 |
+
if decision not in {'CONFIRMED_BLOCKING', 'NOT_CONFIRMED'} or evidence_code not in allowed_evidence:
|
| 5417 |
+
raise DeepSeekRequestError(f'Translation QA adjudication returned invalid bounded decision for {row["id"]}.')
|
| 5418 |
+
if decision == 'CONFIRMED_BLOCKING':
|
| 5419 |
+
confirmed.setdefault(row['block_id'], {}).setdefault(row['claim_id'], []).append(row['code'])
|
| 5420 |
+
self._record_translation_assurance(
|
| 5421 |
+
'QA_ADJUDICATION_CONFIRMED', target_lang=target_lang, block_id=row['id'],
|
| 5422 |
+
severity='critical', decision='HOLD_FINAL',
|
| 5423 |
+
detail={'issue_code': row['code'], 'evidence_code': evidence_code},
|
| 5424 |
+
)
|
| 5425 |
+
else:
|
| 5426 |
+
self._record_translation_assurance(
|
| 5427 |
+
'QA_ADJUDICATION_NOT_CONFIRMED', target_lang=target_lang, block_id=row['id'],
|
| 5428 |
+
severity='diagnostic', decision='PASS_WITH_WARNING',
|
| 5429 |
+
detail={'issue_code': row['code'], 'evidence_code': evidence_code},
|
| 5430 |
+
)
|
| 5431 |
+
except PipelineCancelledError:
|
| 5432 |
+
raise
|
| 5433 |
+
except Exception as error:
|
| 5434 |
+
# A verifier outage or malformed verifier payload is not evidence
|
| 5435 |
+
# that the translation is wrong. Deterministic integrity gates have
|
| 5436 |
+
# already run and remain authoritative.
|
| 5437 |
+
self._record_translation_assurance(
|
| 5438 |
+
'QA_ADJUDICATION_UNCERTAIN', target_lang=target_lang, block_id=section_name or 'section',
|
| 5439 |
+
severity='diagnostic', decision='PASS_WITH_WARNING',
|
| 5440 |
+
detail={'error_type': type(error).__name__, 'items': len(model_rows)},
|
| 5441 |
+
)
|
| 5442 |
+
logging.warning(
|
| 5443 |
+
'Translation QA adjudication technical fallback: section=%s error=%s',
|
| 5444 |
+
section_name or 'section', self._safe_source_error(error),
|
| 5445 |
+
)
|
| 5446 |
+
return {
|
| 5447 |
+
block_id: {claim_id: list(dict.fromkeys(codes)) for claim_id, codes in claim_map.items() if codes}
|
| 5448 |
+
for block_id, claim_map in confirmed.items() if claim_map
|
| 5449 |
+
}
|
| 5450 |
+
|
| 5451 |
def _audit_translation_blocks_with_retry(
|
| 5452 |
self, sources, translations, target_lang, glossary=None, section_name='', max_attempts=2
|
| 5453 |
):
|
|
|
|
| 5579 |
{block_id: translations[block_id] for block_id in sorted(touched_blocks)},
|
| 5580 |
target_lang, frozen_glossary, section_name=section_name or 'frozen section', max_attempts=2,
|
| 5581 |
)
|
| 5582 |
+
if quality_issues:
|
| 5583 |
+
emit_stage('qa')
|
| 5584 |
+
quality_issues = self._adjudicate_persistent_translation_issues(
|
| 5585 |
+
source_blocks, translations, quality_issues, target_lang, frozen_glossary,
|
| 5586 |
+
section_name=section_name or 'frozen section',
|
| 5587 |
+
)
|
| 5588 |
if quality_issues:
|
| 5589 |
detail = '; '.join(
|
| 5590 |
f'SP-P{block_id}-{claim_id}: {",".join(codes)}'
|
|
|
|
| 5592 |
for claim_id, codes in sorted((claim_map or {}).items())
|
| 5593 |
)
|
| 5594 |
raise DeepSeekRequestError(
|
| 5595 |
+
f'Translation claim-local QA confirmed blocking for {section_name or "section"}: {detail[:1000]}'
|
| 5596 |
)
|
| 5597 |
|
| 5598 |
# Rebuild after any localized repairs, then apply deterministic
|
|
|
|
| 5629 |
message = str(error)
|
| 5630 |
if (
|
| 5631 |
message.startswith('Translation claim-local QA failed')
|
| 5632 |
+
or message.startswith('Translation claim-local QA confirmed blocking')
|
| 5633 |
or message.startswith('Translation introduced unowned factual content')
|
| 5634 |
or message.startswith('Translation order-sensitive relation changed')
|
| 5635 |
or message.startswith('Translation fact order changed')
|