Spaces:
Running
Running
Step 4A-6-C-R3.2.8.5 Claim-Bound Translation Compiler + Refresh Diagnostics + 100ms Streaming + Four-Language Arabic Heading Numbering
#23
by GuXiaopeng - opened
- __pycache__/app.cpython-313.pyc +0 -0
- __pycache__/config.cpython-313.pyc +0 -0
- __pycache__/desktop.ini +0 -2
- __pycache__/i18n.cpython-313.pyc +0 -0
- __pycache__/managed_auth.cpython-313.pyc +0 -0
- __pycache__/task_executor.cpython-313.pyc +0 -0
- app.py +8 -0
- core/__pycache__/checkpoint_store.cpython-313.pyc +0 -0
- core/__pycache__/desktop.ini +0 -2
- core/__pycache__/generation_runner.cpython-313.pyc +0 -0
- core/__pycache__/meta_extractor.cpython-313.pyc +0 -0
- core/__pycache__/pipeline_agent.cpython-313.pyc +0 -3
- core/__pycache__/production_guard.cpython-313.pyc +0 -0
- core/__pycache__/qualitative_engine.cpython-313.pyc +0 -0
- core/__pycache__/quantitative_engine.cpython-313.pyc +0 -0
- core/__pycache__/rag_engine.cpython-313.pyc +0 -0
- core/generation_runner.py +1 -1
- core/pipeline_agent.py +353 -132
- smartpaper_restore_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- smartpaper_restore_bridge/__pycache__/desktop.ini +0 -2
- smartpaper_restore_bridge/frontend/index.html +142 -8
- utils/__pycache__/desktop.ini +0 -2
- utils/__pycache__/docx_builder.cpython-313.pyc +0 -0
- utils/__pycache__/file_parser.cpython-313.pyc +0 -0
__pycache__/app.cpython-313.pyc
DELETED
|
Binary file (60.6 kB)
|
|
|
__pycache__/config.cpython-313.pyc
DELETED
|
Binary file (31.4 kB)
|
|
|
__pycache__/desktop.ini
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
[.ShellClassInfo]
|
| 2 |
-
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
|
|
|
|
|
|
|
|
__pycache__/i18n.cpython-313.pyc
DELETED
|
Binary file (76.6 kB)
|
|
|
__pycache__/managed_auth.cpython-313.pyc
DELETED
|
Binary file (31 kB)
|
|
|
__pycache__/task_executor.cpython-313.pyc
DELETED
|
Binary file (33.6 kB)
|
|
|
app.py
CHANGED
|
@@ -709,6 +709,14 @@ def _watch_task_status_live(task: dict) -> dict | None:
|
|
| 709 |
key=f"sp_task_watch_{_safe_key(task_id)}",
|
| 710 |
)
|
| 711 |
boundary_task = watch_result.get("task") if isinstance(watch_result, dict) and watch_result.get("boundary") else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
if isinstance(boundary_task, dict) and str(boundary_task.get("task_id") or "") == task_id:
|
| 713 |
st.session_state["sp_generation_task"] = dict(boundary_task)
|
| 714 |
st.session_state["sp_last_ui_task_status"] = str(boundary_task.get("status") or "")
|
|
|
|
| 709 |
key=f"sp_task_watch_{_safe_key(task_id)}",
|
| 710 |
)
|
| 711 |
boundary_task = watch_result.get("task") if isinstance(watch_result, dict) and watch_result.get("boundary") else None
|
| 712 |
+
if isinstance(watch_result, dict) and watch_result.get("boundary"):
|
| 713 |
+
navdiag = watch_result.get("ui_navdiag")
|
| 714 |
+
if isinstance(navdiag, dict):
|
| 715 |
+
try:
|
| 716 |
+
safe_diag = json.dumps(navdiag, ensure_ascii=False, separators=(",", ":"))[:4000]
|
| 717 |
+
logging.info("[SP_UI_NAVDIAG] task_id=%s snapshot=%s", task_id, safe_diag)
|
| 718 |
+
except Exception:
|
| 719 |
+
logging.info("[SP_UI_NAVDIAG] task_id=%s snapshot_unserializable", task_id)
|
| 720 |
if isinstance(boundary_task, dict) and str(boundary_task.get("task_id") or "") == task_id:
|
| 721 |
st.session_state["sp_generation_task"] = dict(boundary_task)
|
| 722 |
st.session_state["sp_last_ui_task_status"] = str(boundary_task.get("status") or "")
|
core/__pycache__/checkpoint_store.cpython-313.pyc
DELETED
|
Binary file (4.62 kB)
|
|
|
core/__pycache__/desktop.ini
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
[.ShellClassInfo]
|
| 2 |
-
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
|
|
|
|
|
|
|
|
core/__pycache__/generation_runner.cpython-313.pyc
DELETED
|
Binary file (65 kB)
|
|
|
core/__pycache__/meta_extractor.cpython-313.pyc
DELETED
|
Binary file (3.42 kB)
|
|
|
core/__pycache__/pipeline_agent.cpython-313.pyc
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:a58cec79f28771e880158bd36b2afd8c77ac1982a61db28ff832a962c41ed79c
|
| 3 |
-
size 251613
|
|
|
|
|
|
|
|
|
|
|
|
core/__pycache__/production_guard.cpython-313.pyc
DELETED
|
Binary file (11 kB)
|
|
|
core/__pycache__/qualitative_engine.cpython-313.pyc
DELETED
|
Binary file (15.3 kB)
|
|
|
core/__pycache__/quantitative_engine.cpython-313.pyc
DELETED
|
Binary file (24.4 kB)
|
|
|
core/__pycache__/rag_engine.cpython-313.pyc
DELETED
|
Binary file (4.36 kB)
|
|
|
core/generation_runner.py
CHANGED
|
@@ -33,7 +33,7 @@ ProgressCallback = Callable[..., None]
|
|
| 33 |
MIN_FINAL_REFERENCES = 50
|
| 34 |
LITERATURE_EVIDENCE_TARGET = 75
|
| 35 |
CITATION_PLAN_TARGET = 70
|
| 36 |
-
TRANSLATION_CONTRACT_VERSION = '
|
| 37 |
|
| 38 |
|
| 39 |
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
|
|
|
| 33 |
MIN_FINAL_REFERENCES = 50
|
| 34 |
LITERATURE_EVIDENCE_TARGET = 75
|
| 35 |
CITATION_PLAN_TARGET = 70
|
| 36 |
+
TRANSLATION_CONTRACT_VERSION = 'claim-bound-v1'
|
| 37 |
|
| 38 |
|
| 39 |
def _clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
|
core/pipeline_agent.py
CHANGED
|
@@ -56,7 +56,7 @@ class CitationAuditError(DeepSeekRequestError):
|
|
| 56 |
|
| 57 |
|
| 58 |
class SSciPipelineAgent:
|
| 59 |
-
TRANSLATION_CONTRACT_VERSION = '
|
| 60 |
|
| 61 |
def __init__(self, ds_key, s2_key=""):
|
| 62 |
self.ds_key = str(ds_key or "").strip()
|
|
@@ -3310,7 +3310,7 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3310 |
|
| 3311 |
@staticmethod
|
| 3312 |
def _translation_prompt(target_lang):
|
| 3313 |
-
common = """Translate the supplied blocks from the locked English manuscript faithfully. Preserve every [[SP-Pxxxxxx]] paragraph marker
|
| 3314 |
return common + ' ' + SSciPipelineAgent._translation_language_rule(target_lang)
|
| 3315 |
|
| 3316 |
@staticmethod
|
|
@@ -3583,8 +3583,73 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3583 |
)
|
| 3584 |
|
| 3585 |
@classmethod
|
| 3586 |
-
def
|
| 3587 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3588 |
source = str(value or '')
|
| 3589 |
mapping = {}
|
| 3590 |
counter = 0
|
|
@@ -3598,32 +3663,65 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3598 |
anchor_type = cls._translation_anchor_type(original)
|
| 3599 |
mapping[placeholder] = {
|
| 3600 |
'fact_id': f'F{counter:06d}',
|
| 3601 |
-
'source': original,
|
| 3602 |
'type': anchor_type,
|
| 3603 |
'canonical': cls._translation_fact_canonical(original, anchor_type),
|
| 3604 |
'render_policy': cls._translation_fact_render_policy(anchor_type),
|
|
|
|
|
|
|
|
|
|
| 3605 |
}
|
| 3606 |
return placeholder
|
| 3607 |
|
| 3608 |
-
|
| 3609 |
-
|
| 3610 |
-
|
| 3611 |
-
|
| 3612 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3613 |
|
| 3614 |
@classmethod
|
| 3615 |
def _model_safe_translation_manifest(cls, mapping):
|
| 3616 |
-
"""Expose
|
| 3617 |
safe = []
|
| 3618 |
for placeholder, raw in (mapping or {}).items():
|
| 3619 |
item = raw if isinstance(raw, dict) else {'type':'IMMUTABLE'}
|
| 3620 |
-
anchor_type = str(item.get('type') or 'IMMUTABLE')
|
| 3621 |
safe.append({
|
| 3622 |
'id': placeholder,
|
| 3623 |
'fact_id': str(item.get('fact_id') or placeholder),
|
| 3624 |
-
'type':
|
| 3625 |
-
'
|
| 3626 |
-
'mobility': '
|
| 3627 |
})
|
| 3628 |
return safe
|
| 3629 |
|
|
@@ -3635,10 +3733,10 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3635 |
item = raw if isinstance(raw, dict) else {'source':str(raw),'type':'IMMUTABLE'}
|
| 3636 |
records.append({
|
| 3637 |
'block_id': str(block_id),
|
|
|
|
| 3638 |
'fact_id': str(item.get('fact_id') or placeholder),
|
| 3639 |
'type': str(item.get('type') or 'IMMUTABLE'),
|
| 3640 |
'canonical': str(item.get('canonical') or item.get('source') or ''),
|
| 3641 |
-
'sentence_index': int(item.get('sentence_index') or 0),
|
| 3642 |
'render_policy': str(item.get('render_policy') or 'exact'),
|
| 3643 |
})
|
| 3644 |
payload = json.dumps(records, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')
|
|
@@ -3646,7 +3744,7 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3646 |
|
| 3647 |
@classmethod
|
| 3648 |
def translation_fact_ledger_hash(cls, section_text):
|
| 3649 |
-
"""Deterministic
|
| 3650 |
units = cls._translation_body_units(section_text)
|
| 3651 |
ledger = {}
|
| 3652 |
body_index = 0
|
|
@@ -3654,7 +3752,7 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3654 |
if unit.get('kind') != 'body':
|
| 3655 |
continue
|
| 3656 |
body_index += 1
|
| 3657 |
-
_protected, mapping = cls.
|
| 3658 |
ledger[f'{body_index:06d}'] = mapping
|
| 3659 |
return cls._fact_ledger_hash(ledger)
|
| 3660 |
|
|
@@ -3668,11 +3766,62 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3668 |
return str(item.get('canonical'))
|
| 3669 |
return str(item.get('source') or '')
|
| 3670 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3671 |
@classmethod
|
| 3672 |
def _translation_unowned_objects(cls, protected_target):
|
| 3673 |
-
"""Find factual
|
| 3674 |
text = re.sub(r'\[\[SP-T\d{6}\]\]', ' ', str(protected_target or ''))
|
| 3675 |
text = re.sub(r'\[\[SP-P\d{6}\]\]', ' ', text)
|
|
|
|
| 3676 |
unowned = {
|
| 3677 |
'numeric': list(cls._numeric_tokens(text).elements()),
|
| 3678 |
'citation': list(cls._translation_citation_tokens(text).elements()),
|
|
@@ -3688,8 +3837,9 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3688 |
return {key: values for key, values in unowned.items() if values}
|
| 3689 |
|
| 3690 |
@classmethod
|
| 3691 |
-
def _validate_translation_skeleton(cls, value, mapping, block_id=''):
|
| 3692 |
-
|
|
|
|
| 3693 |
expected = Counter((mapping or {}).keys())
|
| 3694 |
if found != expected:
|
| 3695 |
missing = list((expected - found).elements())[:8]
|
|
@@ -3697,47 +3847,78 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3697 |
raise DeepSeekRequestError(
|
| 3698 |
f'Translation protected placeholder mismatch in {block_id}: missing={missing}, added={added}.'
|
| 3699 |
)
|
| 3700 |
-
|
| 3701 |
-
|
| 3702 |
-
|
| 3703 |
-
|
| 3704 |
-
|
| 3705 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3706 |
for placeholder, raw_manifest in (mapping or {}).items():
|
| 3707 |
manifest = raw_manifest if isinstance(raw_manifest, dict) else {'type':'IMMUTABLE'}
|
| 3708 |
-
|
| 3709 |
-
|
| 3710 |
-
if target_sentence != source_sentence:
|
| 3711 |
raise DeepSeekRequestError(
|
| 3712 |
-
f'Translation fact moved across
|
| 3713 |
)
|
| 3714 |
-
|
| 3715 |
-
# inside each claim. DATE/CITATION/NAME may legitimately move for target
|
| 3716 |
-
# grammar, but two sample/statistical values must never trade owners.
|
| 3717 |
ordered_types = {'NUMBER','QUANTITY','STAT','HYPOTHESIS','DOI_URL','FORMULA','QUOTE_ID'}
|
| 3718 |
-
for
|
|
|
|
| 3719 |
expected_order = [
|
| 3720 |
-
|
| 3721 |
-
if
|
| 3722 |
and str((raw if isinstance(raw, dict) else {}).get('type') or '') in ordered_types
|
| 3723 |
]
|
| 3724 |
if len(expected_order) > 1:
|
| 3725 |
-
actual_order = sorted(expected_order, key=lambda
|
| 3726 |
if actual_order != expected_order:
|
| 3727 |
raise DeepSeekRequestError(
|
| 3728 |
-
f'Translation fact order changed within owner
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3729 |
)
|
| 3730 |
return True
|
| 3731 |
|
| 3732 |
@classmethod
|
| 3733 |
-
def _restore_translation_tokens(cls, value, mapping, block_id='', target_lang=''):
|
| 3734 |
text = str(value or '')
|
| 3735 |
-
cls._validate_translation_skeleton(text, mapping, block_id)
|
| 3736 |
for placeholder, manifest in (mapping or {}).items():
|
| 3737 |
text = text.replace(placeholder, cls._render_translation_fact(manifest, target_lang))
|
| 3738 |
if re.search(r'\[\[SP-T\d{6}\]\]', text):
|
| 3739 |
raise DeepSeekRequestError(f'Translation left unresolved protected placeholders in {block_id}.')
|
| 3740 |
-
return text
|
| 3741 |
|
| 3742 |
def extend_translation_glossary(self, body, target_lang, existing=None):
|
| 3743 |
"""Extend a task-level glossary without remapping an existing term."""
|
|
@@ -3826,12 +4007,7 @@ SECTION: {section_name}
|
|
| 3826 |
return True
|
| 3827 |
|
| 3828 |
def audit_translation_blocks(self, sources, translations, target_lang, glossary=None, section_name=''):
|
| 3829 |
-
"""Return
|
| 3830 |
-
|
| 3831 |
-
This combines semantic-equivalence and target-language quality review so
|
| 3832 |
-
a failed paragraph can be regenerated independently rather than forcing
|
| 3833 |
-
a whole-section translation retry.
|
| 3834 |
-
"""
|
| 3835 |
source_map = {str(key): str(value or '').strip() for key, value in dict(sources or {}).items()}
|
| 3836 |
target_map = {str(key): str(value or '').strip() for key, value in dict(translations or {}).items()}
|
| 3837 |
expected_ids = [key for key in source_map if key in target_map]
|
|
@@ -3840,59 +4016,72 @@ SECTION: {section_name}
|
|
| 3840 |
if not expected_ids:
|
| 3841 |
return {}
|
| 3842 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3843 |
glossary_value = dict(glossary or {})
|
| 3844 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3845 |
issues = {}
|
| 3846 |
-
|
| 3847 |
-
|
| 3848 |
-
|
| 3849 |
-
joined_source = '\n'.join(
|
| 3850 |
-
|
| 3851 |
-
|
| 3852 |
-
|
|
|
|
|
|
|
| 3853 |
payload = {
|
| 3854 |
'section': str(section_name or 'section'),
|
| 3855 |
'target_language': target_lang,
|
| 3856 |
'glossary': relevant_glossary,
|
| 3857 |
-
'
|
| 3858 |
-
|
| 3859 |
-
'id': f'SP-P{block_id}',
|
| 3860 |
-
'english': source_map[block_id],
|
| 3861 |
-
'target': target_map[block_id],
|
| 3862 |
-
}
|
| 3863 |
-
for block_id in batch_ids
|
| 3864 |
-
],
|
| 3865 |
}
|
| 3866 |
-
system_prompt = f"""You are a bilingual academic translation QA
|
| 3867 |
-
Review EACH
|
| 3868 |
-
|
| 3869 |
-
|
| 3870 |
response = self.call_deepseek(
|
| 3871 |
-
system_prompt,
|
| 3872 |
-
|
| 3873 |
-
max_tokens=max(1200, min(3600, 700 + 420 * len(batch_ids))),
|
| 3874 |
temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
|
| 3875 |
)
|
| 3876 |
parsed = self._extract_json_from_text(response)
|
| 3877 |
-
|
| 3878 |
-
by_id = {}
|
| 3879 |
-
for row in rows
|
| 3880 |
-
|
| 3881 |
-
continue
|
| 3882 |
-
raw_id = str(row.get('id') or '').strip()
|
| 3883 |
-
match = re.fullmatch(r'SP-P(\d{6})', raw_id)
|
| 3884 |
-
if match and match.group(1) in batch_ids and match.group(1) not in by_id:
|
| 3885 |
-
by_id[match.group(1)] = row
|
| 3886 |
-
if set(by_id) != set(batch_ids):
|
| 3887 |
raise DeepSeekRequestError(
|
| 3888 |
-
f'Translation
|
| 3889 |
)
|
| 3890 |
-
for
|
| 3891 |
-
|
| 3892 |
-
|
|
|
|
| 3893 |
continue
|
| 3894 |
-
|
| 3895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3896 |
return issues
|
| 3897 |
|
| 3898 |
@staticmethod
|
|
@@ -4032,20 +4221,16 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4032 |
return translated
|
| 4033 |
|
| 4034 |
def _translate_marked_batch_with_recovery(self, batch, system_prompt, target_lang, _strict=False):
|
| 4035 |
-
"""Translate
|
| 4036 |
-
|
| 4037 |
-
Multi-block marker/protected-token failures are binary-split first. A
|
| 4038 |
-
single block may accept a marker-free response only when it is the sole
|
| 4039 |
-
source, after which every protected object and existing integrity gate
|
| 4040 |
-
must still pass.
|
| 4041 |
-
"""
|
| 4042 |
expected_ids = [block_id for block_id, _ in batch]
|
| 4043 |
protected_batch = []
|
| 4044 |
protected_maps = {}
|
|
|
|
| 4045 |
for block_id, source in batch:
|
| 4046 |
-
protected_source, token_map = self.
|
| 4047 |
protected_batch.append((block_id, source, protected_source))
|
| 4048 |
protected_maps[block_id] = token_map
|
|
|
|
| 4049 |
marked_source = '\n\n'.join(
|
| 4050 |
f'[[SP-P{block_id}]]\n{protected_source}'
|
| 4051 |
for block_id, _, protected_source in protected_batch
|
|
@@ -4058,23 +4243,20 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4058 |
'\n<SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 4059 |
+ json.dumps(semantic_manifest, ensure_ascii=False)
|
| 4060 |
+ '\n</SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 4061 |
-
'This manifest intentionally contains no factual values.
|
| 4062 |
-
'
|
| 4063 |
-
'
|
| 4064 |
)
|
| 4065 |
if _strict:
|
| 4066 |
effective_prompt += (
|
| 4067 |
-
'\nSTRICT RETRY: Copy every SP-P and SP-T marker byte-for-byte. Do not merge, drop, translate, '
|
| 4068 |
-
'pluralize, or normalize any protected token.
|
| 4069 |
)
|
| 4070 |
result = self.call_deepseek(
|
| 4071 |
effective_prompt, marked_source,
|
| 4072 |
max_tokens=max(1800, min(9000, self._count_tokens(marked_source) * 2 + 1200)),
|
| 4073 |
temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
|
| 4074 |
)
|
| 4075 |
-
# Translation is non-destructive: never run generic manuscript cleanup
|
| 4076 |
-
# on target-language text. Marker/placeholder parsing rejects malformed
|
| 4077 |
-
# output instead of deleting potentially legitimate manuscript lines.
|
| 4078 |
try:
|
| 4079 |
parsed = self._parse_marked_translation(result, expected_ids)
|
| 4080 |
except DeepSeekRequestError as error:
|
|
@@ -4117,19 +4299,18 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4117 |
failed = {}
|
| 4118 |
for block_id, source in batch:
|
| 4119 |
try:
|
| 4120 |
-
|
| 4121 |
-
parsed[block_id], protected_maps.get(block_id, {}), f'SP-P{block_id}', target_lang
|
| 4122 |
-
|
| 4123 |
-
restored[block_id] = self._validate_translation_block_protected(
|
| 4124 |
-
block_id, source, translated, target_lang
|
| 4125 |
)
|
|
|
|
|
|
|
|
|
|
| 4126 |
except DeepSeekRequestError as error:
|
| 4127 |
failed[block_id] = error
|
| 4128 |
|
| 4129 |
if failed:
|
| 4130 |
if _strict:
|
| 4131 |
-
# A strict isolated retry already used the protected gateway.
|
| 4132 |
-
# Failing again is fail-closed; never fall back to raw source.
|
| 4133 |
first_id = next(iter(failed))
|
| 4134 |
raise failed[first_id]
|
| 4135 |
source_by_id = {block_id: source for block_id, source in batch}
|
|
@@ -4144,6 +4325,36 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4144 |
restored[block_id] = recovered[block_id]
|
| 4145 |
return restored
|
| 4146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4147 |
def _audit_translation_blocks_with_retry(
|
| 4148 |
self, sources, translations, target_lang, glossary=None, section_name='', max_attempts=2
|
| 4149 |
):
|
|
@@ -4214,35 +4425,45 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4214 |
local_round = 0
|
| 4215 |
while quality_issues and local_round < 2:
|
| 4216 |
local_round += 1
|
| 4217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4218 |
logging.warning(
|
| 4219 |
-
'Translation
|
| 4220 |
-
section_name or 'section', local_round,
|
|
|
|
| 4221 |
)
|
| 4222 |
-
|
| 4223 |
-
|
| 4224 |
-
|
| 4225 |
-
|
| 4226 |
-
|
| 4227 |
-
|
| 4228 |
-
|
| 4229 |
-
|
| 4230 |
-
|
| 4231 |
-
|
|
|
|
|
|
|
|
|
|
| 4232 |
)
|
| 4233 |
-
translations[block_id] =
|
|
|
|
| 4234 |
quality_issues = self._audit_translation_blocks_with_retry(
|
| 4235 |
-
{block_id: source_blocks[block_id] for block_id in
|
| 4236 |
-
{block_id: translations[block_id] for block_id in
|
| 4237 |
target_lang, frozen_glossary, section_name=section_name or 'frozen section', max_attempts=2,
|
| 4238 |
)
|
| 4239 |
if quality_issues:
|
| 4240 |
detail = '; '.join(
|
| 4241 |
-
f'SP-P{block_id}: {"
|
| 4242 |
-
for block_id,
|
|
|
|
| 4243 |
)
|
| 4244 |
raise DeepSeekRequestError(
|
| 4245 |
-
f'Translation
|
| 4246 |
)
|
| 4247 |
|
| 4248 |
# Rebuild after any localized repairs, then apply deterministic
|
|
@@ -4257,7 +4478,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4257 |
output_units.append('#' * int(unit['level']) + ' ' + translated_heading)
|
| 4258 |
else:
|
| 4259 |
body_index += 1
|
| 4260 |
-
output_units.append(translations[f'{body_index:06d}'])
|
| 4261 |
output = '\n\n'.join(output_units).strip()
|
| 4262 |
self.validate_translation_alignment(value, output)
|
| 4263 |
self._validate_translation_language_quality(output, target_lang)
|
|
@@ -4275,7 +4496,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4275 |
last_error = error
|
| 4276 |
message = str(error)
|
| 4277 |
if (
|
| 4278 |
-
message.startswith('Translation
|
| 4279 |
or message.startswith('Translation alignment')
|
| 4280 |
or message.startswith('Translation changed ')
|
| 4281 |
or message.startswith('Translation protected ')
|
|
@@ -4342,7 +4563,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4342 |
output_units.append('#' * int(unit['level']) + ' ' + translated_heading)
|
| 4343 |
else:
|
| 4344 |
body_index += 1
|
| 4345 |
-
output_units.append(translations[f'{body_index:06d}'])
|
| 4346 |
|
| 4347 |
output = '\n\n'.join(output_units).strip()
|
| 4348 |
if references:
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
class SSciPipelineAgent:
|
| 59 |
+
TRANSLATION_CONTRACT_VERSION = 'claim-bound-v1'
|
| 60 |
|
| 61 |
def __init__(self, ds_key, s2_key=""):
|
| 62 |
self.ds_key = str(ds_key or "").strip()
|
|
|
|
| 3310 |
|
| 3311 |
@staticmethod
|
| 3312 |
def _translation_prompt(target_lang):
|
| 3313 |
+
common = """Translate the supplied blocks from the locked English manuscript faithfully. Preserve every [[SP-Pxxxxxx]] paragraph marker, [[SP-Cxxxxxx]] claim marker, and [[SP-Txxxxxx]] semantic anchor exactly once. Semantic anchors are typed immutable research objects. Keep each anchor attached to the same SP-C claim; citation anchors must stay with the same claim. Target-language sentence count and punctuation may differ naturally inside a claim. Never move an anchor into another claim. Do not translate, normalize, duplicate, or delete anchor placeholders. Preserve quotation meaning and conclusion direction. Do not add or remove claims. Use concise, accurate, rigorous academic language. Avoid metaphors, analogies, rhetorical questions, promotional language, formulaic transitions, and mechanical repetition. Output only the marked translations."""
|
| 3314 |
return common + ' ' + SSciPipelineAgent._translation_language_rule(target_lang)
|
| 3315 |
|
| 3316 |
@staticmethod
|
|
|
|
| 3583 |
)
|
| 3584 |
|
| 3585 |
@classmethod
|
| 3586 |
+
def _translation_claim_units(cls, value):
|
| 3587 |
+
"""Create stable source-side claim IDs without constraining target punctuation."""
|
| 3588 |
+
text = str(value or '').strip()
|
| 3589 |
+
if not text:
|
| 3590 |
+
return []
|
| 3591 |
+
abbreviations = ('e.g.', 'i.e.', 'et al.', 'fig.', 'dr.', 'mr.', 'ms.', 'prof.', 'vs.', 'no.', 'vol.')
|
| 3592 |
+
boundaries = []
|
| 3593 |
+
for match in re.finditer(r'[.!?](?=\s+(?:["“‘\[]?[A-Z0-9])|$)', text):
|
| 3594 |
+
end = match.end()
|
| 3595 |
+
prefix = text[:end].casefold()
|
| 3596 |
+
is_abbrev = any(re.search(rf'(?:^|[^a-z]){re.escape(item)}$', prefix) for item in abbreviations)
|
| 3597 |
+
if not is_abbrev:
|
| 3598 |
+
boundaries.append(end)
|
| 3599 |
+
claims = []
|
| 3600 |
+
start = 0
|
| 3601 |
+
for end in boundaries:
|
| 3602 |
+
part = text[start:end].strip()
|
| 3603 |
+
if part:
|
| 3604 |
+
claims.append(part)
|
| 3605 |
+
start = end
|
| 3606 |
+
tail = text[start:].strip()
|
| 3607 |
+
if tail:
|
| 3608 |
+
claims.append(tail)
|
| 3609 |
+
return [
|
| 3610 |
+
{'claim_id': f'C{index:06d}', 'text': claim}
|
| 3611 |
+
for index, claim in enumerate(claims or [text], start=1)
|
| 3612 |
+
]
|
| 3613 |
+
|
| 3614 |
+
@classmethod
|
| 3615 |
+
def _translation_lexical_quantifier_candidates(cls, value):
|
| 3616 |
+
"""High-confidence source lexical quantities that may render as Arabic digits.
|
| 3617 |
+
|
| 3618 |
+
These candidates are private attribution evidence, not model-visible facts.
|
| 3619 |
+
Rhetorical ordinals are intentionally excluded unless attached to a bounded
|
| 3620 |
+
research/structural noun.
|
| 3621 |
+
"""
|
| 3622 |
+
text = str(value or '')
|
| 3623 |
+
cardinal = {
|
| 3624 |
+
'zero':'0','one':'1','two':'2','three':'3','four':'4','five':'5','six':'6',
|
| 3625 |
+
'seven':'7','eight':'8','nine':'9','ten':'10','eleven':'11','twelve':'12',
|
| 3626 |
+
}
|
| 3627 |
+
ordinal = {
|
| 3628 |
+
'first':'1','second':'2','third':'3','fourth':'4','fifth':'5','sixth':'6',
|
| 3629 |
+
'seventh':'7','eighth':'8','ninth':'9','tenth':'10','eleventh':'11','twelfth':'12',
|
| 3630 |
+
}
|
| 3631 |
+
measurable_nouns = (
|
| 3632 |
+
r'mechanisms?|groups?|stages?|steps?|waves?|models?|hypotheses?|dimensions?|factors?|items?|'
|
| 3633 |
+
r'rounds?|phases?|samples?|studies?|analyses?|conditions?|paths?|constructs?|measures?|components?|'
|
| 3634 |
+
r'categories?|levels?|questions?|options?|variables?|sections?|claims?|explanations?|participants?|teams?'
|
| 3635 |
+
)
|
| 3636 |
+
found = []
|
| 3637 |
+
cardinal_words = '|'.join(cardinal)
|
| 3638 |
+
for match in re.finditer(rf'(?i)\b({cardinal_words})\s+({measurable_nouns})\b', text):
|
| 3639 |
+
found.append(cardinal[match.group(1).casefold()])
|
| 3640 |
+
ordinal_words = '|'.join(ordinal)
|
| 3641 |
+
for match in re.finditer(rf'(?i)\b({ordinal_words})\s+({measurable_nouns})\b', text):
|
| 3642 |
+
found.append(ordinal[match.group(1).casefold()])
|
| 3643 |
+
for match in re.finditer(r'(?i)\b(twice|twofold|threefold|fourfold)\b', text):
|
| 3644 |
+
found.append({'twice':'2','twofold':'2','threefold':'3','fourfold':'4'}[match.group(1).casefold()])
|
| 3645 |
+
# one-to-one is a relational form with two explicit quantitative roles.
|
| 3646 |
+
if re.search(r'(?i)\bone[- ]to[- ]one\b', text):
|
| 3647 |
+
found.extend(['1', '1'])
|
| 3648 |
+
return Counter(found)
|
| 3649 |
+
|
| 3650 |
+
@classmethod
|
| 3651 |
+
def _protect_translation_tokens(cls, value, claim_id='C000001'):
|
| 3652 |
+
"""Protect one claim while keeping factual values in a private server ledger."""
|
| 3653 |
source = str(value or '')
|
| 3654 |
mapping = {}
|
| 3655 |
counter = 0
|
|
|
|
| 3663 |
anchor_type = cls._translation_anchor_type(original)
|
| 3664 |
mapping[placeholder] = {
|
| 3665 |
'fact_id': f'F{counter:06d}',
|
| 3666 |
+
'source': original,
|
| 3667 |
'type': anchor_type,
|
| 3668 |
'canonical': cls._translation_fact_canonical(original, anchor_type),
|
| 3669 |
'render_policy': cls._translation_fact_render_policy(anchor_type),
|
| 3670 |
+
'claim_id': str(claim_id or 'C000001'),
|
| 3671 |
+
# Kept only as diagnostic compatibility; never authoritative.
|
| 3672 |
+
'sentence_index': 0,
|
| 3673 |
}
|
| 3674 |
return placeholder
|
| 3675 |
|
| 3676 |
+
return pattern.sub(replace, source), mapping
|
| 3677 |
+
|
| 3678 |
+
@classmethod
|
| 3679 |
+
def _protect_translation_claims(cls, value):
|
| 3680 |
+
"""Compile a paragraph into claim markers plus a private fact ledger."""
|
| 3681 |
+
claims = cls._translation_claim_units(value)
|
| 3682 |
+
pattern = cls._translation_protected_pattern()
|
| 3683 |
+
mapping = {}
|
| 3684 |
+
claim_sources = {}
|
| 3685 |
+
protected_parts = []
|
| 3686 |
+
fact_counter = 0
|
| 3687 |
+
for claim in claims:
|
| 3688 |
+
claim_id = claim['claim_id']
|
| 3689 |
+
claim_source = claim['text']
|
| 3690 |
+
claim_sources[claim_id] = claim_source
|
| 3691 |
+
|
| 3692 |
+
def replace(match, _claim_id=claim_id):
|
| 3693 |
+
nonlocal fact_counter
|
| 3694 |
+
fact_counter += 1
|
| 3695 |
+
placeholder = f'[[SP-T{fact_counter:06d}]]'
|
| 3696 |
+
original = match.group(0)
|
| 3697 |
+
anchor_type = cls._translation_anchor_type(original)
|
| 3698 |
+
mapping[placeholder] = {
|
| 3699 |
+
'fact_id': f'F{fact_counter:06d}',
|
| 3700 |
+
'source': original,
|
| 3701 |
+
'type': anchor_type,
|
| 3702 |
+
'canonical': cls._translation_fact_canonical(original, anchor_type),
|
| 3703 |
+
'render_policy': cls._translation_fact_render_policy(anchor_type),
|
| 3704 |
+
'claim_id': _claim_id,
|
| 3705 |
+
'sentence_index': 0,
|
| 3706 |
+
}
|
| 3707 |
+
return placeholder
|
| 3708 |
+
|
| 3709 |
+
protected_claim = pattern.sub(replace, claim_source)
|
| 3710 |
+
protected_parts.append(f'[[SP-{claim_id}]]\n{protected_claim}')
|
| 3711 |
+
return '\n'.join(protected_parts), mapping, claim_sources
|
| 3712 |
|
| 3713 |
@classmethod
|
| 3714 |
def _model_safe_translation_manifest(cls, mapping):
|
| 3715 |
+
"""Expose claim/type metadata only; factual values remain server-private."""
|
| 3716 |
safe = []
|
| 3717 |
for placeholder, raw in (mapping or {}).items():
|
| 3718 |
item = raw if isinstance(raw, dict) else {'type':'IMMUTABLE'}
|
|
|
|
| 3719 |
safe.append({
|
| 3720 |
'id': placeholder,
|
| 3721 |
'fact_id': str(item.get('fact_id') or placeholder),
|
| 3722 |
+
'type': str(item.get('type') or 'IMMUTABLE'),
|
| 3723 |
+
'claim_id': str(item.get('claim_id') or 'C000001'),
|
| 3724 |
+
'mobility': 'same_claim',
|
| 3725 |
})
|
| 3726 |
return safe
|
| 3727 |
|
|
|
|
| 3733 |
item = raw if isinstance(raw, dict) else {'source':str(raw),'type':'IMMUTABLE'}
|
| 3734 |
records.append({
|
| 3735 |
'block_id': str(block_id),
|
| 3736 |
+
'claim_id': str(item.get('claim_id') or 'C000001'),
|
| 3737 |
'fact_id': str(item.get('fact_id') or placeholder),
|
| 3738 |
'type': str(item.get('type') or 'IMMUTABLE'),
|
| 3739 |
'canonical': str(item.get('canonical') or item.get('source') or ''),
|
|
|
|
| 3740 |
'render_policy': str(item.get('render_policy') or 'exact'),
|
| 3741 |
})
|
| 3742 |
payload = json.dumps(records, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode('utf-8')
|
|
|
|
| 3744 |
|
| 3745 |
@classmethod
|
| 3746 |
def translation_fact_ledger_hash(cls, section_text):
|
| 3747 |
+
"""Deterministic claim-bound ledger hash used by translation checkpoint reuse."""
|
| 3748 |
units = cls._translation_body_units(section_text)
|
| 3749 |
ledger = {}
|
| 3750 |
body_index = 0
|
|
|
|
| 3752 |
if unit.get('kind') != 'body':
|
| 3753 |
continue
|
| 3754 |
body_index += 1
|
| 3755 |
+
_protected, mapping, _claims = cls._protect_translation_claims(unit.get('text') or '')
|
| 3756 |
ledger[f'{body_index:06d}'] = mapping
|
| 3757 |
return cls._fact_ledger_hash(ledger)
|
| 3758 |
|
|
|
|
| 3766 |
return str(item.get('canonical'))
|
| 3767 |
return str(item.get('source') or '')
|
| 3768 |
|
| 3769 |
+
@staticmethod
|
| 3770 |
+
def _translation_claim_marker(claim_id):
|
| 3771 |
+
return f'[[SP-{claim_id}]]'
|
| 3772 |
+
|
| 3773 |
+
@classmethod
|
| 3774 |
+
def _parse_translation_claim_segments(cls, value, expected_claim_ids=None):
|
| 3775 |
+
text = str(value or '')
|
| 3776 |
+
pattern = re.compile(r'(?m)^\[\[SP-(C\d{6})\]\]\s*$')
|
| 3777 |
+
matches = list(pattern.finditer(text))
|
| 3778 |
+
expected = list(expected_claim_ids or [])
|
| 3779 |
+
if not matches:
|
| 3780 |
+
if len(expected) <= 1:
|
| 3781 |
+
claim_id = expected[0] if expected else 'C000001'
|
| 3782 |
+
return [claim_id], {claim_id: text.strip()}
|
| 3783 |
+
raise DeepSeekRequestError(f'Translation claim marker mismatch: expected={expected}, got=[].')
|
| 3784 |
+
found = [match.group(1) for match in matches]
|
| 3785 |
+
if expected and found != expected:
|
| 3786 |
+
raise DeepSeekRequestError(f'Translation claim marker mismatch: expected={expected}, got={found}.')
|
| 3787 |
+
if text[:matches[0].start()].strip():
|
| 3788 |
+
raise DeepSeekRequestError('Translation output contained unexpected text before the first SP-C marker.')
|
| 3789 |
+
segments = {}
|
| 3790 |
+
for index, match in enumerate(matches):
|
| 3791 |
+
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
| 3792 |
+
segment = text[match.end():end].strip()
|
| 3793 |
+
if not segment:
|
| 3794 |
+
raise DeepSeekRequestError(f'Translation claim {match.group(1)} is empty.')
|
| 3795 |
+
segments[match.group(1)] = segment
|
| 3796 |
+
return found, segments
|
| 3797 |
+
|
| 3798 |
+
@classmethod
|
| 3799 |
+
def _compose_translation_claim_segments(cls, claim_order, segments):
|
| 3800 |
+
return '\n'.join(
|
| 3801 |
+
f'{cls._translation_claim_marker(claim_id)}\n{str(segments.get(claim_id) or "").strip()}'
|
| 3802 |
+
for claim_id in claim_order
|
| 3803 |
+
).strip()
|
| 3804 |
+
|
| 3805 |
+
@classmethod
|
| 3806 |
+
def _strip_translation_claim_markers(cls, value):
|
| 3807 |
+
text = str(value or '')
|
| 3808 |
+
matches = list(re.finditer(r'(?m)^\[\[SP-(C\d{6})\]\]\s*$', text))
|
| 3809 |
+
if not matches:
|
| 3810 |
+
return text.strip()
|
| 3811 |
+
parts = []
|
| 3812 |
+
for index, match in enumerate(matches):
|
| 3813 |
+
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
| 3814 |
+
part = text[match.end():end].strip()
|
| 3815 |
+
if part:
|
| 3816 |
+
parts.append(part)
|
| 3817 |
+
return ' '.join(parts).strip()
|
| 3818 |
+
|
| 3819 |
@classmethod
|
| 3820 |
def _translation_unowned_objects(cls, protected_target):
|
| 3821 |
+
"""Find non-anchor factual surfaces; numeric candidates are adjudicated per claim."""
|
| 3822 |
text = re.sub(r'\[\[SP-T\d{6}\]\]', ' ', str(protected_target or ''))
|
| 3823 |
text = re.sub(r'\[\[SP-P\d{6}\]\]', ' ', text)
|
| 3824 |
+
text = re.sub(r'\[\[SP-C\d{6}\]\]', ' ', text)
|
| 3825 |
unowned = {
|
| 3826 |
'numeric': list(cls._numeric_tokens(text).elements()),
|
| 3827 |
'citation': list(cls._translation_citation_tokens(text).elements()),
|
|
|
|
| 3837 |
return {key: values for key, values in unowned.items() if values}
|
| 3838 |
|
| 3839 |
@classmethod
|
| 3840 |
+
def _validate_translation_skeleton(cls, value, mapping, block_id='', claim_sources=None):
|
| 3841 |
+
text = str(value or '')
|
| 3842 |
+
found = Counter(re.findall(r'\[\[SP-T\d{6}\]\]', text))
|
| 3843 |
expected = Counter((mapping or {}).keys())
|
| 3844 |
if found != expected:
|
| 3845 |
missing = list((expected - found).elements())[:8]
|
|
|
|
| 3847 |
raise DeepSeekRequestError(
|
| 3848 |
f'Translation protected placeholder mismatch in {block_id}: missing={missing}, added={added}.'
|
| 3849 |
)
|
| 3850 |
+
|
| 3851 |
+
private_claims = dict(claim_sources or {})
|
| 3852 |
+
expected_claim_ids = list(private_claims)
|
| 3853 |
+
if not expected_claim_ids:
|
| 3854 |
+
expected_claim_ids = list(dict.fromkeys(
|
| 3855 |
+
str((raw if isinstance(raw, dict) else {}).get('claim_id') or 'C000001')
|
| 3856 |
+
for raw in (mapping or {}).values()
|
| 3857 |
+
)) or ['C000001']
|
| 3858 |
+
claim_order, target_claims = cls._parse_translation_claim_segments(text, expected_claim_ids)
|
| 3859 |
+
|
| 3860 |
+
# Every protected fact must remain inside its source claim; target
|
| 3861 |
+
# sentence count/punctuation is deliberately non-authoritative.
|
| 3862 |
for placeholder, raw_manifest in (mapping or {}).items():
|
| 3863 |
manifest = raw_manifest if isinstance(raw_manifest, dict) else {'type':'IMMUTABLE'}
|
| 3864 |
+
claim_id = str(manifest.get('claim_id') or 'C000001')
|
| 3865 |
+
if placeholder not in str(target_claims.get(claim_id) or ''):
|
|
|
|
| 3866 |
raise DeepSeekRequestError(
|
| 3867 |
+
f'Translation fact moved across claim boundary in {block_id}: {manifest.get("fact_id") or placeholder}.'
|
| 3868 |
)
|
| 3869 |
+
|
|
|
|
|
|
|
| 3870 |
ordered_types = {'NUMBER','QUANTITY','STAT','HYPOTHESIS','DOI_URL','FORMULA','QUOTE_ID'}
|
| 3871 |
+
for claim_id in claim_order:
|
| 3872 |
+
target_claim = str(target_claims.get(claim_id) or '')
|
| 3873 |
expected_order = [
|
| 3874 |
+
placeholder for placeholder, raw in (mapping or {}).items()
|
| 3875 |
+
if str((raw if isinstance(raw, dict) else {}).get('claim_id') or 'C000001') == claim_id
|
| 3876 |
and str((raw if isinstance(raw, dict) else {}).get('type') or '') in ordered_types
|
| 3877 |
]
|
| 3878 |
if len(expected_order) > 1:
|
| 3879 |
+
actual_order = sorted(expected_order, key=lambda placeholder: target_claim.find(placeholder))
|
| 3880 |
if actual_order != expected_order:
|
| 3881 |
raise DeepSeekRequestError(
|
| 3882 |
+
f'Translation fact order changed within owner claim in {block_id}: claim={claim_id}.'
|
| 3883 |
+
)
|
| 3884 |
+
|
| 3885 |
+
unowned = cls._translation_unowned_objects(target_claim)
|
| 3886 |
+
numeric = list(unowned.pop('numeric', []))
|
| 3887 |
+
if unowned:
|
| 3888 |
+
safe_summary = {key: len(values) for key, values in unowned.items()}
|
| 3889 |
+
logging.warning('Translation fact integrity failed: block=%s claim=%s unowned=%s', block_id, claim_id, safe_summary)
|
| 3890 |
+
raise DeepSeekRequestError(
|
| 3891 |
+
f'Translation introduced unowned factual content in {block_id} claim={claim_id}: {safe_summary}.'
|
| 3892 |
+
)
|
| 3893 |
+
if numeric:
|
| 3894 |
+
allowed = cls._translation_lexical_quantifier_candidates(private_claims.get(claim_id, ''))
|
| 3895 |
+
unresolved = []
|
| 3896 |
+
for token in numeric:
|
| 3897 |
+
canonical = cls._canonicalize_numeric_fact(token)
|
| 3898 |
+
if allowed.get(canonical, 0) > 0:
|
| 3899 |
+
allowed[canonical] -= 1
|
| 3900 |
+
else:
|
| 3901 |
+
unresolved.append(token)
|
| 3902 |
+
if unresolved:
|
| 3903 |
+
safe_summary = {'numeric': len(unresolved)}
|
| 3904 |
+
logging.warning(
|
| 3905 |
+
'Translation fact integrity failed: block=%s claim=%s unowned=%s suspect_tokens=%s',
|
| 3906 |
+
block_id, claim_id, safe_summary, unresolved[:4],
|
| 3907 |
+
)
|
| 3908 |
+
raise DeepSeekRequestError(
|
| 3909 |
+
f'Translation introduced unowned factual content in {block_id} claim={claim_id}: {safe_summary}.'
|
| 3910 |
)
|
| 3911 |
return True
|
| 3912 |
|
| 3913 |
@classmethod
|
| 3914 |
+
def _restore_translation_tokens(cls, value, mapping, block_id='', target_lang='', claim_sources=None, keep_claim_markers=False):
|
| 3915 |
text = str(value or '')
|
| 3916 |
+
cls._validate_translation_skeleton(text, mapping, block_id, claim_sources=claim_sources)
|
| 3917 |
for placeholder, manifest in (mapping or {}).items():
|
| 3918 |
text = text.replace(placeholder, cls._render_translation_fact(manifest, target_lang))
|
| 3919 |
if re.search(r'\[\[SP-T\d{6}\]\]', text):
|
| 3920 |
raise DeepSeekRequestError(f'Translation left unresolved protected placeholders in {block_id}.')
|
| 3921 |
+
return text.strip() if keep_claim_markers else cls._strip_translation_claim_markers(text)
|
| 3922 |
|
| 3923 |
def extend_translation_glossary(self, body, target_lang, existing=None):
|
| 3924 |
"""Extend a task-level glossary without remapping an existing term."""
|
|
|
|
| 4007 |
return True
|
| 4008 |
|
| 4009 |
def audit_translation_blocks(self, sources, translations, target_lang, glossary=None, section_name=''):
|
| 4010 |
+
"""Return claim-local bounded QA issue codes without rewriting text."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4011 |
source_map = {str(key): str(value or '').strip() for key, value in dict(sources or {}).items()}
|
| 4012 |
target_map = {str(key): str(value or '').strip() for key, value in dict(translations or {}).items()}
|
| 4013 |
expected_ids = [key for key in source_map if key in target_map]
|
|
|
|
| 4016 |
if not expected_ids:
|
| 4017 |
return {}
|
| 4018 |
|
| 4019 |
+
allowed_codes = {
|
| 4020 |
+
'OMITTED_CLAIM','ADDED_CLAIM','NEGATION_DRIFT','CAUSALITY_STRENGTH_DRIFT',
|
| 4021 |
+
'UNCERTAINTY_DRIFT','TERMINOLOGY','GRAMMAR','LANGUAGE_CONTAMINATION',
|
| 4022 |
+
'CITATION_ATTACHMENT','ACRONYM_ATTACHMENT','SEMANTIC_DRIFT',
|
| 4023 |
+
}
|
| 4024 |
+
claim_rows = []
|
| 4025 |
glossary_value = dict(glossary or {})
|
| 4026 |
+
for block_id in expected_ids:
|
| 4027 |
+
source_claims = self._translation_claim_units(source_map[block_id])
|
| 4028 |
+
claim_order = [item['claim_id'] for item in source_claims]
|
| 4029 |
+
_order, target_claims = self._parse_translation_claim_segments(target_map[block_id], claim_order)
|
| 4030 |
+
for item in source_claims:
|
| 4031 |
+
claim_id = item['claim_id']
|
| 4032 |
+
claim_rows.append({
|
| 4033 |
+
'id': f'SP-P{block_id}-{claim_id}',
|
| 4034 |
+
'block_id': block_id,
|
| 4035 |
+
'claim_id': claim_id,
|
| 4036 |
+
'english': item['text'],
|
| 4037 |
+
'target': target_claims[claim_id],
|
| 4038 |
+
})
|
| 4039 |
+
|
| 4040 |
issues = {}
|
| 4041 |
+
batch_size = 8
|
| 4042 |
+
for offset in range(0, len(claim_rows), batch_size):
|
| 4043 |
+
rows = claim_rows[offset:offset + batch_size]
|
| 4044 |
+
joined_source = '\n'.join(row['english'] for row in rows)
|
| 4045 |
+
relevant_glossary = {
|
| 4046 |
+
str(source_term): str(target_term)
|
| 4047 |
+
for source_term, target_term in glossary_value.items()
|
| 4048 |
+
if source_term and str(source_term) in joined_source
|
| 4049 |
+
}
|
| 4050 |
payload = {
|
| 4051 |
'section': str(section_name or 'section'),
|
| 4052 |
'target_language': target_lang,
|
| 4053 |
'glossary': relevant_glossary,
|
| 4054 |
+
'allowed_issue_codes': sorted(allowed_codes),
|
| 4055 |
+
'items': [{'id': row['id'], 'english': row['english'], 'target': row['target']} for row in rows],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4056 |
}
|
| 4057 |
+
system_prompt = f"""You are a bilingual academic translation QA classifier for {target_lang}.
|
| 4058 |
+
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.
|
| 4059 |
+
Return valid JSON only in this exact form: {{"items":[{{"id":"SP-P000001-C000001","ok":true,"codes":[]}}]}}.
|
| 4060 |
+
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."""
|
| 4061 |
response = self.call_deepseek(
|
| 4062 |
+
system_prompt, json.dumps(payload, ensure_ascii=False),
|
| 4063 |
+
max_tokens=max(1200, min(3600, 650 + 260 * len(rows))),
|
|
|
|
| 4064 |
temp=0.0, model_role='reasoning', thinking=False, allow_fallback=False,
|
| 4065 |
)
|
| 4066 |
parsed = self._extract_json_from_text(response)
|
| 4067 |
+
returned = parsed.get('items', []) if isinstance(parsed, dict) else []
|
| 4068 |
+
by_id = {str(row.get('id') or ''): row for row in returned if isinstance(row, dict)}
|
| 4069 |
+
expected_row_ids = [row['id'] for row in rows]
|
| 4070 |
+
if set(by_id) != set(expected_row_ids):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4071 |
raise DeepSeekRequestError(
|
| 4072 |
+
f'Translation claim QA id mismatch: expected={expected_row_ids}, got={sorted(by_id)}.'
|
| 4073 |
)
|
| 4074 |
+
row_lookup = {row['id']: row for row in rows}
|
| 4075 |
+
for row_id in expected_row_ids:
|
| 4076 |
+
returned_row = by_id[row_id]
|
| 4077 |
+
if returned_row.get('ok') is True:
|
| 4078 |
continue
|
| 4079 |
+
raw_codes = [str(code or '').strip().upper() for code in (returned_row.get('codes') or [])]
|
| 4080 |
+
codes = [code for code in raw_codes if code in allowed_codes]
|
| 4081 |
+
if not codes:
|
| 4082 |
+
codes = ['SEMANTIC_DRIFT']
|
| 4083 |
+
meta = row_lookup[row_id]
|
| 4084 |
+
issues.setdefault(meta['block_id'], {})[meta['claim_id']] = list(dict.fromkeys(codes))[:5]
|
| 4085 |
return issues
|
| 4086 |
|
| 4087 |
@staticmethod
|
|
|
|
| 4221 |
return translated
|
| 4222 |
|
| 4223 |
def _translate_marked_batch_with_recovery(self, batch, system_prompt, target_lang, _strict=False):
|
| 4224 |
+
"""Translate SP-P blocks through claim-bound, value-blind protected skeletons."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4225 |
expected_ids = [block_id for block_id, _ in batch]
|
| 4226 |
protected_batch = []
|
| 4227 |
protected_maps = {}
|
| 4228 |
+
claim_sources_by_block = {}
|
| 4229 |
for block_id, source in batch:
|
| 4230 |
+
protected_source, token_map, claim_sources = self._protect_translation_claims(source)
|
| 4231 |
protected_batch.append((block_id, source, protected_source))
|
| 4232 |
protected_maps[block_id] = token_map
|
| 4233 |
+
claim_sources_by_block[block_id] = claim_sources
|
| 4234 |
marked_source = '\n\n'.join(
|
| 4235 |
f'[[SP-P{block_id}]]\n{protected_source}'
|
| 4236 |
for block_id, _, protected_source in protected_batch
|
|
|
|
| 4243 |
'\n<SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 4244 |
+ json.dumps(semantic_manifest, ensure_ascii=False)
|
| 4245 |
+ '\n</SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 4246 |
+
'This manifest intentionally contains no factual values. Preserve every SP-P, SP-C, and SP-T marker exactly. '
|
| 4247 |
+
'Each SP-C marker identifies one source claim. A target claim may contain one or more grammatical sentences, but every protected fact must remain inside its original SP-C claim. '
|
| 4248 |
+
'Never infer, spell out, calculate, or recreate a hidden number, date, citation, acronym, name, formula, or identifier. Do not print the manifest.'
|
| 4249 |
)
|
| 4250 |
if _strict:
|
| 4251 |
effective_prompt += (
|
| 4252 |
+
'\nSTRICT RETRY: Copy every SP-P, SP-C, and SP-T marker byte-for-byte. Do not merge, drop, translate, '
|
| 4253 |
+
'pluralize, or normalize any protected token. Do not move any SP-T outside its SP-C claim.'
|
| 4254 |
)
|
| 4255 |
result = self.call_deepseek(
|
| 4256 |
effective_prompt, marked_source,
|
| 4257 |
max_tokens=max(1800, min(9000, self._count_tokens(marked_source) * 2 + 1200)),
|
| 4258 |
temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
|
| 4259 |
)
|
|
|
|
|
|
|
|
|
|
| 4260 |
try:
|
| 4261 |
parsed = self._parse_marked_translation(result, expected_ids)
|
| 4262 |
except DeepSeekRequestError as error:
|
|
|
|
| 4299 |
failed = {}
|
| 4300 |
for block_id, source in batch:
|
| 4301 |
try:
|
| 4302 |
+
translated_with_claims = self._restore_translation_tokens(
|
| 4303 |
+
parsed[block_id], protected_maps.get(block_id, {}), f'SP-P{block_id}', target_lang,
|
| 4304 |
+
claim_sources=claim_sources_by_block.get(block_id, {}), keep_claim_markers=True,
|
|
|
|
|
|
|
| 4305 |
)
|
| 4306 |
+
clean_translation = self._strip_translation_claim_markers(translated_with_claims)
|
| 4307 |
+
self._validate_translation_block_protected(block_id, source, clean_translation, target_lang)
|
| 4308 |
+
restored[block_id] = translated_with_claims
|
| 4309 |
except DeepSeekRequestError as error:
|
| 4310 |
failed[block_id] = error
|
| 4311 |
|
| 4312 |
if failed:
|
| 4313 |
if _strict:
|
|
|
|
|
|
|
| 4314 |
first_id = next(iter(failed))
|
| 4315 |
raise failed[first_id]
|
| 4316 |
source_by_id = {block_id: source for block_id, source in batch}
|
|
|
|
| 4325 |
restored[block_id] = recovered[block_id]
|
| 4326 |
return restored
|
| 4327 |
|
| 4328 |
+
def _translate_single_claim_with_recovery(self, block_id, claim_id, source_claim, system_prompt, target_lang, issue_codes):
|
| 4329 |
+
"""Regenerate one failed claim using bounded QA codes only."""
|
| 4330 |
+
protected_claim, mapping = self._protect_translation_tokens(source_claim, claim_id=claim_id)
|
| 4331 |
+
marked_source = f'[[SP-P{block_id}]]\n[[SP-{claim_id}]]\n{protected_claim}'
|
| 4332 |
+
safe_manifest = self._model_safe_translation_manifest(mapping)
|
| 4333 |
+
codes = [str(code or '').strip().upper() for code in (issue_codes or []) if str(code or '').strip()]
|
| 4334 |
+
effective_prompt = system_prompt + (
|
| 4335 |
+
'\nCLAIM QUALITY RETRY. Repair only this one claim. The only permitted repair signals are the following bounded issue codes: '
|
| 4336 |
+
+ json.dumps(codes[:5], ensure_ascii=False)
|
| 4337 |
+
+ '. Do not infer any examples or numbered instructions from those codes. Preserve every SP-P, SP-C, and SP-T marker byte-for-byte. '
|
| 4338 |
+
'Do not add claims or factual content. Return only the marked translated claim.'
|
| 4339 |
+
+ '\n<SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 4340 |
+
+ json.dumps({block_id: safe_manifest}, ensure_ascii=False)
|
| 4341 |
+
+ '\n</SEMANTIC_ANCHOR_MANIFEST>'
|
| 4342 |
+
)
|
| 4343 |
+
result = self.call_deepseek(
|
| 4344 |
+
effective_prompt, marked_source,
|
| 4345 |
+
max_tokens=max(1200, min(2600, self._count_tokens(marked_source) * 2 + 900)),
|
| 4346 |
+
temp=0.0, model_role='translation', thinking=False, allow_fallback=False,
|
| 4347 |
+
)
|
| 4348 |
+
parsed = self._parse_marked_translation(result, [block_id])
|
| 4349 |
+
restored = self._restore_translation_tokens(
|
| 4350 |
+
parsed[block_id], mapping, f'SP-P{block_id}', target_lang,
|
| 4351 |
+
claim_sources={claim_id: source_claim}, keep_claim_markers=True,
|
| 4352 |
+
)
|
| 4353 |
+
clean = self._strip_translation_claim_markers(restored)
|
| 4354 |
+
self._validate_translation_block_protected(block_id, source_claim, clean, target_lang)
|
| 4355 |
+
_order, segments = self._parse_translation_claim_segments(restored, [claim_id])
|
| 4356 |
+
return segments[claim_id]
|
| 4357 |
+
|
| 4358 |
def _audit_translation_blocks_with_retry(
|
| 4359 |
self, sources, translations, target_lang, glossary=None, section_name='', max_attempts=2
|
| 4360 |
):
|
|
|
|
| 4425 |
local_round = 0
|
| 4426 |
while quality_issues and local_round < 2:
|
| 4427 |
local_round += 1
|
| 4428 |
+
failed_pairs = [
|
| 4429 |
+
(block_id, claim_id, codes)
|
| 4430 |
+
for block_id, claim_map in sorted(quality_issues.items())
|
| 4431 |
+
for claim_id, codes in sorted((claim_map or {}).items())
|
| 4432 |
+
]
|
| 4433 |
logging.warning(
|
| 4434 |
+
'Translation claim-local QA recovery: section=%s round=%s failed=%s',
|
| 4435 |
+
section_name or 'section', local_round,
|
| 4436 |
+
[f'SP-P{block_id}-{claim_id}' for block_id, claim_id, _codes in failed_pairs],
|
| 4437 |
)
|
| 4438 |
+
touched_blocks = set()
|
| 4439 |
+
for block_id, claim_id, codes in failed_pairs:
|
| 4440 |
+
source_claims = {
|
| 4441 |
+
item['claim_id']: item['text'] for item in self._translation_claim_units(source_blocks[block_id])
|
| 4442 |
+
}
|
| 4443 |
+
if claim_id not in source_claims:
|
| 4444 |
+
raise DeepSeekRequestError(
|
| 4445 |
+
f'Translation claim-local QA source mismatch: SP-P{block_id}-{claim_id}.'
|
| 4446 |
+
)
|
| 4447 |
+
claim_order = list(source_claims)
|
| 4448 |
+
_order, segments = self._parse_translation_claim_segments(translations[block_id], claim_order)
|
| 4449 |
+
segments[claim_id] = self._translate_single_claim_with_recovery(
|
| 4450 |
+
block_id, claim_id, source_claims[claim_id], system_prompt, target_lang, codes
|
| 4451 |
)
|
| 4452 |
+
translations[block_id] = self._compose_translation_claim_segments(claim_order, segments)
|
| 4453 |
+
touched_blocks.add(block_id)
|
| 4454 |
quality_issues = self._audit_translation_blocks_with_retry(
|
| 4455 |
+
{block_id: source_blocks[block_id] for block_id in sorted(touched_blocks)},
|
| 4456 |
+
{block_id: translations[block_id] for block_id in sorted(touched_blocks)},
|
| 4457 |
target_lang, frozen_glossary, section_name=section_name or 'frozen section', max_attempts=2,
|
| 4458 |
)
|
| 4459 |
if quality_issues:
|
| 4460 |
detail = '; '.join(
|
| 4461 |
+
f'SP-P{block_id}-{claim_id}: {",".join(codes)}'
|
| 4462 |
+
for block_id, claim_map in sorted(quality_issues.items())
|
| 4463 |
+
for claim_id, codes in sorted((claim_map or {}).items())
|
| 4464 |
)
|
| 4465 |
raise DeepSeekRequestError(
|
| 4466 |
+
f'Translation claim-local QA failed for {section_name or "section"}: {detail[:1000]}'
|
| 4467 |
)
|
| 4468 |
|
| 4469 |
# Rebuild after any localized repairs, then apply deterministic
|
|
|
|
| 4478 |
output_units.append('#' * int(unit['level']) + ' ' + translated_heading)
|
| 4479 |
else:
|
| 4480 |
body_index += 1
|
| 4481 |
+
output_units.append(self._strip_translation_claim_markers(translations[f'{body_index:06d}']))
|
| 4482 |
output = '\n\n'.join(output_units).strip()
|
| 4483 |
self.validate_translation_alignment(value, output)
|
| 4484 |
self._validate_translation_language_quality(output, target_lang)
|
|
|
|
| 4496 |
last_error = error
|
| 4497 |
message = str(error)
|
| 4498 |
if (
|
| 4499 |
+
message.startswith('Translation claim-local QA failed')
|
| 4500 |
or message.startswith('Translation alignment')
|
| 4501 |
or message.startswith('Translation changed ')
|
| 4502 |
or message.startswith('Translation protected ')
|
|
|
|
| 4563 |
output_units.append('#' * int(unit['level']) + ' ' + translated_heading)
|
| 4564 |
else:
|
| 4565 |
body_index += 1
|
| 4566 |
+
output_units.append(self._strip_translation_claim_markers(translations[f'{body_index:06d}']))
|
| 4567 |
|
| 4568 |
output = '\n\n'.join(output_units).strip()
|
| 4569 |
if references:
|
smartpaper_restore_bridge/__pycache__/__init__.cpython-313.pyc
DELETED
|
Binary file (3.12 kB)
|
|
|
smartpaper_restore_bridge/__pycache__/desktop.ini
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
[.ShellClassInfo]
|
| 2 |
-
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
|
|
|
|
|
|
|
|
smartpaper_restore_bridge/frontend/index.html
CHANGED
|
@@ -19,6 +19,8 @@
|
|
| 19 |
const MAX_TOKEN_LENGTH = 4096;
|
| 20 |
const MAX_RESULT_AGE_MS = 10 * 60 * 1000;
|
| 21 |
const REQUEST_TIMEOUT_MS = 20000;
|
|
|
|
|
|
|
| 22 |
const TERMINAL_TASK_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
| 23 |
let lastResponseSignature = "";
|
| 24 |
let activeTaskWatch = null;
|
|
@@ -32,6 +34,86 @@
|
|
| 32 |
target:{ authoritative:"", displayed:"", queue:[], timer:null }
|
| 33 |
};
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
const ACTION_PATHS = Object.freeze({
|
| 36 |
task_start: "/edge/task/start",
|
| 37 |
task_cancel: "/edge/task/cancel",
|
|
@@ -274,15 +356,63 @@
|
|
| 274 |
target.textContent = value;
|
| 275 |
}
|
| 276 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
function normalizePreviewDisplay(text) {
|
| 278 |
-
return
|
| 279 |
-
.replace(/\r/g, "")
|
| 280 |
-
.replace(/^#{1,3}\s+/gm, "");
|
| 281 |
}
|
| 282 |
|
| 283 |
function wordLikeUnits(value) {
|
| 284 |
-
const
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
function ensureStreamNode(parentDocument, target, channel) {
|
|
@@ -311,7 +441,7 @@
|
|
| 311 |
if (!channel.queue.length) return;
|
| 312 |
const stream = ensureStreamNode(parentDocument, target, channel);
|
| 313 |
const backlog = channel.queue.length;
|
| 314 |
-
const perTick = backlog >
|
| 315 |
let chunk = "";
|
| 316 |
for (let i=0; i<perTick && channel.queue.length; i++) chunk += channel.queue.shift();
|
| 317 |
if (chunk) {
|
|
@@ -319,7 +449,7 @@
|
|
| 319 |
channel.displayed += chunk;
|
| 320 |
if (shell && shell.dataset.spAutoFollow !== "0") shell.scrollTop = shell.scrollHeight;
|
| 321 |
}
|
| 322 |
-
channel.timer = window.setTimeout(() => pumpRenderQueue(parentDocument, target, shell, channel),
|
| 323 |
}
|
| 324 |
|
| 325 |
function renderBufferedPreview(parentDocument, target, shell, text, channel) {
|
|
@@ -528,6 +658,7 @@
|
|
| 528 |
const bestEffortPageExitCancel = payload.best_effort_page_exit_cancel === true;
|
| 529 |
const watch = { commandId, taskId, apiOrigin, intervalMs, previewIntervalMs, previewUrl:resolvedPreviewUrl, expectedEpoch, draftStatusLabels, targetStatusLabels, bestEffortPageExitCancel, timer:null, previewTimer:null, controller:null, stopped:false, lastCheckpointSeq:initialCheckpointSeq };
|
| 530 |
activeTaskWatch = watch;
|
|
|
|
| 531 |
installPageExitCancel(watch);
|
| 532 |
lastPreviewText = "";
|
| 533 |
lastTargetPreviewText = "";
|
|
@@ -562,12 +693,14 @@
|
|
| 562 |
watch.stopped = true;
|
| 563 |
activeTaskWatch = null;
|
| 564 |
removePageExitCancel();
|
|
|
|
| 565 |
sendValue(Object.assign({}, result, {
|
| 566 |
action:"task_watch",
|
| 567 |
command_id:commandId,
|
| 568 |
terminal:TERMINAL_TASK_STATUSES.has(status),
|
| 569 |
boundary:true,
|
| 570 |
-
boundary_type:"terminal"
|
|
|
|
| 571 |
}));
|
| 572 |
return;
|
| 573 |
}
|
|
@@ -624,6 +757,7 @@
|
|
| 624 |
sendValue(response);
|
| 625 |
}
|
| 626 |
|
|
|
|
| 627 |
window.addEventListener("message", onRender);
|
| 628 |
sendMessage("streamlit:componentReady", { apiVersion:1 });
|
| 629 |
setFrameHeight();
|
|
|
|
| 19 |
const MAX_TOKEN_LENGTH = 4096;
|
| 20 |
const MAX_RESULT_AGE_MS = 10 * 60 * 1000;
|
| 21 |
const REQUEST_TIMEOUT_MS = 20000;
|
| 22 |
+
const NAV_DIAG_KEY = "smartpaper.ui.navdiag.v1";
|
| 23 |
+
const NAV_DIAG_MAX_EVENTS = 24;
|
| 24 |
const TERMINAL_TASK_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
| 25 |
let lastResponseSignature = "";
|
| 26 |
let activeTaskWatch = null;
|
|
|
|
| 34 |
target:{ authoritative:"", displayed:"", queue:[], timer:null }
|
| 35 |
};
|
| 36 |
|
| 37 |
+
|
| 38 |
+
function readSessionJson(key, fallback=null) {
|
| 39 |
+
try {
|
| 40 |
+
const raw = window.sessionStorage.getItem(key);
|
| 41 |
+
return raw ? JSON.parse(raw) : fallback;
|
| 42 |
+
} catch (_) { return fallback; }
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function writeSessionJson(key, value) {
|
| 46 |
+
try { window.sessionStorage.setItem(key, JSON.stringify(value)); return true; }
|
| 47 |
+
catch (_) { return false; }
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function navigationSnapshot() {
|
| 51 |
+
let timeOrigin = 0;
|
| 52 |
+
let navigationType = "unknown";
|
| 53 |
+
try {
|
| 54 |
+
const perf = window.parent && window.parent.performance ? window.parent.performance : window.performance;
|
| 55 |
+
timeOrigin = Number(perf.timeOrigin || 0);
|
| 56 |
+
const entries = perf.getEntriesByType ? perf.getEntriesByType("navigation") : [];
|
| 57 |
+
if (entries && entries[0] && entries[0].type) navigationType = String(entries[0].type);
|
| 58 |
+
} catch (_) {
|
| 59 |
+
try {
|
| 60 |
+
timeOrigin = Number(window.performance.timeOrigin || 0);
|
| 61 |
+
const entries = window.performance.getEntriesByType("navigation");
|
| 62 |
+
if (entries && entries[0] && entries[0].type) navigationType = String(entries[0].type);
|
| 63 |
+
} catch (_) {}
|
| 64 |
+
}
|
| 65 |
+
return { time_origin:timeOrigin, navigation_type:navigationType };
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function recordNavDiagnostic(kind, detail={}) {
|
| 69 |
+
const current = readSessionJson(NAV_DIAG_KEY, { version:1, mount_count:0, events:[] }) || { version:1, mount_count:0, events:[] };
|
| 70 |
+
if (!Array.isArray(current.events)) current.events = [];
|
| 71 |
+
const snapshot = navigationSnapshot();
|
| 72 |
+
current.last_time_origin = snapshot.time_origin;
|
| 73 |
+
current.navigation_type = snapshot.navigation_type;
|
| 74 |
+
current.events.push(Object.assign({
|
| 75 |
+
t:Date.now(), kind:String(kind || "event"), time_origin:snapshot.time_origin,
|
| 76 |
+
navigation_type:snapshot.navigation_type
|
| 77 |
+
}, detail && typeof detail === "object" ? detail : {}));
|
| 78 |
+
if (current.events.length > NAV_DIAG_MAX_EVENTS) current.events = current.events.slice(-NAV_DIAG_MAX_EVENTS);
|
| 79 |
+
writeSessionJson(NAV_DIAG_KEY, current);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
function mountNavigationDiagnostics() {
|
| 83 |
+
const current = readSessionJson(NAV_DIAG_KEY, { version:1, mount_count:0, events:[] }) || { version:1, mount_count:0, events:[] };
|
| 84 |
+
current.mount_count = Number(current.mount_count || 0) + 1;
|
| 85 |
+
if (!Array.isArray(current.events)) current.events = [];
|
| 86 |
+
writeSessionJson(NAV_DIAG_KEY, current);
|
| 87 |
+
recordNavDiagnostic("bridge_mount", { mount_count:current.mount_count });
|
| 88 |
+
const add = (target, name, handler) => { try { target && target.addEventListener && target.addEventListener(name, handler); } catch (_) {} };
|
| 89 |
+
add(window, "pageshow", event => recordNavDiagnostic("pageshow", { persisted:!!event.persisted }));
|
| 90 |
+
add(window, "pagehide", event => recordNavDiagnostic("pagehide", { persisted:!!event.persisted }));
|
| 91 |
+
add(window, "online", () => recordNavDiagnostic("online"));
|
| 92 |
+
add(window, "offline", () => recordNavDiagnostic("offline"));
|
| 93 |
+
add(document, "visibilitychange", () => recordNavDiagnostic("visibilitychange", { state:String(document.visibilityState || "") }));
|
| 94 |
+
try {
|
| 95 |
+
const pw = window.parent;
|
| 96 |
+
add(pw, "pageshow", event => recordNavDiagnostic("parent_pageshow", { persisted:!!event.persisted }));
|
| 97 |
+
add(pw, "pagehide", event => recordNavDiagnostic("parent_pagehide", { persisted:!!event.persisted }));
|
| 98 |
+
add(pw, "online", () => recordNavDiagnostic("parent_online"));
|
| 99 |
+
add(pw, "offline", () => recordNavDiagnostic("parent_offline"));
|
| 100 |
+
if (pw.navigation && pw.navigation.addEventListener) {
|
| 101 |
+
add(pw.navigation, "navigate", event => recordNavDiagnostic("navigation", { navigation_type:String(event.navigationType || "") }));
|
| 102 |
+
}
|
| 103 |
+
} catch (_) {}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function navDiagnosticSnapshot() {
|
| 107 |
+
const current = readSessionJson(NAV_DIAG_KEY, { version:1, mount_count:0, events:[] }) || { version:1, mount_count:0, events:[] };
|
| 108 |
+
return {
|
| 109 |
+
version:1,
|
| 110 |
+
mount_count:Number(current.mount_count || 0),
|
| 111 |
+
last_time_origin:Number(current.last_time_origin || 0),
|
| 112 |
+
navigation_type:String(current.navigation_type || "unknown"),
|
| 113 |
+
events:Array.isArray(current.events) ? current.events.slice(-NAV_DIAG_MAX_EVENTS) : []
|
| 114 |
+
};
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
const ACTION_PATHS = Object.freeze({
|
| 118 |
task_start: "/edge/task/start",
|
| 119 |
task_cancel: "/edge/task/cancel",
|
|
|
|
| 356 |
target.textContent = value;
|
| 357 |
}
|
| 358 |
|
| 359 |
+
const UNNUMBERED_HEADINGS = new Set([
|
| 360 |
+
"abstract","keywords","references","摘要","关键词","参考文献",
|
| 361 |
+
"аннотация","ключевые слова","литература","список литературы",
|
| 362 |
+
"аңдатпа","аннотация","түйінді сөздер","кілт сөздер","әдебиеттер","пайдаланылған әдебиеттер"
|
| 363 |
+
]);
|
| 364 |
+
|
| 365 |
+
function cleanHeadingTitle(value) {
|
| 366 |
+
return String(value || "").trim().replace(/^\d+(?:\.\d+){0,2}\s+/, "");
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
function numberMarkdownHeadings(value) {
|
| 370 |
+
const text = String(value || "").replace(/\r/g, "");
|
| 371 |
+
const lines = text.split("\n");
|
| 372 |
+
const counters = [0,0,0];
|
| 373 |
+
const headingRows = lines.map((line, index) => {
|
| 374 |
+
const match = line.match(/^(#{1,3})\s+(.+?)\s*$/);
|
| 375 |
+
return match ? { index, level:match[1].length, title:cleanHeadingTitle(match[2]) } : null;
|
| 376 |
+
}).filter(Boolean);
|
| 377 |
+
const hasFrontMatter = headingRows.some(row => UNNUMBERED_HEADINGS.has(row.title.toLocaleLowerCase()));
|
| 378 |
+
const firstH1 = headingRows.find(row => row.level === 1);
|
| 379 |
+
for (const row of headingRows) {
|
| 380 |
+
const key = row.title.toLocaleLowerCase();
|
| 381 |
+
const isKnownUnnumbered = UNNUMBERED_HEADINGS.has(key);
|
| 382 |
+
const isTitle = hasFrontMatter && firstH1 && row.index === firstH1.index && !isKnownUnnumbered;
|
| 383 |
+
if (isKnownUnnumbered || isTitle) {
|
| 384 |
+
lines[row.index] = row.title;
|
| 385 |
+
continue;
|
| 386 |
+
}
|
| 387 |
+
if (row.level === 1) {
|
| 388 |
+
counters[0] += 1; counters[1] = 0; counters[2] = 0;
|
| 389 |
+
lines[row.index] = `${counters[0]} ${row.title}`;
|
| 390 |
+
} else if (row.level === 2) {
|
| 391 |
+
if (counters[0] === 0) counters[0] = 1;
|
| 392 |
+
counters[1] += 1; counters[2] = 0;
|
| 393 |
+
lines[row.index] = `${counters[0]}.${counters[1]} ${row.title}`;
|
| 394 |
+
} else {
|
| 395 |
+
if (counters[0] === 0) counters[0] = 1;
|
| 396 |
+
if (counters[1] === 0) counters[1] = 1;
|
| 397 |
+
counters[2] += 1;
|
| 398 |
+
lines[row.index] = `${counters[0]}.${counters[1]}.${counters[2]} ${row.title}`;
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
return lines.join("\n");
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
function normalizePreviewDisplay(text) {
|
| 405 |
+
return numberMarkdownHeadings(text);
|
|
|
|
|
|
|
| 406 |
}
|
| 407 |
|
| 408 |
function wordLikeUnits(value) {
|
| 409 |
+
const raw = String(value || "").match(/[\u4e00-\u9fff]{1,2}|[^\s\u4e00-\u9fff]+|\s+/g) || [];
|
| 410 |
+
const units = [];
|
| 411 |
+
for (const token of raw) {
|
| 412 |
+
if (/^\s+$/.test(token) && units.length) units[units.length - 1] += token;
|
| 413 |
+
else units.push(token);
|
| 414 |
+
}
|
| 415 |
+
return units;
|
| 416 |
}
|
| 417 |
|
| 418 |
function ensureStreamNode(parentDocument, target, channel) {
|
|
|
|
| 441 |
if (!channel.queue.length) return;
|
| 442 |
const stream = ensureStreamNode(parentDocument, target, channel);
|
| 443 |
const backlog = channel.queue.length;
|
| 444 |
+
const perTick = backlog > 500 ? 2 : 1;
|
| 445 |
let chunk = "";
|
| 446 |
for (let i=0; i<perTick && channel.queue.length; i++) chunk += channel.queue.shift();
|
| 447 |
if (chunk) {
|
|
|
|
| 449 |
channel.displayed += chunk;
|
| 450 |
if (shell && shell.dataset.spAutoFollow !== "0") shell.scrollTop = shell.scrollHeight;
|
| 451 |
}
|
| 452 |
+
channel.timer = window.setTimeout(() => pumpRenderQueue(parentDocument, target, shell, channel), 100);
|
| 453 |
}
|
| 454 |
|
| 455 |
function renderBufferedPreview(parentDocument, target, shell, text, channel) {
|
|
|
|
| 658 |
const bestEffortPageExitCancel = payload.best_effort_page_exit_cancel === true;
|
| 659 |
const watch = { commandId, taskId, apiOrigin, intervalMs, previewIntervalMs, previewUrl:resolvedPreviewUrl, expectedEpoch, draftStatusLabels, targetStatusLabels, bestEffortPageExitCancel, timer:null, previewTimer:null, controller:null, stopped:false, lastCheckpointSeq:initialCheckpointSeq };
|
| 660 |
activeTaskWatch = watch;
|
| 661 |
+
recordNavDiagnostic("task_watch_start");
|
| 662 |
installPageExitCancel(watch);
|
| 663 |
lastPreviewText = "";
|
| 664 |
lastTargetPreviewText = "";
|
|
|
|
| 693 |
watch.stopped = true;
|
| 694 |
activeTaskWatch = null;
|
| 695 |
removePageExitCancel();
|
| 696 |
+
recordNavDiagnostic("task_watch_boundary", { status });
|
| 697 |
sendValue(Object.assign({}, result, {
|
| 698 |
action:"task_watch",
|
| 699 |
command_id:commandId,
|
| 700 |
terminal:TERMINAL_TASK_STATUSES.has(status),
|
| 701 |
boundary:true,
|
| 702 |
+
boundary_type:"terminal",
|
| 703 |
+
ui_navdiag:navDiagnosticSnapshot()
|
| 704 |
}));
|
| 705 |
return;
|
| 706 |
}
|
|
|
|
| 757 |
sendValue(response);
|
| 758 |
}
|
| 759 |
|
| 760 |
+
mountNavigationDiagnostics();
|
| 761 |
window.addEventListener("message", onRender);
|
| 762 |
sendMessage("streamlit:componentReady", { apiVersion:1 });
|
| 763 |
setFrameHeight();
|
utils/__pycache__/desktop.ini
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
[.ShellClassInfo]
|
| 2 |
-
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
|
|
|
|
|
|
|
|
utils/__pycache__/docx_builder.cpython-313.pyc
DELETED
|
Binary file (9.38 kB)
|
|
|
utils/__pycache__/file_parser.cpython-313.pyc
DELETED
|
Binary file (8.01 kB)
|
|
|