Spaces:
Running
Running
Step 4A-6-C-R3.2.8.3 Translation Semantic Anchor Working Final Abstract Fix
#21
by GuXiaopeng - opened
- .gitattributes +1 -0
- __pycache__/app.cpython-313.pyc +0 -0
- __pycache__/config.cpython-313.pyc +0 -0
- __pycache__/desktop.ini +2 -0
- __pycache__/i18n.cpython-313.pyc +0 -0
- __pycache__/managed_auth.cpython-313.pyc +0 -0
- __pycache__/task_executor.cpython-313.pyc +0 -0
- core/__pycache__/checkpoint_store.cpython-313.pyc +0 -0
- core/__pycache__/desktop.ini +2 -0
- 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 +3 -0
- 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 +139 -10
- core/pipeline_agent.py +317 -25
- smartpaper_restore_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- smartpaper_restore_bridge/__pycache__/desktop.ini +2 -0
- utils/__pycache__/desktop.ini +2 -0
- utils/__pycache__/docx_builder.cpython-313.pyc +0 -0
- utils/__pycache__/file_parser.cpython-313.pyc +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
core/__pycache__/pipeline_agent.cpython-313.pyc filter=lfs diff=lfs merge=lfs -text
|
__pycache__/app.cpython-313.pyc
ADDED
|
Binary file (60.6 kB). View file
|
|
|
__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (31.4 kB). View file
|
|
|
__pycache__/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
__pycache__/i18n.cpython-313.pyc
ADDED
|
Binary file (76.6 kB). View file
|
|
|
__pycache__/managed_auth.cpython-313.pyc
ADDED
|
Binary file (31 kB). View file
|
|
|
__pycache__/task_executor.cpython-313.pyc
ADDED
|
Binary file (33.6 kB). View file
|
|
|
core/__pycache__/checkpoint_store.cpython-313.pyc
ADDED
|
Binary file (4.62 kB). View file
|
|
|
core/__pycache__/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
Binary file (65 kB). View file
|
|
|
core/__pycache__/meta_extractor.cpython-313.pyc
ADDED
|
Binary file (3.42 kB). View file
|
|
|
core/__pycache__/pipeline_agent.cpython-313.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a58cec79f28771e880158bd36b2afd8c77ac1982a61db28ff832a962c41ed79c
|
| 3 |
+
size 251613
|
core/__pycache__/production_guard.cpython-313.pyc
ADDED
|
Binary file (11 kB). View file
|
|
|
core/__pycache__/qualitative_engine.cpython-313.pyc
ADDED
|
Binary file (15.3 kB). View file
|
|
|
core/__pycache__/quantitative_engine.cpython-313.pyc
ADDED
|
Binary file (24.4 kB). View file
|
|
|
core/__pycache__/rag_engine.cpython-313.pyc
ADDED
|
Binary file (4.36 kB). View file
|
|
|
core/generation_runner.py
CHANGED
|
@@ -224,6 +224,20 @@ class GenerationRunner:
|
|
| 224 |
match = re.search(rf'(?m)^#\s+{re.escape(str(first_section_title))}\s*$', value)
|
| 225 |
return value[:match.start()].strip() if match else ''
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
@staticmethod
|
| 228 |
def _section_is_complete(tasks, completed_display_names, section_key: str) -> bool:
|
| 229 |
relevant = [
|
|
@@ -463,13 +477,29 @@ class GenerationRunner:
|
|
| 463 |
raise RuntimeError("research_logic_contract_missing")
|
| 464 |
agent.global_memory["research_logic_contract"] = research_logic_contract
|
| 465 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
self._progress(14, "outline_planning")
|
| 467 |
plan = agent.generate_outline(topic, keywords, mode=mode, data_metadata=data_metadata)
|
| 468 |
en_topic = str(plan.get("en_topic", topic))
|
| 469 |
structure = plan.get("structure", [])
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
front_budget = max(180, int(APP_CONFIG.get("word_budget", {}).get("front_matter_words", 220)))
|
| 474 |
body_target_words = max(1000, target_words - front_budget)
|
| 475 |
adaptive_structure = agent.build_generation_tasks(structure, body_target_words, safe_chunk_words)
|
|
@@ -498,6 +528,9 @@ class GenerationRunner:
|
|
| 498 |
"adaptive_structure": adaptive_structure,
|
| 499 |
"citation_plan": citation_plan,
|
| 500 |
"research_logic_contract": research_logic_contract,
|
|
|
|
|
|
|
|
|
|
| 501 |
"body_target_words": body_target_words,
|
| 502 |
"full_draft_en": full_draft_en,
|
| 503 |
"full_draft_translated": "",
|
|
@@ -526,8 +559,62 @@ class GenerationRunner:
|
|
| 526 |
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
|
| 527 |
translation_sections = dict(state.get('translation_sections') or {})
|
| 528 |
translation_glossary = dict(state.get('translation_glossary') or {})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
if self._preview_target_text == '':
|
| 530 |
self._preview_target_text = str(state.get('full_draft_translated') or '')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
if not citation_plan:
|
| 532 |
self._progress(15, "citation_planning")
|
| 533 |
citation_plan = agent.build_claim_evidence_plan(
|
|
@@ -653,6 +740,10 @@ Target length: {int(adjusted_target * min_ratio)}-{int(adjusted_target * max_rat
|
|
| 653 |
<PROVISIONAL_RESEARCH_LOGIC>
|
| 654 |
{research_logic_contract}
|
| 655 |
</PROVISIONAL_RESEARCH_LOGIC>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
{memory_block}
|
| 657 |
Literature synthesis: {synthesis_result}
|
| 658 |
<FROZEN_RESEARCH_EVIDENCE>
|
|
@@ -751,9 +842,9 @@ Output only final academic prose. Do not report the word count."""
|
|
| 751 |
agent, unit_key=section_key, unit_name=sec_title, source_text=frozen_section,
|
| 752 |
target_lang=target_lang, translation_sections=translation_sections,
|
| 753 |
translation_glossary=translation_glossary, adaptive_structure=adaptive_structure,
|
| 754 |
-
include_front=
|
| 755 |
)
|
| 756 |
-
partial_translation = self._assemble_partial_translation(translation_sections, adaptive_structure)
|
| 757 |
state.update(
|
| 758 |
{
|
| 759 |
"completed_sections": completed_sections,
|
|
@@ -772,10 +863,9 @@ Output only final academic prose. Do not report the word count."""
|
|
| 772 |
if not state.get("english_finalized"):
|
| 773 |
self._progress(91, "english_validation")
|
| 774 |
full_draft_en = _clean_trailing_references(full_draft_en, ["Reference", "References", "Reference List"])
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 779 |
agent.validate_manuscript_structure(full_draft_en, adaptive_structure, translated=False)
|
| 780 |
if mode == "asq":
|
| 781 |
repeated_quotes = agent.find_repeated_qualitative_quotes(full_draft_en, data_metadata)
|
|
@@ -807,15 +897,54 @@ Output only final academic prose. Do not report the word count."""
|
|
| 807 |
)
|
| 808 |
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 809 |
agent.enforce_resolved_claim_specific_citations(body_only)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
self._validate_target_length(full_draft_en, target_words)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
self._publish_preview(full_draft_en, "english_complete", force=True, complete=True)
|
| 812 |
state.update(
|
| 813 |
{
|
| 814 |
"full_draft_en": full_draft_en,
|
| 815 |
-
"full_draft_translated":
|
| 816 |
"translation_sections": translation_sections,
|
| 817 |
"translation_glossary": translation_glossary,
|
|
|
|
|
|
|
|
|
|
| 818 |
"english_finalized": True,
|
|
|
|
| 819 |
"agent_state": agent.export_state(),
|
| 820 |
}
|
| 821 |
)
|
|
|
|
| 224 |
match = re.search(rf'(?m)^#\s+{re.escape(str(first_section_title))}\s*$', value)
|
| 225 |
return value[:match.start()].strip() if match else ''
|
| 226 |
|
| 227 |
+
@staticmethod
|
| 228 |
+
def _replace_keywords(manuscript: str, keywords) -> str:
|
| 229 |
+
value = str(manuscript or '')
|
| 230 |
+
if isinstance(keywords, (list, tuple)):
|
| 231 |
+
rendered = ', '.join(str(item).strip() for item in keywords if str(item).strip())
|
| 232 |
+
else:
|
| 233 |
+
rendered = str(keywords or '').strip()
|
| 234 |
+
pattern = re.compile(r'(?ms)(^##\s+Keywords\s*$\n)(.*?)(?=^#\s+[^#].*?$|\Z)')
|
| 235 |
+
matches = list(pattern.finditer(value))
|
| 236 |
+
if len(matches) != 1:
|
| 237 |
+
raise RuntimeError(f'keywords_heading_count_invalid:{len(matches)}')
|
| 238 |
+
match = matches[0]
|
| 239 |
+
return value[:match.start()] + match.group(1) + rendered + '\n\n' + value[match.end():].lstrip('\n')
|
| 240 |
+
|
| 241 |
@staticmethod
|
| 242 |
def _section_is_complete(tasks, completed_display_names, section_key: str) -> bool:
|
| 243 |
relevant = [
|
|
|
|
| 477 |
raise RuntimeError("research_logic_contract_missing")
|
| 478 |
agent.global_memory["research_logic_contract"] = research_logic_contract
|
| 479 |
|
| 480 |
+
self._progress(14, "working_front_matter")
|
| 481 |
+
working_front = agent.generate_working_front_matter(
|
| 482 |
+
topic, keywords, research_logic_contract, synthesis_result,
|
| 483 |
+
data_evidence=user_data_md, mode=mode,
|
| 484 |
+
)
|
| 485 |
+
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 486 |
+
working_keywords = list(working_front.get("keywords") or [])
|
| 487 |
+
if not working_abstract or not working_keywords:
|
| 488 |
+
raise RuntimeError("working_front_matter_missing")
|
| 489 |
+
agent.global_memory["working_abstract"] = working_abstract
|
| 490 |
+
|
| 491 |
self._progress(14, "outline_planning")
|
| 492 |
plan = agent.generate_outline(topic, keywords, mode=mode, data_metadata=data_metadata)
|
| 493 |
en_topic = str(plan.get("en_topic", topic))
|
| 494 |
structure = plan.get("structure", [])
|
| 495 |
+
full_draft_en = (
|
| 496 |
+
f"# {en_topic}\n\n## Abstract\n{working_abstract}\n\n"
|
| 497 |
+
f"## Keywords\n{', '.join(working_keywords)}\n\n"
|
| 498 |
+
)
|
| 499 |
+
# Publish the Working Abstract/Keywords immediately, before JIT and
|
| 500 |
+
# citation planning, so the user sees the manuscript anchor before
|
| 501 |
+
# Introduction while those longer planning stages continue.
|
| 502 |
+
self._publish_preview(full_draft_en, "working_front_matter", force=True)
|
| 503 |
front_budget = max(180, int(APP_CONFIG.get("word_budget", {}).get("front_matter_words", 220)))
|
| 504 |
body_target_words = max(1000, target_words - front_budget)
|
| 505 |
adaptive_structure = agent.build_generation_tasks(structure, body_target_words, safe_chunk_words)
|
|
|
|
| 528 |
"adaptive_structure": adaptive_structure,
|
| 529 |
"citation_plan": citation_plan,
|
| 530 |
"research_logic_contract": research_logic_contract,
|
| 531 |
+
"working_abstract": working_abstract,
|
| 532 |
+
"working_keywords": working_keywords,
|
| 533 |
+
"front_matter_phase": "working",
|
| 534 |
"body_target_words": body_target_words,
|
| 535 |
"full_draft_en": full_draft_en,
|
| 536 |
"full_draft_translated": "",
|
|
|
|
| 559 |
citation_plan = state.get("citation_plan") if isinstance(state.get("citation_plan"), dict) else {}
|
| 560 |
translation_sections = dict(state.get('translation_sections') or {})
|
| 561 |
translation_glossary = dict(state.get('translation_glossary') or {})
|
| 562 |
+
working_abstract = str(state.get("working_abstract") or "").strip()
|
| 563 |
+
working_keywords = list(state.get("working_keywords") or [])
|
| 564 |
+
if working_abstract:
|
| 565 |
+
agent.global_memory["working_abstract"] = working_abstract
|
| 566 |
if self._preview_target_text == '':
|
| 567 |
self._preview_target_text = str(state.get('full_draft_translated') or '')
|
| 568 |
+
|
| 569 |
+
# Backward-compatible resume path for a pre-R3.2.8.3 checkpoint that has
|
| 570 |
+
# an outline but no Working Abstract yet. Generate it once, checkpoint it,
|
| 571 |
+
# and reuse it thereafter; never regenerate an unchanged anchor on resume.
|
| 572 |
+
if not working_abstract and not state.get("english_finalized"):
|
| 573 |
+
working_front = agent.generate_working_front_matter(
|
| 574 |
+
topic, keywords, research_logic_contract, synthesis_result,
|
| 575 |
+
data_evidence=user_data_md, mode=mode,
|
| 576 |
+
)
|
| 577 |
+
working_abstract = str(working_front.get("abstract") or "").strip()
|
| 578 |
+
working_keywords = list(working_front.get("keywords") or [])
|
| 579 |
+
if not working_abstract or not working_keywords:
|
| 580 |
+
raise RuntimeError("working_front_matter_missing")
|
| 581 |
+
agent.global_memory["working_abstract"] = working_abstract
|
| 582 |
+
full_draft_en = replace_abstract(full_draft_en, working_abstract)
|
| 583 |
+
full_draft_en = self._replace_keywords(full_draft_en, working_keywords)
|
| 584 |
+
state.update({
|
| 585 |
+
"working_abstract": working_abstract,
|
| 586 |
+
"working_keywords": working_keywords,
|
| 587 |
+
"front_matter_phase": "working",
|
| 588 |
+
"full_draft_en": full_draft_en,
|
| 589 |
+
"agent_state": agent.export_state(),
|
| 590 |
+
})
|
| 591 |
+
self._publish_preview(full_draft_en, "working_front_matter", force=True)
|
| 592 |
+
self._checkpoint("working_front_matter_ready", state, 15)
|
| 593 |
+
|
| 594 |
+
# Translate the QA-frozen Working front matter before Introduction. The
|
| 595 |
+
# same __front_matter__ record is hash-reused on resume and invalidated
|
| 596 |
+
# later when Final Abstract/Keywords replace the working front matter.
|
| 597 |
+
if working_abstract and not state.get("english_finalized"):
|
| 598 |
+
section_order_for_front = self._unique_top_level_sections(adaptive_structure)
|
| 599 |
+
if section_order_for_front:
|
| 600 |
+
working_front_source = self._extract_front_matter(full_draft_en, section_order_for_front[0][1])
|
| 601 |
+
translation_sections, translation_glossary = self._translate_frozen_unit(
|
| 602 |
+
agent, unit_key='__front_matter__', unit_name='Working Front Matter',
|
| 603 |
+
source_text=working_front_source, target_lang=target_lang,
|
| 604 |
+
translation_sections=translation_sections, translation_glossary=translation_glossary,
|
| 605 |
+
adaptive_structure=adaptive_structure, include_front=True,
|
| 606 |
+
)
|
| 607 |
+
working_target = self._assemble_partial_translation(
|
| 608 |
+
translation_sections, adaptive_structure, include_front=True
|
| 609 |
+
)
|
| 610 |
+
state.update({
|
| 611 |
+
"full_draft_en": full_draft_en,
|
| 612 |
+
"full_draft_translated": working_target,
|
| 613 |
+
"translation_sections": translation_sections,
|
| 614 |
+
"translation_glossary": translation_glossary,
|
| 615 |
+
"agent_state": agent.export_state(),
|
| 616 |
+
})
|
| 617 |
+
self._checkpoint("working_front_matter_translated", state, 16)
|
| 618 |
if not citation_plan:
|
| 619 |
self._progress(15, "citation_planning")
|
| 620 |
citation_plan = agent.build_claim_evidence_plan(
|
|
|
|
| 740 |
<PROVISIONAL_RESEARCH_LOGIC>
|
| 741 |
{research_logic_contract}
|
| 742 |
</PROVISIONAL_RESEARCH_LOGIC>
|
| 743 |
+
<WORKING_ABSTRACT_ANCHOR>
|
| 744 |
+
{working_abstract}
|
| 745 |
+
</WORKING_ABSTRACT_ANCHOR>
|
| 746 |
+
ANCHOR PRECEDENCE: Frozen research evidence and the PROVISIONAL_RESEARCH_LOGIC override the Working Abstract if any tension appears. The Working Abstract is continuity guidance only and must never force a result.
|
| 747 |
{memory_block}
|
| 748 |
Literature synthesis: {synthesis_result}
|
| 749 |
<FROZEN_RESEARCH_EVIDENCE>
|
|
|
|
| 842 |
agent, unit_key=section_key, unit_name=sec_title, source_text=frozen_section,
|
| 843 |
target_lang=target_lang, translation_sections=translation_sections,
|
| 844 |
translation_glossary=translation_glossary, adaptive_structure=adaptive_structure,
|
| 845 |
+
include_front=True,
|
| 846 |
)
|
| 847 |
+
partial_translation = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=True)
|
| 848 |
state.update(
|
| 849 |
{
|
| 850 |
"completed_sections": completed_sections,
|
|
|
|
| 863 |
if not state.get("english_finalized"):
|
| 864 |
self._progress(91, "english_validation")
|
| 865 |
full_draft_en = _clean_trailing_references(full_draft_en, ["Reference", "References", "Reference List"])
|
| 866 |
+
# Validate the completed body while the Working Abstract is still in
|
| 867 |
+
# place. Final front matter must not be generated until all body
|
| 868 |
+
# sections, citation cleanup, and any Discussion rebalance are frozen.
|
|
|
|
| 869 |
agent.validate_manuscript_structure(full_draft_en, adaptive_structure, translated=False)
|
| 870 |
if mode == "asq":
|
| 871 |
repeated_quotes = agent.find_repeated_qualitative_quotes(full_draft_en, data_metadata)
|
|
|
|
| 897 |
)
|
| 898 |
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 899 |
agent.enforce_resolved_claim_specific_citations(body_only)
|
| 900 |
+
|
| 901 |
+
# Body is now frozen. Generate both Final Abstract and Final Keywords
|
| 902 |
+
# from the completed manuscript/evidence, then replace the provisional
|
| 903 |
+
# front matter deterministically. No body rebalance may occur afterward.
|
| 904 |
+
self._progress(92, "final_front_matter")
|
| 905 |
+
final_front = agent.generate_final_front_matter(
|
| 906 |
+
topic, keywords, body_only, data_evidence=user_data_md, mode=mode
|
| 907 |
+
)
|
| 908 |
+
final_abstract = str(final_front.get("abstract") or "").strip()
|
| 909 |
+
final_keywords = list(final_front.get("keywords") or [])
|
| 910 |
+
if not final_abstract or not final_keywords:
|
| 911 |
+
raise RuntimeError("final_front_matter_missing")
|
| 912 |
+
full_draft_en = replace_abstract(full_draft_en, final_abstract)
|
| 913 |
+
full_draft_en = self._replace_keywords(full_draft_en, final_keywords)
|
| 914 |
+
body_only, _reference_tail = agent.validate_reference_tail(
|
| 915 |
+
full_draft_en, minimum_references=MIN_FINAL_REFERENCES
|
| 916 |
+
)
|
| 917 |
+
agent.validate_manuscript_structure(body_only, adaptive_structure, translated=False)
|
| 918 |
+
agent.enforce_resolved_claim_specific_citations(body_only)
|
| 919 |
self._validate_target_length(full_draft_en, target_words)
|
| 920 |
+
|
| 921 |
+
# Final front matter has a different source hash from the Working
|
| 922 |
+
# front matter. Retranslate only __front_matter__; unchanged body
|
| 923 |
+
# translations are retained and reused.
|
| 924 |
+
section_order_for_front = self._unique_top_level_sections(adaptive_structure)
|
| 925 |
+
if section_order_for_front:
|
| 926 |
+
final_front_source = self._extract_front_matter(body_only, section_order_for_front[0][1])
|
| 927 |
+
translation_sections, translation_glossary = self._translate_frozen_unit(
|
| 928 |
+
agent, unit_key='__front_matter__', unit_name='Final Front Matter',
|
| 929 |
+
source_text=final_front_source, target_lang=target_lang,
|
| 930 |
+
translation_sections=translation_sections, translation_glossary=translation_glossary,
|
| 931 |
+
adaptive_structure=adaptive_structure, include_front=True,
|
| 932 |
+
)
|
| 933 |
+
partial_target = self._assemble_partial_translation(
|
| 934 |
+
translation_sections, adaptive_structure, include_front=True
|
| 935 |
+
)
|
| 936 |
self._publish_preview(full_draft_en, "english_complete", force=True, complete=True)
|
| 937 |
state.update(
|
| 938 |
{
|
| 939 |
"full_draft_en": full_draft_en,
|
| 940 |
+
"full_draft_translated": partial_target,
|
| 941 |
"translation_sections": translation_sections,
|
| 942 |
"translation_glossary": translation_glossary,
|
| 943 |
+
"final_abstract": final_abstract,
|
| 944 |
+
"final_keywords": final_keywords,
|
| 945 |
+
"front_matter_phase": "final",
|
| 946 |
"english_finalized": True,
|
| 947 |
+
"translation_complete": False,
|
| 948 |
"agent_state": agent.export_state(),
|
| 949 |
}
|
| 950 |
)
|
core/pipeline_agent.py
CHANGED
|
@@ -1229,6 +1229,122 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
|
|
| 1229 |
model_role="reasoning", thinking=True, allow_fallback=False,
|
| 1230 |
)
|
| 1231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1232 |
# Backward-compatible name retained for old checkpoints/tests.
|
| 1233 |
def stream_generate_anchors(self, topic, keywords, synthesis_result, refs_text, data_evidence="", mode="amj"):
|
| 1234 |
return self.stream_generate_research_logic(
|
|
@@ -3176,18 +3292,10 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3176 |
return self._truncate_context(combined, max_tokens)
|
| 3177 |
|
| 3178 |
def generate_final_abstract(self, topic, keywords, manuscript, data_evidence='', mode='amj'):
|
| 3179 |
-
|
| 3180 |
-
|
| 3181 |
-
|
| 3182 |
-
abstract
|
| 3183 |
-
'You are a senior journal editor.', prompt, max_tokens=900, temp=0.1,
|
| 3184 |
-
model_role='writing', thinking=False, allow_fallback=False,
|
| 3185 |
-
)
|
| 3186 |
-
abstract = self._remove_ai_meta_traces(abstract)
|
| 3187 |
-
abstract = re.sub(r'(?im)^#+\s*abstract\s*', '', abstract).strip()
|
| 3188 |
-
abstract = self.polish_academic_section(abstract, 'Abstract', abstract=True, mode=mode)
|
| 3189 |
-
max_words = max(80, int(APP_CONFIG.get('manuscript_quality', {}).get('abstract_max_words', 200)))
|
| 3190 |
-
return self._trim_at_sentence_boundary(abstract, max_words)
|
| 3191 |
|
| 3192 |
@staticmethod
|
| 3193 |
def _translation_language_rule(target_lang):
|
|
@@ -3199,7 +3307,7 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3199 |
|
| 3200 |
@staticmethod
|
| 3201 |
def _translation_prompt(target_lang):
|
| 3202 |
-
common = """Translate the supplied blocks from the locked English manuscript faithfully. Preserve every [[SP-Pxxxxxx]] paragraph marker and every [[SP-Txxxxxx]]
|
| 3203 |
return common + ' ' + SSciPipelineAgent._translation_language_rule(target_lang)
|
| 3204 |
|
| 3205 |
@staticmethod
|
|
@@ -3244,6 +3352,139 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3244 |
)
|
| 3245 |
)
|
| 3246 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3247 |
@classmethod
|
| 3248 |
def _translation_protected_pattern(cls):
|
| 3249 |
# One-pass alternation prevents a placeholder created for a citation or
|
|
@@ -3251,7 +3492,10 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3251 |
# conservative: protect immutable research objects, not prose.
|
| 3252 |
acronym = cls._translation_acronym_regex()
|
| 3253 |
numeric = cls._translation_numeric_regex()
|
|
|
|
| 3254 |
return re.compile(
|
|
|
|
|
|
|
| 3255 |
r'(?i:https?://[^\s)>]+)'
|
| 3256 |
r'|(?i:\bdoi:\s*[^\s)>]+)'
|
| 3257 |
r'|(?i:\b10\.\d{4,9}/[-._;()/:A-Z0-9]+)'
|
|
@@ -3271,6 +3515,7 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3271 |
|
| 3272 |
@classmethod
|
| 3273 |
def _protect_translation_tokens(cls, value):
|
|
|
|
| 3274 |
mapping = {}
|
| 3275 |
counter = 0
|
| 3276 |
pattern = cls._translation_protected_pattern()
|
|
@@ -3279,13 +3524,25 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3279 |
nonlocal counter
|
| 3280 |
counter += 1
|
| 3281 |
placeholder = f'[[SP-T{counter:06d}]]'
|
| 3282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3283 |
return placeholder
|
| 3284 |
|
| 3285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3286 |
|
| 3287 |
-
@
|
| 3288 |
-
def _restore_translation_tokens(value, mapping, block_id=''):
|
| 3289 |
text = str(value or '')
|
| 3290 |
found = Counter(re.findall(r'\[\[SP-T\d{6}\]\]', text))
|
| 3291 |
expected = Counter(mapping.keys())
|
|
@@ -3295,8 +3552,22 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3295 |
raise DeepSeekRequestError(
|
| 3296 |
f'Translation protected placeholder mismatch in {block_id}: missing={missing}, added={added}.'
|
| 3297 |
)
|
| 3298 |
-
for placeholder,
|
| 3299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3300 |
if re.search(r'\[\[SP-T\d{6}\]\]', text):
|
| 3301 |
raise DeepSeekRequestError(f'Translation left unresolved protected placeholders in {block_id}.')
|
| 3302 |
return text
|
|
@@ -3308,8 +3579,9 @@ Output only the revised Discussion body, without the '# Discussion' heading."""
|
|
| 3308 |
acronyms = sorted(self._translation_acronym_tokens(body))
|
| 3309 |
phrases = re.findall(r'\b(?:[A-Z][a-z]+(?:[- ][A-Z][a-z]+){1,3})\b', str(body or ''))
|
| 3310 |
recurring = [term for term, count in Counter(phrases).most_common() if count >= 2]
|
|
|
|
| 3311 |
term_limit = max(10, int(APP_CONFIG.get('translation_glossary_term_limit', 40)))
|
| 3312 |
-
sources = list(dict.fromkeys(headings + recurring))[:term_limit]
|
| 3313 |
missing = [term for term in sources if not str(mapping.get(term) or '').strip()]
|
| 3314 |
if missing:
|
| 3315 |
language_rule = self._translation_language_rule(target_lang)
|
|
@@ -3555,7 +3827,9 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 3555 |
return parsed
|
| 3556 |
|
| 3557 |
def _validate_translation_block(self, source, translated, block_id, target_lang):
|
| 3558 |
-
if self.
|
|
|
|
|
|
|
| 3559 |
raise DeepSeekRequestError(f'Translation changed numerical content in {block_id}.')
|
| 3560 |
if self._translation_citation_tokens(source) != self._translation_citation_tokens(translated):
|
| 3561 |
raise DeepSeekRequestError(f'Translation changed citations in {block_id}.')
|
|
@@ -3570,6 +3844,11 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 3570 |
f'Translation changed variable abbreviations in {block_id}: '
|
| 3571 |
f'missing={missing}, added={added}.'
|
| 3572 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3573 |
|
| 3574 |
source_compact = re.sub(r'\s+', '', str(source or ''))
|
| 3575 |
target_compact = re.sub(r'\s+', '', str(translated or ''))
|
|
@@ -3616,11 +3895,24 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 3616 |
f'[[SP-P{block_id}]]\n{protected_source}'
|
| 3617 |
for block_id, _, protected_source in protected_batch
|
| 3618 |
)
|
| 3619 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3620 |
if _strict:
|
| 3621 |
effective_prompt += (
|
| 3622 |
-
'\nSTRICT RETRY: Copy every SP-P and SP-T marker byte-for-byte. Do not merge, drop,
|
| 3623 |
-
'
|
| 3624 |
)
|
| 3625 |
result = self.call_deepseek(
|
| 3626 |
effective_prompt, marked_source,
|
|
@@ -3673,7 +3965,7 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 3673 |
for block_id, source in batch:
|
| 3674 |
try:
|
| 3675 |
translated = self._restore_translation_tokens(
|
| 3676 |
-
parsed[block_id], protected_maps.get(block_id, {}), f'SP-P{block_id}'
|
| 3677 |
)
|
| 3678 |
restored[block_id] = self._validate_translation_block_protected(
|
| 3679 |
block_id, source, translated, target_lang
|
|
|
|
| 1229 |
model_role="reasoning", thinking=True, allow_fallback=False,
|
| 1230 |
)
|
| 1231 |
|
| 1232 |
+
@staticmethod
|
| 1233 |
+
def _front_matter_keywords(value, minimum=4, maximum=6):
|
| 1234 |
+
if isinstance(value, list):
|
| 1235 |
+
raw = value
|
| 1236 |
+
else:
|
| 1237 |
+
raw = re.split(r'[,;\n]+', str(value or ''))
|
| 1238 |
+
cleaned = []
|
| 1239 |
+
for item in raw:
|
| 1240 |
+
term = re.sub(r'^[\s\-•]+|[\s.;]+$', '', str(item or '')).strip()
|
| 1241 |
+
if term and term.casefold() not in {existing.casefold() for existing in cleaned}:
|
| 1242 |
+
cleaned.append(term)
|
| 1243 |
+
if not minimum <= len(cleaned) <= maximum:
|
| 1244 |
+
raise DeepSeekRequestError(
|
| 1245 |
+
f'Front-matter keywords must contain {minimum}-{maximum} distinct terms; got {len(cleaned)}.'
|
| 1246 |
+
)
|
| 1247 |
+
return cleaned
|
| 1248 |
+
|
| 1249 |
+
@staticmethod
|
| 1250 |
+
def _front_matter_has_citation(value):
|
| 1251 |
+
text = str(value or '')
|
| 1252 |
+
return bool(
|
| 1253 |
+
re.search(r'\[?CITE-\d+\]?', text, flags=re.I)
|
| 1254 |
+
or re.search(r'\([^()\n]*?(?:19|20)\d{2}[a-z]?[^()\n]*?\)', text, flags=re.I)
|
| 1255 |
+
)
|
| 1256 |
+
|
| 1257 |
+
@classmethod
|
| 1258 |
+
def _validate_working_abstract(cls, value):
|
| 1259 |
+
text = re.sub(r'(?im)^#+\s*abstract\s*', '', str(value or '')).strip()
|
| 1260 |
+
words = re.findall(r"\b[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*\b", text)
|
| 1261 |
+
if len(words) < 80 or len(words) > 180:
|
| 1262 |
+
raise DeepSeekRequestError(
|
| 1263 |
+
f'Working Abstract must contain 80-180 English words; got {len(words)}.'
|
| 1264 |
+
)
|
| 1265 |
+
if cls._front_matter_has_citation(text):
|
| 1266 |
+
raise DeepSeekRequestError('Working Abstract must not contain citations.')
|
| 1267 |
+
forbidden = (
|
| 1268 |
+
r'\bwe\s+(?:find|found|show|demonstrate|observe|establish)\b',
|
| 1269 |
+
r'\b(?:results?|findings?)\s+(?:show|indicate|reveal|demonstrate|confirm|support)\b',
|
| 1270 |
+
r'\b(?:supported|rejected)\s+(?:H\d+|hypothes)',
|
| 1271 |
+
r'\bstatistically\s+significant\b',
|
| 1272 |
+
r'(?:β|R²|χ²|ΔR²)\s*(?:=|<|>|≤|≥)',
|
| 1273 |
+
r'\bp\s*(?:=|<|>|≤|≥)\s*(?:\d|\.)',
|
| 1274 |
+
)
|
| 1275 |
+
if any(re.search(pattern, text, flags=re.I) for pattern in forbidden):
|
| 1276 |
+
raise DeepSeekRequestError('Working Abstract asserted a result that is not yet frozen.')
|
| 1277 |
+
return text
|
| 1278 |
+
|
| 1279 |
+
def generate_working_front_matter(
|
| 1280 |
+
self, topic, keywords, research_logic_contract, synthesis_result, data_evidence='', mode='amj'
|
| 1281 |
+
):
|
| 1282 |
+
"""Create a provisional visible abstract/keyword anchor without prewriting results."""
|
| 1283 |
+
evidence = self._truncate_context(str(data_evidence or ''), 3500)
|
| 1284 |
+
logic = self._truncate_context(str(research_logic_contract or ''), 3500)
|
| 1285 |
+
synthesis = self._truncate_context(str(synthesis_result or ''), 3500)
|
| 1286 |
+
prompt = f"""Create WORKING front matter for an empirical {str(mode).upper()}-oriented manuscript before body drafting.
|
| 1287 |
+
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 1288 |
+
The Working Abstract must be 120-180 English words, have no citations, and function only as a continuity anchor. It may state the research problem, theoretical tension, focal concepts, question/hypothesis architecture, design or method class, and intended contribution domain. It MUST NOT state coefficients, p-values, fit statistics, sample statistics unless already indispensable to define the design, supported/unsupported hypotheses, result direction, substantive findings, or a conclusion that has not yet been frozen. Do not predict findings.
|
| 1289 |
+
Return 4-6 concise academic keywords. User keywords are seeds, not an authoritative final list.
|
| 1290 |
+
TOPIC: {topic}
|
| 1291 |
+
USER KEYWORD SEEDS: {keywords}
|
| 1292 |
+
<PROVISIONAL_RESEARCH_LOGIC>
|
| 1293 |
+
{logic}
|
| 1294 |
+
</PROVISIONAL_RESEARCH_LOGIC>
|
| 1295 |
+
<LITERATURE_SYNTHESIS>
|
| 1296 |
+
{synthesis}
|
| 1297 |
+
</LITERATURE_SYNTHESIS>
|
| 1298 |
+
<FROZEN_RESEARCH_EVIDENCE>
|
| 1299 |
+
{evidence}
|
| 1300 |
+
</FROZEN_RESEARCH_EVIDENCE>"""
|
| 1301 |
+
response = self.call_deepseek(
|
| 1302 |
+
'You are a senior management-journal editor creating provisional front matter without anticipating results.',
|
| 1303 |
+
prompt, max_tokens=1100, temp=0.0, model_role='writing', thinking=False, allow_fallback=False,
|
| 1304 |
+
)
|
| 1305 |
+
parsed = self._extract_json_from_text(response)
|
| 1306 |
+
if not isinstance(parsed, dict):
|
| 1307 |
+
raise DeepSeekRequestError('Working front matter was not valid JSON.')
|
| 1308 |
+
abstract = self._validate_working_abstract(parsed.get('abstract'))
|
| 1309 |
+
working_keywords = self._front_matter_keywords(parsed.get('keywords'))
|
| 1310 |
+
return {'abstract': abstract, 'keywords': working_keywords}
|
| 1311 |
+
|
| 1312 |
+
def generate_final_front_matter(self, topic, keywords, manuscript, data_evidence='', mode='amj'):
|
| 1313 |
+
"""Generate final Abstract + Keywords only from the frozen completed body/evidence."""
|
| 1314 |
+
evidence = self._truncate_context(str(data_evidence or ''), 5000)
|
| 1315 |
+
body = self._abstract_manuscript_context(manuscript, 18000)
|
| 1316 |
+
prompt = f"""Write FINAL English front matter for this completed {str(mode).upper()} manuscript.
|
| 1317 |
+
Return JSON only: {{\"abstract\":\"...\",\"keywords\":[\"...\"]}}.
|
| 1318 |
+
The Abstract must be no more than 200 words, use no citations, and accurately state the research problem, validated design, main frozen finding(s), and contribution. Include only the minimum numerical evidence required; do not list coefficients, p-values, and fit statistics unless one value is indispensable. Do not infer beyond the frozen evidence or completed manuscript.
|
| 1319 |
+
Return 4-6 concise final academic keywords derived from the completed manuscript. User keyword seeds are advisory only.
|
| 1320 |
+
TOPIC: {topic}
|
| 1321 |
+
USER KEYWORD SEEDS: {keywords}
|
| 1322 |
+
<FROZEN_RESEARCH_EVIDENCE>
|
| 1323 |
+
{evidence}
|
| 1324 |
+
</FROZEN_RESEARCH_EVIDENCE>
|
| 1325 |
+
<COMPLETED_FROZEN_MANUSCRIPT>
|
| 1326 |
+
{body}
|
| 1327 |
+
</COMPLETED_FROZEN_MANUSCRIPT>"""
|
| 1328 |
+
response = self.call_deepseek(
|
| 1329 |
+
'You are a senior journal editor finalizing front matter from frozen manuscript evidence.',
|
| 1330 |
+
prompt, max_tokens=1200, temp=0.0, model_role='writing', thinking=False, allow_fallback=False,
|
| 1331 |
+
)
|
| 1332 |
+
parsed = self._extract_json_from_text(response)
|
| 1333 |
+
if not isinstance(parsed, dict):
|
| 1334 |
+
raise DeepSeekRequestError('Final front matter was not valid JSON.')
|
| 1335 |
+
abstract = re.sub(r'(?im)^#+\s*abstract\s*', '', str(parsed.get('abstract') or '')).strip()
|
| 1336 |
+
if self._front_matter_has_citation(abstract):
|
| 1337 |
+
raise DeepSeekRequestError('Final Abstract must not contain citations.')
|
| 1338 |
+
abstract = self.polish_academic_section(abstract, 'Abstract', abstract=True, mode=mode)
|
| 1339 |
+
if self._front_matter_has_citation(abstract):
|
| 1340 |
+
raise DeepSeekRequestError('Final Abstract revision introduced a citation.')
|
| 1341 |
+
max_words = max(80, int(APP_CONFIG.get('manuscript_quality', {}).get('abstract_max_words', 200)))
|
| 1342 |
+
abstract = self._trim_at_sentence_boundary(abstract, max_words)
|
| 1343 |
+
if len(re.findall(r"\b[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*\b", abstract)) < 80:
|
| 1344 |
+
raise DeepSeekRequestError('Final Abstract is too short after validation.')
|
| 1345 |
+
final_keywords = self._front_matter_keywords(parsed.get('keywords'))
|
| 1346 |
+
return {'abstract': abstract, 'keywords': final_keywords}
|
| 1347 |
+
|
| 1348 |
# Backward-compatible name retained for old checkpoints/tests.
|
| 1349 |
def stream_generate_anchors(self, topic, keywords, synthesis_result, refs_text, data_evidence="", mode="amj"):
|
| 1350 |
return self.stream_generate_research_logic(
|
|
|
|
| 3292 |
return self._truncate_context(combined, max_tokens)
|
| 3293 |
|
| 3294 |
def generate_final_abstract(self, topic, keywords, manuscript, data_evidence='', mode='amj'):
|
| 3295 |
+
"""Backward-compatible abstract-only facade over final front-matter generation."""
|
| 3296 |
+
return self.generate_final_front_matter(
|
| 3297 |
+
topic, keywords, manuscript, data_evidence=data_evidence, mode=mode
|
| 3298 |
+
)['abstract']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3299 |
|
| 3300 |
@staticmethod
|
| 3301 |
def _translation_language_rule(target_lang):
|
|
|
|
| 3307 |
|
| 3308 |
@staticmethod
|
| 3309 |
def _translation_prompt(target_lang):
|
| 3310 |
+
common = """Translate the supplied blocks from the locked English manuscript faithfully. Preserve every [[SP-Pxxxxxx]] paragraph marker and every [[SP-Txxxxxx]] semantic anchor exactly once. Semantic anchors are typed immutable research objects. Keep each anchor attached to the same source sentence and semantic owner/claim; citation anchors must stay with the same claim. Anchor order may change inside the same sentence only when target-language grammar requires it. Never move an anchor into another sentence or 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."""
|
| 3311 |
return common + ' ' + SSciPipelineAgent._translation_language_rule(target_lang)
|
| 3312 |
|
| 3313 |
@staticmethod
|
|
|
|
| 3352 |
)
|
| 3353 |
)
|
| 3354 |
|
| 3355 |
+
@staticmethod
|
| 3356 |
+
def _translation_date_regex():
|
| 3357 |
+
months = (
|
| 3358 |
+
r'January|February|March|April|May|June|July|August|September|October|November|December|'
|
| 3359 |
+
r'Jan\.?|Feb\.?|Mar\.?|Apr\.?|Jun\.?|Jul\.?|Aug\.?|Sep\.?|Sept\.?|Oct\.?|Nov\.?|Dec\.?'
|
| 3360 |
+
)
|
| 3361 |
+
return (
|
| 3362 |
+
rf'(?<![A-Za-z])(?:{months})\s+\d{{1,2}}(?:st|nd|rd|th)?(?:,)?\s+(?:19|20)\d{{2}}(?!\d)'
|
| 3363 |
+
rf'|(?<![A-Za-z0-9])\d{{1,2}}(?:st|nd|rd|th)?\s+(?:{months})\s+(?:19|20)\d{{2}}(?!\d)'
|
| 3364 |
+
r'|(?<!\d)(?:19|20)\d{2}-\d{1,2}-\d{1,2}(?!\d)'
|
| 3365 |
+
)
|
| 3366 |
+
|
| 3367 |
+
@classmethod
|
| 3368 |
+
def _canonicalize_translation_date(cls, value):
|
| 3369 |
+
text = str(value or '').strip().replace(',', '')
|
| 3370 |
+
iso = re.fullmatch(r'((?:19|20)\d{2})-(\d{1,2})-(\d{1,2})', text)
|
| 3371 |
+
if iso:
|
| 3372 |
+
year, month, day = map(int, iso.groups())
|
| 3373 |
+
try:
|
| 3374 |
+
return datetime(year, month, day).strftime('%Y-%m-%d')
|
| 3375 |
+
except ValueError:
|
| 3376 |
+
return ''
|
| 3377 |
+
month_map = {
|
| 3378 |
+
'jan':1,'january':1,'feb':2,'february':2,'mar':3,'march':3,'apr':4,'april':4,'may':5,
|
| 3379 |
+
'jun':6,'june':6,'jul':7,'july':7,'aug':8,'august':8,'sep':9,'sept':9,'september':9,
|
| 3380 |
+
'oct':10,'october':10,'nov':11,'november':11,'dec':12,'december':12,
|
| 3381 |
+
}
|
| 3382 |
+
m = re.fullmatch(r'([A-Za-z.]+)\s+(\d{1,2})(?:st|nd|rd|th)?\s+((?:19|20)\d{2})', text, flags=re.I)
|
| 3383 |
+
if m:
|
| 3384 |
+
month_name, day, year = m.groups()
|
| 3385 |
+
else:
|
| 3386 |
+
m = re.fullmatch(r'(\d{1,2})(?:st|nd|rd|th)?\s+([A-Za-z.]+)\s+((?:19|20)\d{2})', text, flags=re.I)
|
| 3387 |
+
if not m:
|
| 3388 |
+
return ''
|
| 3389 |
+
day, month_name, year = m.groups()
|
| 3390 |
+
month = month_map.get(month_name.rstrip('.').casefold())
|
| 3391 |
+
if not month:
|
| 3392 |
+
return ''
|
| 3393 |
+
try:
|
| 3394 |
+
return datetime(int(year), int(month), int(day)).strftime('%Y-%m-%d')
|
| 3395 |
+
except ValueError:
|
| 3396 |
+
return ''
|
| 3397 |
+
|
| 3398 |
+
@staticmethod
|
| 3399 |
+
def _render_translation_date(canonical, target_lang):
|
| 3400 |
+
try:
|
| 3401 |
+
parsed = datetime.strptime(str(canonical), '%Y-%m-%d')
|
| 3402 |
+
except Exception:
|
| 3403 |
+
return str(canonical or '')
|
| 3404 |
+
year, month, day = parsed.year, parsed.month, parsed.day
|
| 3405 |
+
if 'Chinese' in str(target_lang):
|
| 3406 |
+
return f'{year}年{month}月{day}日'
|
| 3407 |
+
if 'Russian' in str(target_lang):
|
| 3408 |
+
months = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря']
|
| 3409 |
+
return f'{day} {months[month-1]} {year} года'
|
| 3410 |
+
if 'Kazakh' in str(target_lang):
|
| 3411 |
+
months = ['қаңтар','ақпан','наурыз','сәуір','мамыр','маусым','шілде','тамыз','қыркүйек','қазан','қараша','желтоқсан']
|
| 3412 |
+
return f'{year} жылғы {day} {months[month-1]}'
|
| 3413 |
+
return parsed.strftime('%B %-d, %Y') if os.name != 'nt' else f'{parsed.strftime("%B")} {day}, {year}'
|
| 3414 |
+
|
| 3415 |
+
@classmethod
|
| 3416 |
+
def _translation_date_canonicals(cls, value):
|
| 3417 |
+
text = str(value or '')
|
| 3418 |
+
found = []
|
| 3419 |
+
for match in re.finditer(cls._translation_date_regex(), text, flags=re.I):
|
| 3420 |
+
canonical = cls._canonicalize_translation_date(match.group(0))
|
| 3421 |
+
if canonical:
|
| 3422 |
+
found.append(canonical)
|
| 3423 |
+
for year, month, day in re.findall(r'(?<!\d)((?:19|20)\d{2})年(\d{1,2})月(\d{1,2})日', text):
|
| 3424 |
+
try:
|
| 3425 |
+
found.append(datetime(int(year), int(month), int(day)).strftime('%Y-%m-%d'))
|
| 3426 |
+
except ValueError:
|
| 3427 |
+
pass
|
| 3428 |
+
ru_months = {'января':1,'февраля':2,'марта':3,'апреля':4,'мая':5,'июня':6,'июля':7,'августа':8,'сентября':9,'октября':10,'ноября':11,'декабря':12}
|
| 3429 |
+
for day, month_name, year in re.findall(r'(?<!\d)(\d{1,2})\s+(января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)\s+((?:19|20)\d{2})(?:\s+года|\s+г\.)?', text, flags=re.I):
|
| 3430 |
+
try:
|
| 3431 |
+
found.append(datetime(int(year), ru_months[month_name.casefold()], int(day)).strftime('%Y-%m-%d'))
|
| 3432 |
+
except ValueError:
|
| 3433 |
+
pass
|
| 3434 |
+
kz_months = {'қаңтар':1,'ақпан':2,'наурыз':3,'сәуір':4,'мамыр':5,'маусым':6,'шілде':7,'тамыз':8,'қыркүйек':9,'қазан':10,'қараша':11,'желтоқсан':12}
|
| 3435 |
+
for year, day, month_name in re.findall(r'(?<!\d)((?:19|20)\d{2})\s+жылғы\s+(\d{1,2})\s+(қаңтар|ақпан|наурыз|сәуір|мамыр|маусым|шілде|тамыз|қыркүйек|қазан|қараша|желтоқсан)', text, flags=re.I):
|
| 3436 |
+
try:
|
| 3437 |
+
found.append(datetime(int(year), kz_months[month_name.casefold()], int(day)).strftime('%Y-%m-%d'))
|
| 3438 |
+
except ValueError:
|
| 3439 |
+
pass
|
| 3440 |
+
return Counter(found)
|
| 3441 |
+
|
| 3442 |
+
@classmethod
|
| 3443 |
+
def _numeric_tokens_without_dates(cls, value):
|
| 3444 |
+
text = str(value or '')
|
| 3445 |
+
text = re.sub(cls._translation_date_regex(), ' ', text, flags=re.I)
|
| 3446 |
+
text = re.sub(r'(?<!\d)(?:19|20)\d{2}年\d{1,2}月\d{1,2}日', ' ', text)
|
| 3447 |
+
text = re.sub(r'(?<!\d)\d{1,2}\s+(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)\s+(?:19|20)\d{2}(?:\s+года|\s+г\.)?', ' ', text, flags=re.I)
|
| 3448 |
+
text = re.sub(r'(?<!\d)(?:19|20)\d{2}\s+жылғы\s+\d{1,2}\s+(?:қаңтар|ақпан|наурыз|сәуір|мамыр|маусым|шілде|тамыз|қыркүйек|қазан|қараша|желтоқсан)', ' ', text, flags=re.I)
|
| 3449 |
+
return cls._numeric_tokens(text)
|
| 3450 |
+
|
| 3451 |
+
@classmethod
|
| 3452 |
+
def _translation_anchor_type(cls, token):
|
| 3453 |
+
value = str(token or '')
|
| 3454 |
+
if re.fullmatch(cls._translation_date_regex(), value, flags=re.I): return 'DATE'
|
| 3455 |
+
if re.fullmatch(r'\([^()\n]*?(?:19|20)\d{2}[a-z]?[^()\n]*?\)', value, flags=re.I): return 'CITATION'
|
| 3456 |
+
if re.fullmatch(r'(?i:https?://[^\s)>]+|doi:\s*[^\s)>]+|10\.\d{4,9}/[-._;()/:A-Z0-9]+)', value): return 'DOI_URL'
|
| 3457 |
+
if re.fullmatch(r'(?i:H\d+[A-Za-z]?)', value): return 'HYPOTHESIS'
|
| 3458 |
+
if re.search(r'(?:p|t|f|z|r|β|χ²|R²|ΔR²)\s*(?:<=|>=|=|<|>|≤|≥)', value, flags=re.I): return 'STAT'
|
| 3459 |
+
if re.fullmatch(r'(?:fsQCA|SmartPLS|Mplus|OpenAlex|DeepSeek|ChatGPT|RStudio|SPSS|AMOS|Stata)', value): return 'NAME'
|
| 3460 |
+
if re.fullmatch(cls._translation_acronym_regex(), value): return 'ACRONYM'
|
| 3461 |
+
if re.fullmatch(r'(?:QUOTE|CASE|RESP|P)-\d+', value, flags=re.I): return 'QUOTE_ID'
|
| 3462 |
+
if re.fullmatch(cls._translation_numeric_regex(), value): return 'NUMBER'
|
| 3463 |
+
if (value.startswith('`') and value.endswith('`')) or (value.startswith('$') and value.endswith('$')): return 'FORMULA'
|
| 3464 |
+
return 'IMMUTABLE'
|
| 3465 |
+
|
| 3466 |
+
@staticmethod
|
| 3467 |
+
def _translation_sentence_index(text, position):
|
| 3468 |
+
prefix = str(text or '')[:max(0, int(position))]
|
| 3469 |
+
# Chinese sentence punctuation does not require whitespace. For Latin/
|
| 3470 |
+
# Cyrillic punctuation, count a boundary only before a plausible new
|
| 3471 |
+
# sentence or end-of-prefix so abbreviations such as e.g. do not create
|
| 3472 |
+
# false semantic-owner boundaries.
|
| 3473 |
+
return len(re.findall(
|
| 3474 |
+
r'[。!?]|[.!?](?=\s*(?:[A-ZА-ЯЁӘҒҚҢӨҰҮҺІ\["“]|$))', prefix
|
| 3475 |
+
))
|
| 3476 |
+
|
| 3477 |
+
@classmethod
|
| 3478 |
+
def _translation_longform_acronym_pairs(cls, value):
|
| 3479 |
+
pairs = []
|
| 3480 |
+
text = str(value or '')
|
| 3481 |
+
pattern = re.compile(r'(?<![A-Za-z])([A-Za-z][A-Za-z-]*(?:\s+[A-Za-z][A-Za-z-]*){1,5})\s*\(([^()\s]{2,20})\)')
|
| 3482 |
+
for match in pattern.finditer(text):
|
| 3483 |
+
long_form, acronym = match.group(1).strip(), match.group(2).strip()
|
| 3484 |
+
if cls._translation_acronym_tokens(acronym) == Counter({acronym: 1}):
|
| 3485 |
+
pairs.append((long_form, acronym))
|
| 3486 |
+
return pairs
|
| 3487 |
+
|
| 3488 |
@classmethod
|
| 3489 |
def _translation_protected_pattern(cls):
|
| 3490 |
# One-pass alternation prevents a placeholder created for a citation or
|
|
|
|
| 3492 |
# conservative: protect immutable research objects, not prose.
|
| 3493 |
acronym = cls._translation_acronym_regex()
|
| 3494 |
numeric = cls._translation_numeric_regex()
|
| 3495 |
+
date_pattern = cls._translation_date_regex()
|
| 3496 |
return re.compile(
|
| 3497 |
+
rf'(?:{date_pattern})'
|
| 3498 |
+
r'|'
|
| 3499 |
r'(?i:https?://[^\s)>]+)'
|
| 3500 |
r'|(?i:\bdoi:\s*[^\s)>]+)'
|
| 3501 |
r'|(?i:\b10\.\d{4,9}/[-._;()/:A-Z0-9]+)'
|
|
|
|
| 3515 |
|
| 3516 |
@classmethod
|
| 3517 |
def _protect_translation_tokens(cls, value):
|
| 3518 |
+
source = str(value or '')
|
| 3519 |
mapping = {}
|
| 3520 |
counter = 0
|
| 3521 |
pattern = cls._translation_protected_pattern()
|
|
|
|
| 3524 |
nonlocal counter
|
| 3525 |
counter += 1
|
| 3526 |
placeholder = f'[[SP-T{counter:06d}]]'
|
| 3527 |
+
original = match.group(0)
|
| 3528 |
+
anchor_type = cls._translation_anchor_type(original)
|
| 3529 |
+
manifest = {'source': original, 'type': anchor_type}
|
| 3530 |
+
if anchor_type == 'DATE':
|
| 3531 |
+
manifest['canonical'] = cls._canonicalize_translation_date(original)
|
| 3532 |
+
left = source[max(0, match.start() - 80):match.start()]
|
| 3533 |
+
right = source[match.end():min(len(source), match.end() + 60)]
|
| 3534 |
+
manifest['owner_hint'] = (left + '{ANCHOR}' + right).replace('\n', ' ')
|
| 3535 |
+
mapping[placeholder] = manifest
|
| 3536 |
return placeholder
|
| 3537 |
|
| 3538 |
+
protected = pattern.sub(replace, source)
|
| 3539 |
+
for placeholder, manifest in mapping.items():
|
| 3540 |
+
position = protected.find(placeholder)
|
| 3541 |
+
manifest['sentence_index'] = cls._translation_sentence_index(protected, position)
|
| 3542 |
+
return protected, mapping
|
| 3543 |
|
| 3544 |
+
@classmethod
|
| 3545 |
+
def _restore_translation_tokens(cls, value, mapping, block_id='', target_lang=''):
|
| 3546 |
text = str(value or '')
|
| 3547 |
found = Counter(re.findall(r'\[\[SP-T\d{6}\]\]', text))
|
| 3548 |
expected = Counter(mapping.keys())
|
|
|
|
| 3552 |
raise DeepSeekRequestError(
|
| 3553 |
f'Translation protected placeholder mismatch in {block_id}: missing={missing}, added={added}.'
|
| 3554 |
)
|
| 3555 |
+
for placeholder, raw_manifest in mapping.items():
|
| 3556 |
+
manifest = raw_manifest if isinstance(raw_manifest, dict) else {'source': str(raw_manifest), 'type': 'IMMUTABLE'}
|
| 3557 |
+
source_sentence = int(manifest.get('sentence_index') or 0)
|
| 3558 |
+
target_sentence = cls._translation_sentence_index(text, text.find(placeholder))
|
| 3559 |
+
if manifest.get('type') in {'DATE','CITATION','STAT','HYPOTHESIS','ACRONYM','NAME'} and target_sentence != source_sentence:
|
| 3560 |
+
raise DeepSeekRequestError(
|
| 3561 |
+
f'Translation semantic anchor moved across sentence boundary in {block_id}: {placeholder}.'
|
| 3562 |
+
)
|
| 3563 |
+
for placeholder, raw_manifest in mapping.items():
|
| 3564 |
+
manifest = raw_manifest if isinstance(raw_manifest, dict) else {'source': str(raw_manifest), 'type': 'IMMUTABLE'}
|
| 3565 |
+
original = str(manifest.get('source') or '')
|
| 3566 |
+
if target_lang and manifest.get('type') == 'DATE' and manifest.get('canonical'):
|
| 3567 |
+
replacement = cls._render_translation_date(manifest.get('canonical'), target_lang)
|
| 3568 |
+
else:
|
| 3569 |
+
replacement = original
|
| 3570 |
+
text = text.replace(placeholder, replacement)
|
| 3571 |
if re.search(r'\[\[SP-T\d{6}\]\]', text):
|
| 3572 |
raise DeepSeekRequestError(f'Translation left unresolved protected placeholders in {block_id}.')
|
| 3573 |
return text
|
|
|
|
| 3579 |
acronyms = sorted(self._translation_acronym_tokens(body))
|
| 3580 |
phrases = re.findall(r'\b(?:[A-Z][a-z]+(?:[- ][A-Z][a-z]+){1,3})\b', str(body or ''))
|
| 3581 |
recurring = [term for term, count in Counter(phrases).most_common() if count >= 2]
|
| 3582 |
+
paired_long_forms = [long_form for long_form, _ in self._translation_longform_acronym_pairs(body)]
|
| 3583 |
term_limit = max(10, int(APP_CONFIG.get('translation_glossary_term_limit', 40)))
|
| 3584 |
+
sources = list(dict.fromkeys(headings + paired_long_forms + recurring))[:term_limit]
|
| 3585 |
missing = [term for term in sources if not str(mapping.get(term) or '').strip()]
|
| 3586 |
if missing:
|
| 3587 |
language_rule = self._translation_language_rule(target_lang)
|
|
|
|
| 3827 |
return parsed
|
| 3828 |
|
| 3829 |
def _validate_translation_block(self, source, translated, block_id, target_lang):
|
| 3830 |
+
if self._translation_date_canonicals(source) != self._translation_date_canonicals(translated):
|
| 3831 |
+
raise DeepSeekRequestError(f'Translation changed date content in {block_id}.')
|
| 3832 |
+
if self._numeric_tokens_without_dates(source) != self._numeric_tokens_without_dates(translated):
|
| 3833 |
raise DeepSeekRequestError(f'Translation changed numerical content in {block_id}.')
|
| 3834 |
if self._translation_citation_tokens(source) != self._translation_citation_tokens(translated):
|
| 3835 |
raise DeepSeekRequestError(f'Translation changed citations in {block_id}.')
|
|
|
|
| 3844 |
f'Translation changed variable abbreviations in {block_id}: '
|
| 3845 |
f'missing={missing}, added={added}.'
|
| 3846 |
)
|
| 3847 |
+
for _long_form, acronym in self._translation_longform_acronym_pairs(source):
|
| 3848 |
+
if not re.search(rf'[((]\s*{re.escape(acronym)}\s*[))]', str(translated or '')):
|
| 3849 |
+
raise DeepSeekRequestError(
|
| 3850 |
+
f'Translation detached long-form/acronym pair in {block_id}: {acronym}.'
|
| 3851 |
+
)
|
| 3852 |
|
| 3853 |
source_compact = re.sub(r'\s+', '', str(source or ''))
|
| 3854 |
target_compact = re.sub(r'\s+', '', str(translated or ''))
|
|
|
|
| 3895 |
f'[[SP-P{block_id}]]\n{protected_source}'
|
| 3896 |
for block_id, _, protected_source in protected_batch
|
| 3897 |
)
|
| 3898 |
+
semantic_manifest = {
|
| 3899 |
+
block_id: [
|
| 3900 |
+
{'id': placeholder, **manifest}
|
| 3901 |
+
for placeholder, manifest in protected_maps.get(block_id, {}).items()
|
| 3902 |
+
]
|
| 3903 |
+
for block_id, _source, _protected in protected_batch
|
| 3904 |
+
}
|
| 3905 |
+
effective_prompt = system_prompt + (
|
| 3906 |
+
'\n<SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 3907 |
+
+ json.dumps(semantic_manifest, ensure_ascii=False)
|
| 3908 |
+
+ '\n</SEMANTIC_ANCHOR_MANIFEST>\n'
|
| 3909 |
+
'Use anchor type, canonical value, sentence_index, and owner_hint only to keep each placeholder attached to its original semantic owner. '
|
| 3910 |
+
'Do not print the manifest. DATE anchors may change local word order inside their sentence; CITATION anchors must remain with their original claim.'
|
| 3911 |
+
)
|
| 3912 |
if _strict:
|
| 3913 |
effective_prompt += (
|
| 3914 |
+
'\nSTRICT RETRY: Copy every SP-P and SP-T marker byte-for-byte. Do not merge, drop, translate, '
|
| 3915 |
+
'pluralize, or normalize any protected token. Reposition an SP-T only inside the same sentence when target-language grammar requires it; never move it to another claim.'
|
| 3916 |
)
|
| 3917 |
result = self.call_deepseek(
|
| 3918 |
effective_prompt, marked_source,
|
|
|
|
| 3965 |
for block_id, source in batch:
|
| 3966 |
try:
|
| 3967 |
translated = self._restore_translation_tokens(
|
| 3968 |
+
parsed[block_id], protected_maps.get(block_id, {}), f'SP-P{block_id}', target_lang
|
| 3969 |
)
|
| 3970 |
restored[block_id] = self._validate_translation_block_protected(
|
| 3971 |
block_id, source, translated, target_lang
|
smartpaper_restore_bridge/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (3.12 kB). View file
|
|
|
smartpaper_restore_bridge/__pycache__/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[.ShellClassInfo]
|
| 2 |
+
IconResource=C:\Program Files\Google\Drive File Stream\129.0.1.0\GoogleDriveFS.exe,27
|
utils/__pycache__/desktop.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
Binary file (9.38 kB). View file
|
|
|
utils/__pycache__/file_parser.cpython-313.pyc
ADDED
|
Binary file (8.01 kB). View file
|
|
|