R6A-R4-R10 typed qualitative quote translation object and deterministic target slot injection

#46
Files changed (2) hide show
  1. core/generation_runner.py +51 -33
  2. core/pipeline_agent.py +76 -0
core/generation_runner.py CHANGED
@@ -651,12 +651,11 @@ class GenerationRunner:
651
 
652
  @classmethod
653
  def _prepare_qualitative_translation_source(cls, source, quote_sources, quote_ledger):
654
- """Use one task-level target rendering for every frozen qualitative quote.
655
 
656
- The first translated occurrence keeps the English quote in context and
657
- appends its protected QUOTE tag so the target rendering can be captured.
658
- Later occurrences (notably the Appendix) send only the protected tag and
659
- are restored from the already QA-passed task-level quote ledger.
660
  """
661
  value = str(source or "")
662
  pending = []
@@ -665,42 +664,21 @@ class GenerationRunner:
665
  wrapped = f'"{quote}"'
666
  tagged = f'{wrapped} [{tag}]'
667
  if tagged in value:
668
- if tag in (quote_ledger or {}):
669
- value = value.replace(tagged, f"[{tag}]")
670
- reused.append(tag)
671
- else:
672
- pending.append(tag)
673
  continue
674
  if wrapped not in value:
675
  continue
676
- if tag in (quote_ledger or {}):
677
- value = value.replace(wrapped, f"[{tag}]")
678
- reused.append(tag)
679
- else:
680
- value = value.replace(wrapped, tagged)
681
- pending.append(tag)
682
  return value, pending, reused
683
 
684
  @classmethod
685
  def _finalize_qualitative_translation_quotes(cls, translated, pending, reused, quote_ledger):
686
  value = str(translated or "")
687
  ledger = dict(quote_ledger or {})
688
- quote_pattern = r'(?P<quote>"[^"\n]{4,2500}"|“[^”\n]{4,2500}”|«[^»\n]{4,2500}»|„[^“\n]{4,2500}“)'
689
- for tag in pending or []:
690
- pattern = re.compile(
691
- quote_pattern + rf"\s*(?<!\w)\[?{re.escape(tag)}\]?(?!\w)",
692
- flags=re.UNICODE,
693
- )
694
- matches = list(pattern.finditer(value))
695
- if len(matches) != 1:
696
- raise RuntimeError(
697
- f"Qualitative quote translation ledger capture failed: {tag} matches={len(matches)}"
698
- )
699
- rendered_quote = matches[0].group("quote")
700
- ledger[tag] = rendered_quote
701
- value = value[:matches[0].start()] + rendered_quote + value[matches[0].end():]
702
-
703
- for tag in reused or []:
704
  rendered_quote = str(ledger.get(tag) or "").strip()
705
  if not rendered_quote:
706
  raise RuntimeError(f"Qualitative quote translation ledger missing target text: {tag}")
@@ -708,7 +686,7 @@ class GenerationRunner:
708
  count = len(re.findall(pattern, value))
709
  if count != 1:
710
  raise RuntimeError(
711
- f"Qualitative quote translation ledger reuse failed: {tag} matches={count}"
712
  )
713
  value = re.sub(pattern, lambda _m, q=rendered_quote: q, value, count=1)
714
 
@@ -1104,6 +1082,46 @@ class GenerationRunner:
1104
  )
1105
 
1106
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1107
  translate_method = agent.translate_frozen_section
