Spaces:
Running
Running
S0-R1-C5.5-R6-R4 Canonical Concept + Protected Anchor Translation Closure
#92
by GuXiaopeng - opened
- core/generation_runner.py +22 -6
- core/pipeline_agent.py +90 -11
- core/qualitative_engine.py +33 -1
core/generation_runner.py
CHANGED
|
@@ -2168,13 +2168,28 @@ class GenerationRunner:
|
|
| 2168 |
if not callable(translate_cells):
|
| 2169 |
raise RuntimeError('Typed qualitative Appendix cell translation entry point is unavailable.')
|
| 2170 |
try:
|
| 2171 |
-
|
| 2172 |
-
|
| 2173 |
-
glossary=translation_glossary, max_attempts=2,
|
| 2174 |
-
claim_states=(existing.get('appendix_cell_claim_states') if isinstance(existing.get('appendix_cell_claim_states'), dict) else None),
|
| 2175 |
)
|
| 2176 |
-
|
| 2177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2178 |
translated_text = qual_engine.bind_target_language_appendix(
|
| 2179 |
manifest, target_lang, cell_translations, quote_map
|
| 2180 |
)
|
|
@@ -2222,6 +2237,7 @@ class GenerationRunner:
|
|
| 2222 |
'qualitative_appendix_manifest_hash': manifest_hash,
|
| 2223 |
'appendix_cell_contract_version': str((result or {}).get('appendix_cell_contract_version') or ''),
|
| 2224 |
'appendix_cell_claim_states': dict((result or {}).get('claim_states') or {}),
|
|
|
|
| 2225 |
'updated_at': datetime.now(timezone.utc).isoformat(),
|
| 2226 |
}
|
| 2227 |
return sections, updated_glossary
|
|
|
|
| 2168 |
if not callable(translate_cells):
|
| 2169 |
raise RuntimeError('Typed qualitative Appendix cell translation entry point is unavailable.')
|
| 2170 |
try:
|
| 2171 |
+
prebound_cells, model_cells = qual_engine.appendix_translation_plan(
|
| 2172 |
+
manifest, translation_glossary
|
|
|
|
|
|
|
| 2173 |
)
|
| 2174 |
+
if model_cells:
|
| 2175 |
+
result = translate_cells(
|
| 2176 |
+
model_cells, target_lang,
|
| 2177 |
+
glossary=translation_glossary, max_attempts=2,
|
| 2178 |
+
claim_states=(existing.get('appendix_cell_claim_states') if isinstance(existing.get('appendix_cell_claim_states'), dict) else None),
|
| 2179 |
+
)
|
| 2180 |
+
model_translations = dict((result or {}).get('translations') or {})
|
| 2181 |
+
updated_glossary = dict((result or {}).get('glossary') or translation_glossary or {})
|
| 2182 |
+
else:
|
| 2183 |
+
result = {
|
| 2184 |
+
'qa_status': 'passed', 'attempts': 1, 'claim_states': {},
|
| 2185 |
+
'appendix_cell_contract_version': 'typed-qualitative-appendix-cells-v2',
|
| 2186 |
+
}
|
| 2187 |
+
model_translations = {}
|
| 2188 |
+
updated_glossary = dict(translation_glossary or {})
|
| 2189 |
+
cell_translations = {**dict(prebound_cells), **dict(model_translations)}
|
| 2190 |
+
expected_cell_ids = set(qual_engine.appendix_translation_cells(manifest))
|
| 2191 |
+
if set(cell_translations) != expected_cell_ids:
|
| 2192 |
+
raise RuntimeError('qualitative_appendix_target_cell_binding_incomplete')
|
| 2193 |
translated_text = qual_engine.bind_target_language_appendix(
|
| 2194 |
manifest, target_lang, cell_translations, quote_map
|
| 2195 |
)
|
|
|
|
| 2237 |
'qualitative_appendix_manifest_hash': manifest_hash,
|
| 2238 |
'appendix_cell_contract_version': str((result or {}).get('appendix_cell_contract_version') or ''),
|
| 2239 |
'appendix_cell_claim_states': dict((result or {}).get('claim_states') or {}),
|
| 2240 |
+
'appendix_prebound_label_count': len(prebound_cells),
|
| 2241 |
'updated_at': datetime.now(timezone.utc).isoformat(),
|
| 2242 |
}
|
| 2243 |
return sections, updated_glossary
|
core/pipeline_agent.py
CHANGED
|
@@ -74,7 +74,7 @@ class SSciPipelineAgent:
|
|
| 74 |
MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS = 2
|
| 75 |
MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
|
| 76 |
PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
|
| 77 |
-
CANONICAL_GLOSSARY_QUALITY_VERSION = 'canonical-glossary-quality-
|
| 78 |
SEMANTIC_CLAIM_FRAME_VERSION = 'smartpaper-core-semantic-claim-frame-v1'
|
| 79 |
CITATION_CLAIM_FRAME_VERSION = 'smartpaper-citation-claim-frame-v1'
|
| 80 |
|
|
@@ -6026,7 +6026,10 @@ TEXT:
|
|
| 6026 |
}
|
| 6027 |
|
| 6028 |
@classmethod
|
| 6029 |
-
def _canonical_glossary_violations(
|
|
|
|
|
|
|
|
|
|
| 6030 |
"""Return canonical concept terms whose frozen target identity disappeared.
|
| 6031 |
|
| 6032 |
Chinese uses compact exact identity. Russian/Kazakh use conservative
|
|
@@ -6039,16 +6042,37 @@ TEXT:
|
|
| 6039 |
language_code = profile.get('language_code')
|
| 6040 |
violations = []
|
| 6041 |
canonical = {str(item or '').strip().casefold() for item in (canonical_sources or []) if str(item or '').strip()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6042 |
for source_term, target_term in dict(glossary or {}).items():
|
| 6043 |
source_term = str(source_term or '').strip()
|
| 6044 |
target_term = str(target_term or '').strip()
|
| 6045 |
if canonical and source_term.casefold() not in canonical:
|
| 6046 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6047 |
if not source_term or not target_term or source_term.casefold() not in source_value.casefold():
|
| 6048 |
continue
|
| 6049 |
# Pure technical/acronym identities are protected elsewhere.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6050 |
compact = re.sub(r'[^A-Za-z0-9]', '', target_term)
|
| 6051 |
-
if compact and ' ' not in target_term and (
|
| 6052 |
compact.isupper() or any(ch.isdigit() for ch in compact)
|
| 6053 |
or (len(compact) > 2 and any(ch.isupper() for ch in compact[1:]))
|
| 6054 |
):
|
|
@@ -6076,6 +6100,30 @@ TEXT:
|
|
| 6076 |
violations.append(source_term)
|
| 6077 |
return list(dict.fromkeys(violations))[:8]
|
| 6078 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6079 |
@classmethod
|
| 6080 |
def _compile_translation_ir(cls, source):
|
| 6081 |
"""Compile a private, language-neutral translation intermediate representation."""
|
|
@@ -7049,6 +7097,7 @@ TEXT:
|
|
| 7049 |
sources = []
|
| 7050 |
strict_sources = []
|
| 7051 |
flexible_sources = []
|
|
|
|
| 7052 |
for row in rows:
|
| 7053 |
if not isinstance(row, dict):
|
| 7054 |
continue
|
|
@@ -7056,6 +7105,7 @@ TEXT:
|
|
| 7056 |
if not term or term in sources:
|
| 7057 |
continue
|
| 7058 |
sources.append(term)
|
|
|
|
| 7059 |
policy = str(row.get('policy') or 'STRICT_CONSTRUCT').strip().upper()
|
| 7060 |
if policy == 'FLEXIBLE_TERM':
|
| 7061 |
flexible_sources.append(term)
|
|
@@ -7067,6 +7117,7 @@ TEXT:
|
|
| 7067 |
# grammar or equivalent wording; semantic QA remains authoritative.
|
| 7068 |
self._canonical_concept_sources = set(strict_sources)
|
| 7069 |
self._canonical_flexible_sources = set(flexible_sources)
|
|
|
|
| 7070 |
if not sources:
|
| 7071 |
return mapping
|
| 7072 |
|
|
@@ -7155,6 +7206,7 @@ TEXT:
|
|
| 7155 |
f"Independently review strict research-construct translations for {target_lang}. {language_rule} "
|
| 7156 |
"Judge semantic identity and natural academic terminology, not stylistic preference. "
|
| 7157 |
"Reject duplicated/malformed wording, concept narrowing/broadening, or a target that changes the construct. "
|
|
|
|
| 7158 |
"If the candidate is sound, return it unchanged with accepted=true. If it is materially flawed, return exactly one corrected canonical target with accepted=false. "
|
| 7159 |
"Do not merge distinct constructs. Return JSON only as "
|
| 7160 |
'{"terms":[{"source":"...","candidate_target":"...","accepted":true,"target":"..."}]}'
|
|
@@ -7183,7 +7235,10 @@ TEXT:
|
|
| 7183 |
accepted_raw = item.get('accepted')
|
| 7184 |
accepted = accepted_raw is True or str(accepted_raw).strip().casefold() == 'true'
|
| 7185 |
reviewed_target = re.sub(r'\s+', ' ', str(item.get('target') or '')).strip()
|
| 7186 |
-
suspicious =
|
|
|
|
|
|
|
|
|
|
| 7187 |
if accepted and not suspicious:
|
| 7188 |
# An acceptance cannot silently remap the frozen candidate.
|
| 7189 |
if reviewed_target and reviewed_target != candidate:
|
|
@@ -7195,7 +7250,10 @@ TEXT:
|
|
| 7195 |
)
|
| 7196 |
if reviewed_target.casefold() == source.casefold() and re.search(r'[a-z]{4,}', source):
|
| 7197 |
raise DeepSeekRequestError(f'Canonical concept remained untranslated after quality review: {source[:80]}')
|
| 7198 |
-
if
|
|
|
|
|
|
|
|
|
|
| 7199 |
raise DeepSeekRequestError(f'Canonical concept correction remains malformed: {source[:80]}')
|
| 7200 |
refined[source] = reviewed_target
|
| 7201 |
self._record_translation_assurance(
|
|
@@ -7523,6 +7581,8 @@ If ok=false, codes MUST contain only values from allowed_issue_codes. For SEMANT
|
|
| 7523 |
canonical_violations = self._canonical_glossary_violations(
|
| 7524 |
meta['english'], meta['target'], target_lang, relevant_glossary,
|
| 7525 |
canonical_sources=getattr(self, '_canonical_concept_sources', set()),
|
|
|
|
|
|
|
| 7526 |
)
|
| 7527 |
if canonical_violations:
|
| 7528 |
if 'TERMINOLOGY' not in codes:
|
|
@@ -8580,10 +8640,21 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8580 |
section_name='Qualitative Appendix semantic cells', max_attempts=max_attempts,
|
| 8581 |
stage_callback=stage_callback, claim_states=claim_states,
|
| 8582 |
)
|
| 8583 |
-
|
| 8584 |
-
|
| 8585 |
-
|
| 8586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8587 |
if len(body_units) != len(ordered):
|
| 8588 |
raise DeepSeekRequestError(
|
| 8589 |
'Qualitative Appendix cell identity mismatch after translation: '
|
|
@@ -8601,7 +8672,7 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 8601 |
**dict(result or {}),
|
| 8602 |
'translations': translations,
|
| 8603 |
'cell_ids': list(cell_objects.keys()),
|
| 8604 |
-
'appendix_cell_contract_version': 'typed-qualitative-appendix-cells-
|
| 8605 |
}
|
| 8606 |
|
| 8607 |
def translate_frozen_section(self, section_text, target_lang, glossary=None, section_name='', max_attempts=2, stage_callback=None, claim_states=None):
|
|
@@ -9018,6 +9089,7 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 9018 |
# Rebuild after any localized repairs, then apply deterministic
|
| 9019 |
# whole-unit integrity/language checks before publication.
|
| 9020 |
output_units = []
|
|
|
|
| 9021 |
body_index = 0
|
| 9022 |
for unit in units:
|
| 9023 |
if unit['kind'] == 'heading':
|
|
@@ -9027,7 +9099,10 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 9027 |
output_units.append('#' * int(unit['level']) + ' ' + translated_heading)
|
| 9028 |
else:
|
| 9029 |
body_index += 1
|
| 9030 |
-
|
|
|
|
|
|
|
|
|
|
| 9031 |
output = '\n\n'.join(output_units).strip()
|
| 9032 |
self.validate_translation_alignment(value, output)
|
| 9033 |
self._validate_translation_language_quality(output, target_lang)
|
|
@@ -9045,6 +9120,10 @@ Decision must be CONFIRMED_BLOCKING or NOT_CONFIRMED. evidence_code must be one
|
|
| 9045 |
'diagnostics': diagnostics[-20:],
|
| 9046 |
'claim_states': dict(self._last_translation_claim_states),
|
| 9047 |
'claim_state_version': 'translation-claim-state-v2',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9048 |
}
|
| 9049 |
except PipelineCancelledError:
|
| 9050 |
raise
|
|
|
|
| 74 |
MAX_TRANSLATION_CLAIM_REPAIR_ATTEMPTS = 2
|
| 75 |
MAX_TRANSLATION_CLAIM_TOTAL_REPAIRS = 6
|
| 76 |
PROTECTED_ENTITY_LEDGER_VERSION = 'protected-entity-ledger-v2'
|
| 77 |
+
CANONICAL_GLOSSARY_QUALITY_VERSION = 'canonical-glossary-quality-v2'
|
| 78 |
SEMANTIC_CLAIM_FRAME_VERSION = 'smartpaper-core-semantic-claim-frame-v1'
|
| 79 |
CITATION_CLAIM_FRAME_VERSION = 'smartpaper-citation-claim-frame-v1'
|
| 80 |
|
|
|
|
| 6026 |
}
|
| 6027 |
|
| 6028 |
@classmethod
|
| 6029 |
+
def _canonical_glossary_violations(
|
| 6030 |
+
cls, source, target, target_lang, glossary=None, canonical_sources=None,
|
| 6031 |
+
canonical_kinds=None, section_name='',
|
| 6032 |
+
):
|
| 6033 |
"""Return canonical concept terms whose frozen target identity disappeared.
|
| 6034 |
|
| 6035 |
Chinese uses compact exact identity. Russian/Kazakh use conservative
|
|
|
|
| 6042 |
language_code = profile.get('language_code')
|
| 6043 |
violations = []
|
| 6044 |
canonical = {str(item or '').strip().casefold() for item in (canonical_sources or []) if str(item or '').strip()}
|
| 6045 |
+
kinds = {
|
| 6046 |
+
str(key or '').strip().casefold(): str(value or '').strip().casefold()
|
| 6047 |
+
for key, value in dict(canonical_kinds or {}).items() if str(key or '').strip()
|
| 6048 |
+
}
|
| 6049 |
+
label_context = 'qualitative appendix semantic cells' in str(section_name or '').casefold()
|
| 6050 |
for source_term, target_term in dict(glossary or {}).items():
|
| 6051 |
source_term = str(source_term or '').strip()
|
| 6052 |
target_term = str(target_term or '').strip()
|
| 6053 |
if canonical and source_term.casefold() not in canonical:
|
| 6054 |
continue
|
| 6055 |
+
# Qualitative theme/concept names are exact reader-facing labels in
|
| 6056 |
+
# the deterministic Appendix, but ordinary prose may realize the
|
| 6057 |
+
# same semantic identity with natural grammar. Requiring an exact
|
| 6058 |
+
# frozen phrase inside every ZH/RU/KK sentence created a false
|
| 6059 |
+
# terminology loop (especially for morphologically rich RU/KK and
|
| 6060 |
+
# compact Chinese paraphrase). Bilingual semantic QA remains the
|
| 6061 |
+
# prose authority; label identity stays exact in the Appendix binder.
|
| 6062 |
+
concept_kind = kinds.get(source_term.casefold(), '')
|
| 6063 |
+
if concept_kind in {'qualitative_theme', 'qualitative_concept'} and not label_context:
|
| 6064 |
+
continue
|
| 6065 |
if not source_term or not target_term or source_term.casefold() not in source_value.casefold():
|
| 6066 |
continue
|
| 6067 |
# Pure technical/acronym identities are protected elsewhere.
|
| 6068 |
+
# Skip deterministic canonical checking only when the *entire*
|
| 6069 |
+
# target is a pure technical/acronym identity. A localized label
|
| 6070 |
+
# containing an embedded acronym (e.g. ``AI推荐...``) is still a
|
| 6071 |
+
# conceptual phrase and must not be mistaken for the bare ``AI``
|
| 6072 |
+
# token merely because stripping non-ASCII characters leaves AI.
|
| 6073 |
+
pure_ascii = bool(re.fullmatch(r'[A-Za-z0-9+.#-]+', target_term))
|
| 6074 |
compact = re.sub(r'[^A-Za-z0-9]', '', target_term)
|
| 6075 |
+
if pure_ascii and compact and ' ' not in target_term and (
|
| 6076 |
compact.isupper() or any(ch.isdigit() for ch in compact)
|
| 6077 |
or (len(compact) > 2 and any(ch.isupper() for ch in compact[1:]))
|
| 6078 |
):
|
|
|
|
| 6100 |
violations.append(source_term)
|
| 6101 |
return list(dict.fromkeys(violations))[:8]
|
| 6102 |
|
| 6103 |
+
@classmethod
|
| 6104 |
+
def _canonical_target_preserves_source_anchors(cls, source, target):
|
| 6105 |
+
"""Require exact-render source anchors to survive inside a canonical label.
|
| 6106 |
+
|
| 6107 |
+
Canonical labels and the protected-anchor compiler must never issue
|
| 6108 |
+
contradictory instructions. Dates/word-quantities are intentionally
|
| 6109 |
+
excluded because their render policy is localized/canonicalized; exact
|
| 6110 |
+
acronyms, names, hypotheses, IDs, formulas, statistics and numbers remain
|
| 6111 |
+
byte-stable under the existing translation contract.
|
| 6112 |
+
"""
|
| 6113 |
+
_protected, mapping = cls._protect_translation_tokens(str(source or ''))
|
| 6114 |
+
target_value = str(target or '')
|
| 6115 |
+
required = Counter()
|
| 6116 |
+
for manifest in dict(mapping or {}).values():
|
| 6117 |
+
if not isinstance(manifest, dict):
|
| 6118 |
+
continue
|
| 6119 |
+
if str(manifest.get('render_policy') or '') != 'exact':
|
| 6120 |
+
continue
|
| 6121 |
+
surface = str(manifest.get('source') or '')
|
| 6122 |
+
if not surface or surface == '|':
|
| 6123 |
+
continue
|
| 6124 |
+
required[surface] += 1
|
| 6125 |
+
return all(target_value.count(surface) >= count for surface, count in required.items())
|
| 6126 |
+
|
| 6127 |
@classmethod
|
| 6128 |
def _compile_translation_ir(cls, source):
|
| 6129 |
"""Compile a private, language-neutral translation intermediate representation."""
|
|
|
|
| 7097 |
sources = []
|
| 7098 |
strict_sources = []
|
| 7099 |
flexible_sources = []
|
| 7100 |
+
source_kinds = {}
|
| 7101 |
for row in rows:
|
| 7102 |
if not isinstance(row, dict):
|
| 7103 |
continue
|
|
|
|
| 7105 |
if not term or term in sources:
|
| 7106 |
continue
|
| 7107 |
sources.append(term)
|
| 7108 |
+
source_kinds[term] = str(row.get('kind') or 'concept').strip()
|
| 7109 |
policy = str(row.get('policy') or 'STRICT_CONSTRUCT').strip().upper()
|
| 7110 |
if policy == 'FLEXIBLE_TERM':
|
| 7111 |
flexible_sources.append(term)
|
|
|
|
| 7117 |
# grammar or equivalent wording; semantic QA remains authoritative.
|
| 7118 |
self._canonical_concept_sources = set(strict_sources)
|
| 7119 |
self._canonical_flexible_sources = set(flexible_sources)
|
| 7120 |
+
self._canonical_concept_kinds = dict(source_kinds)
|
| 7121 |
if not sources:
|
| 7122 |
return mapping
|
| 7123 |
|
|
|
|
| 7206 |
f"Independently review strict research-construct translations for {target_lang}. {language_rule} "
|
| 7207 |
"Judge semantic identity and natural academic terminology, not stylistic preference. "
|
| 7208 |
"Reject duplicated/malformed wording, concept narrowing/broadening, or a target that changes the construct. "
|
| 7209 |
+
"Every protected exact source anchor inside a construct (for example AI, LLM, model/tool IDs, hypothesis IDs, formulas, statistics or exact numbers) must remain byte-for-byte in the target canonical label; never localize, delete, duplicate, or rename such an anchor. "
|
| 7210 |
"If the candidate is sound, return it unchanged with accepted=true. If it is materially flawed, return exactly one corrected canonical target with accepted=false. "
|
| 7211 |
"Do not merge distinct constructs. Return JSON only as "
|
| 7212 |
'{"terms":[{"source":"...","candidate_target":"...","accepted":true,"target":"..."}]}'
|
|
|
|
| 7235 |
accepted_raw = item.get('accepted')
|
| 7236 |
accepted = accepted_raw is True or str(accepted_raw).strip().casefold() == 'true'
|
| 7237 |
reviewed_target = re.sub(r'\s+', ' ', str(item.get('target') or '')).strip()
|
| 7238 |
+
suspicious = (
|
| 7239 |
+
self._canonical_target_surface_suspicious(candidate, target_lang)
|
| 7240 |
+
or not self._canonical_target_preserves_source_anchors(source, candidate)
|
| 7241 |
+
)
|
| 7242 |
if accepted and not suspicious:
|
| 7243 |
# An acceptance cannot silently remap the frozen candidate.
|
| 7244 |
if reviewed_target and reviewed_target != candidate:
|
|
|
|
| 7250 |
)
|
| 7251 |
if reviewed_target.casefold() == source.casefold() and re.search(r'[a-z]{4,}', source):
|
| 7252 |
raise DeepSeekRequestError(f'Canonical concept remained untranslated after quality review: {source[:80]}')
|
| 7253 |
+
if (
|
| 7254 |
+
self._canonical_target_surface_suspicious(reviewed_target, target_lang)
|
| 7255 |
+
or not self._canonical_target_preserves_source_anchors(source, reviewed_target)
|
| 7256 |
+
):
|
| 7257 |
raise DeepSeekRequestError(f'Canonical concept correction remains malformed: {source[:80]}')
|
| 7258 |
refined[source] = reviewed_target
|
| 7259 |
self._record_translation_assurance(
|
|
|
|
| 7581 |
canonical_violations = self._canonical_glossary_violations(
|
| 7582 |
meta['english'], meta['target'], target_lang, relevant_glossary,
|
| 7583 |
canonical_sources=getattr(self, '_canonical_concept_sources', set()),
|
| 7584 |
+
canonical_kinds=getattr(self, '_canonical_concept_kinds', {}),
|
| 7585 |
+
section_name=section_name,
|
| 7586 |
)
|
| 7587 |
if canonical_violations:
|
| 7588 |
if 'TERMINOLOGY' not in codes:
|
|
|
|
| 8640 |
section_name='Qualitative Appendix semantic cells', max_attempts=max_attempts,
|
| 8641 |
stage_callback=stage_callback, claim_states=claim_states,
|
| 8642 |
)
|
| 8643 |
+
body_map = dict((result or {}).get('body_translations') or {})
|
| 8644 |
+
if body_map:
|
| 8645 |
+
expected_block_ids = [f'{index:06d}' for index in range(1, len(ordered) + 1)]
|
| 8646 |
+
if set(body_map) != set(expected_block_ids):
|
| 8647 |
+
raise DeepSeekRequestError(
|
| 8648 |
+
'Qualitative Appendix compiler block identity mismatch after translation: '
|
| 8649 |
+
f'expected={expected_block_ids[:4]} got={sorted(body_map)[:4]}.'
|
| 8650 |
+
)
|
| 8651 |
+
body_units = [str(body_map[block_id] or '').strip() for block_id in expected_block_ids]
|
| 8652 |
+
else:
|
| 8653 |
+
# Backward-compatible fallback for test doubles/legacy callers.
|
| 8654 |
+
body_units = [
|
| 8655 |
+
unit['text'] for unit in self._translation_body_units(result.get('text') or '')
|
| 8656 |
+
if unit.get('kind') == 'body'
|
| 8657 |
+
]
|
| 8658 |
if len(body_units) != len(ordered):
|
| 8659 |
raise DeepSeekRequestError(
|
| 8660 |
'Qualitative Appendix cell identity mismatch after translation: '
|
|
|
|
| 8672 |
**dict(result or {}),
|
| 8673 |
'translations': translations,
|
| 8674 |
'cell_ids': list(cell_objects.keys()),
|
| 8675 |
+
'appendix_cell_contract_version': 'typed-qualitative-appendix-cells-v2',
|
| 8676 |
}
|
| 8677 |
|
| 8678 |
def translate_frozen_section(self, section_text, target_lang, glossary=None, section_name='', max_attempts=2, stage_callback=None, claim_states=None):
|
|
|
|
| 9089 |
# Rebuild after any localized repairs, then apply deterministic
|
| 9090 |
# whole-unit integrity/language checks before publication.
|
| 9091 |
output_units = []
|
| 9092 |
+
body_translations = {}
|
| 9093 |
body_index = 0
|
| 9094 |
for unit in units:
|
| 9095 |
if unit['kind'] == 'heading':
|
|
|
|
| 9099 |
output_units.append('#' * int(unit['level']) + ' ' + translated_heading)
|
| 9100 |
else:
|
| 9101 |
body_index += 1
|
| 9102 |
+
block_id = f'{body_index:06d}'
|
| 9103 |
+
clean_body = self._strip_translation_claim_markers(translations[block_id])
|
| 9104 |
+
body_translations[block_id] = clean_body
|
| 9105 |
+
output_units.append(clean_body)
|
| 9106 |
output = '\n\n'.join(output_units).strip()
|
| 9107 |
self.validate_translation_alignment(value, output)
|
| 9108 |
self._validate_translation_language_quality(output, target_lang)
|
|
|
|
| 9120 |
'diagnostics': diagnostics[-20:],
|
| 9121 |
'claim_states': dict(self._last_translation_claim_states),
|
| 9122 |
'claim_state_version': 'translation-claim-state-v2',
|
| 9123 |
+
# Internal typed consumers (notably the qualitative Appendix)
|
| 9124 |
+
# bind by compiler block identity rather than reparsing
|
| 9125 |
+
# paragraph breaks from reader-facing text.
|
| 9126 |
+
'body_translations': dict(body_translations),
|
| 9127 |
}
|
| 9128 |
except PipelineCancelledError:
|
| 9129 |
raise
|
core/qualitative_engine.py
CHANGED
|
@@ -13,9 +13,10 @@ from config import APP_CONFIG
|
|
| 13 |
|
| 14 |
class QualitativeEngine:
|
| 15 |
QUALITATIVE_APPENDIX_MANIFEST_VERSION = "qualitative-appendix-manifest-v1"
|
| 16 |
-
TARGET_APPENDIX_BINDING_VERSION = "target-qualitative-appendix-binding-
|
| 17 |
SEMANTIC_CLAIM_FRAME_VERSION = "smartpaper-core-semantic-claim-frame-v1"
|
| 18 |
APPENDIX_FIELDS = ("theme", "concept", "stage", "case_label", "evidence_role")
|
|
|
|
| 19 |
APPENDIX_HEADERS = (
|
| 20 |
"Higher-order Theme", "Empirical Concept", "Process Stage",
|
| 21 |
"Case/Actor", "Evidence Role", "Representative Quote",
|
|
@@ -412,6 +413,37 @@ class QualitativeEngine:
|
|
| 412 |
cells[f"{row_id}:{field}"] = value
|
| 413 |
return cells
|
| 414 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
@staticmethod
|
| 416 |
def _escape_appendix_markdown_cell(value):
|
| 417 |
return re.sub(r"\s+", " ", str(value or "").strip()).replace("|", r"\|")
|
|
|
|
| 13 |
|
| 14 |
class QualitativeEngine:
|
| 15 |
QUALITATIVE_APPENDIX_MANIFEST_VERSION = "qualitative-appendix-manifest-v1"
|
| 16 |
+
TARGET_APPENDIX_BINDING_VERSION = "target-qualitative-appendix-binding-v2"
|
| 17 |
SEMANTIC_CLAIM_FRAME_VERSION = "smartpaper-core-semantic-claim-frame-v1"
|
| 18 |
APPENDIX_FIELDS = ("theme", "concept", "stage", "case_label", "evidence_role")
|
| 19 |
+
APPENDIX_CANONICAL_LABEL_FIELDS = ("theme", "concept")
|
| 20 |
APPENDIX_HEADERS = (
|
| 21 |
"Higher-order Theme", "Empirical Concept", "Process Stage",
|
| 22 |
"Case/Actor", "Evidence Role", "Representative Quote",
|
|
|
|
| 413 |
cells[f"{row_id}:{field}"] = value
|
| 414 |
return cells
|
| 415 |
|
| 416 |
+
@classmethod
|
| 417 |
+
def appendix_translation_plan(cls, manifest, glossary=None):
|
| 418 |
+
"""Split Appendix cells into server-bound labels and model-owned surfaces.
|
| 419 |
+
|
| 420 |
+
Theme/concept cells are frozen research labels. When the task-level
|
| 421 |
+
canonical glossary already owns their target identity, the translation
|
| 422 |
+
model must not rewrite those labels or their internal anchors. Remaining
|
| 423 |
+
ordinary cells (stage/case/evidence-role) continue through the typed
|
| 424 |
+
translation compiler and are rebound by server-side cell ID.
|
| 425 |
+
"""
|
| 426 |
+
expected = cls.appendix_translation_cells(manifest)
|
| 427 |
+
glossary_value = dict(glossary or {})
|
| 428 |
+
folded_glossary = {
|
| 429 |
+
str(key or '').strip().casefold(): str(value or '').strip()
|
| 430 |
+
for key, value in glossary_value.items() if str(key or '').strip() and str(value or '').strip()
|
| 431 |
+
}
|
| 432 |
+
prebound = {}
|
| 433 |
+
model_cells = {}
|
| 434 |
+
for cell_id, source in expected.items():
|
| 435 |
+
field = str(cell_id or '').rsplit(':', 1)[-1]
|
| 436 |
+
canonical_target = str(glossary_value.get(source) or '').strip()
|
| 437 |
+
if not canonical_target:
|
| 438 |
+
canonical_target = folded_glossary.get(str(source or '').strip().casefold(), '')
|
| 439 |
+
if field in cls.APPENDIX_CANONICAL_LABEL_FIELDS and canonical_target:
|
| 440 |
+
prebound[cell_id] = canonical_target
|
| 441 |
+
else:
|
| 442 |
+
model_cells[cell_id] = source
|
| 443 |
+
if set(prebound) & set(model_cells) or set(prebound) | set(model_cells) != set(expected):
|
| 444 |
+
raise RuntimeError("qualitative_appendix_translation_plan_identity_mismatch")
|
| 445 |
+
return prebound, model_cells
|
| 446 |
+
|
| 447 |
@staticmethod
|
| 448 |
def _escape_appendix_markdown_cell(value):
|
| 449 |
return re.sub(r"\s+", " ", str(value or "").strip()).replace("|", r"\|")
|