diff --git a/skills/materialsub/SKILL.md b/skills/materialsub/SKILL.md index 0e04e0c..1f5fb5f 100644 --- a/skills/materialsub/SKILL.md +++ b/skills/materialsub/SKILL.md @@ -110,7 +110,8 @@ This produces a separate reviewed translation set and JSON/Markdown report; the initial translations remain unchanged. Evidence-backed terminology or semantic corrections are applied, while uncertain source-caption, numeric, unit, drug, gene, protein, vector, strain, or model-name issues are preserved -and flagged rather than guessed. Unresolved high-risk flags do not block the +and flagged rather than guessed. Flagged cue pairs stay in the review report +but are omitted from rendered subtitles. Unresolved high-risk flags do not block the ordinary internal-use workflow, but must be disclosed in the final handoff. Render only the reviewed translation set and bind its exact review report: diff --git a/skills/materialsub/references/scientific-review.md b/skills/materialsub/references/scientific-review.md index 0ab4706..0d3208c 100644 --- a/skills/materialsub/references/scientific-review.md +++ b/skills/materialsub/references/scientific-review.md @@ -113,7 +113,9 @@ Use the statuses conservatively: scientific or semantic error. Do not use it for preference-only rewriting. - `flagged`: preserve the initial translation exactly and record a medium/high unresolved concern. Never guess a correction to a suspected source-caption - error, drug, gene, protein, vector, strain, model, dose, or unit. + error, drug, gene, protein, vector, strain, model, dose, or unit. Flagged + cue pairs remain in the report but are excluded from the rendered bilingual + subtitles, so uncertainty markers such as “(听不清)” never appear in the MP4. Numbers and scientific names are protected. The validator rejects a correction that changes them; use `flagged` when such a change may be necessary. Preserve diff --git a/skills/materialsub/scripts/scientific_review.py b/skills/materialsub/scripts/scientific_review.py index cb61ce6..ebce8a7 100755 --- a/skills/materialsub/scripts/scientific_review.py +++ b/skills/materialsub/scripts/scientific_review.py @@ -392,6 +392,9 @@ def finalize( if [item["id"] for item in all_reviews] != [item["id"] for item in items]: raise ReviewError("scientific reviews do not cover every subtitle segment exactly once") reviewed = {record["id"]: record["translation"] for record in all_reviews} + suppressed_segment_ids = [ + record["id"] for record in all_reviews if record["status"] == "flagged" + ] reviewed_dir = review_dir / REVIEWED_DIR_NAME reviewed_dir.mkdir(parents=True, exist_ok=True) reviewed_path = reviewed_dir / "translations.json" @@ -423,6 +426,7 @@ def finalize( "initial_translation_sha256": _sha256_json(initial), "reviewed_translation_sha256": _sha256_json(reviewed), "reviewed_translations_dir": str(reviewed_dir.resolve()), + "suppressed_segment_ids": suppressed_segment_ids, "profile": profile, "counts": { "total": len(all_reviews), diff --git a/skills/materialsub/scripts/subtitle_pipeline.py b/skills/materialsub/scripts/subtitle_pipeline.py index 651278f..f74a120 100755 --- a/skills/materialsub/scripts/subtitle_pipeline.py +++ b/skills/materialsub/scripts/subtitle_pipeline.py @@ -1001,7 +1001,8 @@ def _ass_layout(manifest: dict[str, Any]) -> dict[str, int]: def _render_ass( - manifest: dict[str, Any], translations: dict[str, str], font: str + manifest: dict[str, Any], translations: dict[str, str], font: str, + suppressed_segment_ids: set[str] | None = None, ) -> str: font = _validate_font(font) cue_by_id = _cue_map(manifest) @@ -1035,6 +1036,10 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text target_language = str(manifest.get("target_language") or DEFAULT_TARGET_LANGUAGE) dialogue: list[str] = [] for segment in _render_segments(manifest): + if suppressed_segment_ids and any( + segment_id in suppressed_segment_ids for segment_id in segment["cue_ids"] + ): + continue source_exact = _segment_source_text(segment, cue_by_id) target_exact = normalize_target_caption( _display_translation(manifest, segment, translations), target_language @@ -1070,7 +1075,8 @@ def _target_srt_name(manifest: dict[str, Any]) -> str: def _expected_outputs( - manifest: dict[str, Any], translations: dict[str, str], font: str + manifest: dict[str, Any], translations: dict[str, str], font: str, + suppressed_segment_ids: set[str] | None = None, ) -> dict[str, bytes]: cue_by_id = _cue_map(manifest) layout = _ass_layout(manifest) @@ -1079,6 +1085,10 @@ def _expected_outputs( target_entries: list[tuple[int, int, str]] = [] bilingual_entries: list[tuple[int, int, str]] = [] for segment in _render_segments(manifest): + if suppressed_segment_ids and any( + segment_id in suppressed_segment_ids for segment_id in segment["cue_ids"] + ): + continue source_exact = _segment_source_text(segment, cue_by_id) source_chunks = wrap_layout_chunks(source_exact, layout["source_columns"]) if "".join(source_chunks) != source_exact: @@ -1099,7 +1109,9 @@ def _expected_outputs( "source.srt": _render_srt(source_entries).encode("utf-8"), _target_srt_name(manifest): _render_srt(target_entries).encode("utf-8"), "bilingual.srt": _render_srt(bilingual_entries).encode("utf-8"), - "bilingual.ass": _render_ass(manifest, translations, font).encode("utf-8"), + "bilingual.ass": _render_ass( + manifest, translations, font, suppressed_segment_ids + ).encode("utf-8"), } @@ -1155,6 +1167,9 @@ def _scientific_review_binding( counts = value.get("counts") if not isinstance(counts, dict) or counts.get("total") != len(translations): raise PipelineError("scientific review report segment count is invalid") + suppressed = value.get("suppressed_segment_ids", []) + if not isinstance(suppressed, list) or not all(isinstance(segment_id, str) for segment_id in suppressed): + raise PipelineError("scientific review suppression list is malformed") return { "report_path": str(review_report), "report_sha256": _sha256_bytes(review_report.read_bytes()), @@ -1162,6 +1177,7 @@ def _scientific_review_binding( "human_expert_reviewed": False, "counts": counts, "disclosure": value.get("disclosure"), + "suppressed_segment_ids": suppressed, } @@ -1211,7 +1227,21 @@ def render( scientific_review = _scientific_review_binding( scientific_review_report, manifest_path, translations_dir, translations ) - expected = _expected_outputs(manifest, translations, font) + suppressed_cue_ids = None + if scientific_review: + suppressed_review_ids = set(scientific_review.get("suppressed_segment_ids", [])) + suppressed_cue_ids = { + cue_id + for translation_segment in manifest["segments"] + if translation_segment["id"] in suppressed_review_ids + for cue_id in translation_segment["cue_ids"] + } + expected = _expected_outputs( + manifest, + translations, + font, + suppressed_cue_ids, + ) output_dir.mkdir(parents=True, exist_ok=True) for name, data in expected.items(): _atomic_write(output_dir / name, data) diff --git a/skills/materialsub/tests/test_scientific_review.py b/skills/materialsub/tests/test_scientific_review.py index 298f824..afd4670 100644 --- a/skills/materialsub/tests/test_scientific_review.py +++ b/skills/materialsub/tests/test_scientific_review.py @@ -133,6 +133,7 @@ class ScientificReviewTests(unittest.TestCase): target = (self.root / "rendered" / "zh-CN.srt").read_text(encoding="utf-8") self.assertIn("暴露上方巩膜", target) self.assertNotIn("暴露上方巩膜组织", target) + self.assertNotIn(items[2]["translation"], target) def test_correction_cannot_change_protected_number_or_scientific_name(self) -> None: self.write_profile()