1108
  call_kwargs = {
1109
  'glossary': translation_glossary, 'section_name': unit_name, 'max_attempts': 2,
 
651
 
652
  @classmethod
653
  def _prepare_qualitative_translation_source(cls, source, quote_sources, quote_ledger):
654
+ """Replace every frozen qualitative quote with a typed semantic slot.
655
 
656
+ QUOTE identity is never inferred from translated narrative surface text.
657
+ Missing target objects are classified as pending and translated through
658
+ the dedicated typed-object path before the surrounding narrative runs.
 
659
  """
660
  value = str(source or "")
661
  pending = []
 
664
  wrapped = f'"{quote}"'
665
  tagged = f'{wrapped} [{tag}]'
666
  if tagged in value:
667
+ value = value.replace(tagged, f"[{tag}]")
668
+ (reused if tag in (quote_ledger or {}) else pending).append(tag)
 
 
 
669
  continue
670
  if wrapped not in value:
671
  continue
672
+ value = value.replace(wrapped, f"[{tag}]")
673
+ (reused if tag in (quote_ledger or {}) else pending).append(tag)
 
 
 
 
674
  return value, pending, reused
675
 
676
  @classmethod
677
  def _finalize_qualitative_translation_quotes(cls, translated, pending, reused, quote_ledger):
678
  value = str(translated or "")
679
  ledger = dict(quote_ledger or {})
680
+ ordered_tags = list(dict.fromkeys(list(pending or []) + list(reused or [])))
681
+ for tag in ordered_tags:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  rendered_quote = str(ledger.get(tag) or "").strip()
683
  if not rendered_quote:
684
  raise RuntimeError(f"Qualitative quote translation ledger missing target text: {tag}")
 
686
  count = len(re.findall(pattern, value))
687
  if count != 1:
688
  raise RuntimeError(
689
+ f"Qualitative quote deterministic target slot injection failed: {tag} matches={count}"
690
  )
691
  value = re.sub(pattern, lambda _m, q=rendered_quote: q, value, count=1)
692
 
 
1082
  )
1083
 
1084
  try:
1085
+ if pending_quote_tags:
1086
+ quote_translate_method = getattr(agent, 'translate_qualitative_quote_objects', None)
1087
+ if not callable(quote_translate_method):
1088
+ raise RuntimeError('Typed qualitative quote translation entry point is unavailable.')
1089
+ quote_objects = {
1090
+ tag: getattr(self, '_qualitative_quote_sources', {}).get(tag, '')
1091
+ for tag in pending_quote_tags
1092
+ }
1093
+ quote_result = quote_translate_method(
1094
+ quote_objects,
1095
+ target_lang,
1096
+ glossary=translation_glossary,
1097
+ max_attempts=2,
1098
+ stage_callback=publish_translation_stage,
1099
+ )
1100
+ returned_quotes = (quote_result or {}).get('translations')
1101
+ if not isinstance(returned_quotes, dict) or set(returned_quotes) != set(pending_quote_tags):
1102
+ raise RuntimeError(
1103
+ 'Typed qualitative quote translation identity mismatch: '
1104
+ f'expected={sorted(pending_quote_tags)} got={sorted(returned_quotes) if isinstance(returned_quotes, dict) else []}'
1105
+ )
1106
+ for tag in pending_quote_tags:
1107
+ rendered_quote = str(returned_quotes.get(tag) or '').strip()
1108
+ if not rendered_quote:
1109
+ raise RuntimeError(f'Typed qualitative quote translation is empty: {tag}')
1110
+ quote_ledger[tag] = rendered_quote
1111
+ returned_glossary = (quote_result or {}).get('glossary')
1112
+ if isinstance(returned_glossary, dict):
1113
+ translation_glossary = dict(returned_glossary)
1114
+ updated_sections['__quote_ledger__'] = {
1115
+ 'task_id': getattr(getattr(self, "task_dir", None), "name", ""),
1116
+ 'request_hash': current_request_hash,
1117
+ 'writer_execution_epoch': int(getattr(self, "execution_epoch", 0) or 0),
1118
+ 'target_lang': target_lang,
1119
+ 'translation_contract_version': TRANSLATION_CONTRACT_VERSION,
1120
+ 'source_quote_hashes': current_quote_hashes,
1121
+ 'translations': dict(quote_ledger),
1122
+ 'updated_at': datetime.now(timezone.utc).isoformat(),
1123
+ }
1124
+
1125
  translate_method = agent.translate_frozen_section
1126
  call_kwargs = {
1127
  'glossary': translation_glossary, 'section_name': unit_name, 'max_attempts': 2,
core/pipeline_agent.py CHANGED
@@ -4695,6 +4695,82 @@ Output only the repaired section."""
4695
  )
4696
  return True
4697
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4698
  def validate_translation_semantic_equivalence(self, source, translated, target_lang, section_name=''):
4699
  """Fail closed when translation changes claim meaning or evidence strength."""
4700
  source_value = str(source or '').strip()
 
4695
  )
4696
  return True
4697
 
4698
+ @classmethod
4699
+ def _render_qualitative_quote_translation(cls, translated, target_lang):
4700
+ """Return one reader-facing target quotation from a typed quote object.
4701
+
4702
+ Quote identity is carried by the caller's object key, never inferred
4703
+ from where a QUOTE-ID happened to surface in translated narrative.
4704
+ The model translates only the frozen quotation payload; the outer
4705
+ quotation punctuation is rendered deterministically by language.
4706
+ """
4707
+ text = str(translated or '').strip()
4708
+ if not text:
4709
+ raise DeepSeekRequestError('Qualitative quote translation is empty.')
4710
+ if re.search(r'\bQUOTE-\d+\b', text):
4711
+ raise DeepSeekRequestError('Qualitative quote translation leaked an internal Quote ID.')
4712
+
4713
+ quote_pairs = (
4714
+ ('"', '"'), ('“', '”'), ('«', '»'), ('„', '“'), ('「', '」'), ('『', '』'),
4715
+ )
4716
+ inner = text
4717
+ for left, right in quote_pairs:
4718
+ if inner.startswith(left) and inner.endswith(right) and len(inner) > len(left) + len(right):
4719
+ inner = inner[len(left):-len(right)].strip()
4720
+ break
4721
+ if not inner:
4722
+ raise DeepSeekRequestError('Qualitative quote translation contains no target text.')
4723
+
4724
+ profile = cls._translation_language_profile(target_lang)
4725
+ if profile.get('language_code') == 'zh':
4726
+ left, right = '“', '”'
4727
+ elif profile.get('language_code') in {'ru', 'kk'}:
4728
+ left, right = '«', '»'
4729
+ else:
4730
+ left, right = '“', '”'
4731
+ return f'{left}{inner}{right}'
4732
+
4733
+ def translate_qualitative_quote_objects(
4734
+ self, quote_objects, target_lang, glossary=None, max_attempts=2, stage_callback=None,
4735
+ ):
4736
+ """Translate frozen qualitative quotations as typed objects.
4737
+
4738
+ Each QUOTE-ID is an out-of-band object key. The corresponding English
4739
+ quotation is translated in isolation through the existing frozen-section
4740
+ compiler and QA stack. The returned mapping is therefore authoritative
4741
+ without parsing target-language narrative surface order or punctuation.
4742
+ """
4743
+ if not isinstance(quote_objects, dict):
4744
+ raise DeepSeekRequestError('Qualitative quote objects must be a mapping.')
4745
+ frozen_glossary = dict(glossary or {})
4746
+ translations = {}
4747
+ for tag, source_quote in sorted(quote_objects.items()):
4748
+ tag_value = str(tag or '').strip()
4749
+ source_value = str(source_quote or '').strip()
4750
+ if not re.fullmatch(r'QUOTE-\d+', tag_value):
4751
+ raise DeepSeekRequestError(f'Invalid qualitative Quote ID: {tag_value!r}.')
4752
+ if not source_value:
4753
+ raise DeepSeekRequestError(f'Qualitative quote source is empty: {tag_value}.')
4754
+ result = self.translate_frozen_section(
4755
+ source_value,
4756
+ target_lang,
4757
+ glossary=frozen_glossary,
4758
+ section_name=f'Qualitative Quote {tag_value}',
4759
+ max_attempts=max_attempts,
4760
+ stage_callback=stage_callback,
4761
+ )
4762
+ translated = str((result or {}).get('text') or '').strip()
4763
+ translations[tag_value] = self._render_qualitative_quote_translation(translated, target_lang)
4764
+ returned_glossary = (result or {}).get('glossary')
4765
+ if isinstance(returned_glossary, dict):
4766
+ frozen_glossary = dict(returned_glossary)
4767
+ return {
4768
+ 'translations': translations,
4769
+ 'glossary': frozen_glossary,
4770
+ 'qa_status': 'passed',
4771
+ 'translation_contract_version': self.TRANSLATION_CONTRACT_VERSION,
4772
+ }
4773
+
4774
  def validate_translation_semantic_equivalence(self, source, translated, target_lang, section_name=''):
4775
  """Fail closed when translation changes claim meaning or evidence strength."""
4776
  source_value = str(source or '').strip()