feat: hide flagged scientific review captions

This commit is contained in:
2026-08-25 17:16:55 +08:00
parent fb5a4017f6
commit 257d7e1759
5 changed files with 44 additions and 6 deletions
+2 -1
View File
@@ -110,7 +110,8 @@ This produces a separate reviewed translation set and JSON/Markdown report;
the initial translations remain unchanged. Evidence-backed terminology or the initial translations remain unchanged. Evidence-backed terminology or
semantic corrections are applied, while uncertain source-caption, numeric, semantic corrections are applied, while uncertain source-caption, numeric,
unit, drug, gene, protein, vector, strain, or model-name issues are preserved 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. ordinary internal-use workflow, but must be disclosed in the final handoff.
Render only the reviewed translation set and bind its exact review report: Render only the reviewed translation set and bind its exact review report:
@@ -113,7 +113,9 @@ Use the statuses conservatively:
scientific or semantic error. Do not use it for preference-only rewriting. scientific or semantic error. Do not use it for preference-only rewriting.
- `flagged`: preserve the initial translation exactly and record a medium/high - `flagged`: preserve the initial translation exactly and record a medium/high
unresolved concern. Never guess a correction to a suspected source-caption 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 Numbers and scientific names are protected. The validator rejects a correction
that changes them; use `flagged` when such a change may be necessary. Preserve that changes them; use `flagged` when such a change may be necessary. Preserve
@@ -392,6 +392,9 @@ def finalize(
if [item["id"] for item in all_reviews] != [item["id"] for item in items]: 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") raise ReviewError("scientific reviews do not cover every subtitle segment exactly once")
reviewed = {record["id"]: record["translation"] for record in all_reviews} 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 = review_dir / REVIEWED_DIR_NAME
reviewed_dir.mkdir(parents=True, exist_ok=True) reviewed_dir.mkdir(parents=True, exist_ok=True)
reviewed_path = reviewed_dir / "translations.json" reviewed_path = reviewed_dir / "translations.json"
@@ -423,6 +426,7 @@ def finalize(
"initial_translation_sha256": _sha256_json(initial), "initial_translation_sha256": _sha256_json(initial),
"reviewed_translation_sha256": _sha256_json(reviewed), "reviewed_translation_sha256": _sha256_json(reviewed),
"reviewed_translations_dir": str(reviewed_dir.resolve()), "reviewed_translations_dir": str(reviewed_dir.resolve()),
"suppressed_segment_ids": suppressed_segment_ids,
"profile": profile, "profile": profile,
"counts": { "counts": {
"total": len(all_reviews), "total": len(all_reviews),
@@ -1001,7 +1001,8 @@ def _ass_layout(manifest: dict[str, Any]) -> dict[str, int]:
def _render_ass( 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: ) -> str:
font = _validate_font(font) font = _validate_font(font)
cue_by_id = _cue_map(manifest) 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) target_language = str(manifest.get("target_language") or DEFAULT_TARGET_LANGUAGE)
dialogue: list[str] = [] dialogue: list[str] = []
for segment in _render_segments(manifest): 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_exact = _segment_source_text(segment, cue_by_id)
target_exact = normalize_target_caption( target_exact = normalize_target_caption(
_display_translation(manifest, segment, translations), target_language _display_translation(manifest, segment, translations), target_language
@@ -1070,7 +1075,8 @@ def _target_srt_name(manifest: dict[str, Any]) -> str:
def _expected_outputs( 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]: ) -> dict[str, bytes]:
cue_by_id = _cue_map(manifest) cue_by_id = _cue_map(manifest)
layout = _ass_layout(manifest) layout = _ass_layout(manifest)
@@ -1079,6 +1085,10 @@ def _expected_outputs(
target_entries: list[tuple[int, int, str]] = [] target_entries: list[tuple[int, int, str]] = []
bilingual_entries: list[tuple[int, int, str]] = [] bilingual_entries: list[tuple[int, int, str]] = []
for segment in _render_segments(manifest): 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_exact = _segment_source_text(segment, cue_by_id)
source_chunks = wrap_layout_chunks(source_exact, layout["source_columns"]) source_chunks = wrap_layout_chunks(source_exact, layout["source_columns"])
if "".join(source_chunks) != source_exact: if "".join(source_chunks) != source_exact:
@@ -1099,7 +1109,9 @@ def _expected_outputs(
"source.srt": _render_srt(source_entries).encode("utf-8"), "source.srt": _render_srt(source_entries).encode("utf-8"),
_target_srt_name(manifest): _render_srt(target_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.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") counts = value.get("counts")
if not isinstance(counts, dict) or counts.get("total") != len(translations): if not isinstance(counts, dict) or counts.get("total") != len(translations):
raise PipelineError("scientific review report segment count is invalid") 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 { return {
"report_path": str(review_report), "report_path": str(review_report),
"report_sha256": _sha256_bytes(review_report.read_bytes()), "report_sha256": _sha256_bytes(review_report.read_bytes()),
@@ -1162,6 +1177,7 @@ def _scientific_review_binding(
"human_expert_reviewed": False, "human_expert_reviewed": False,
"counts": counts, "counts": counts,
"disclosure": value.get("disclosure"), "disclosure": value.get("disclosure"),
"suppressed_segment_ids": suppressed,
} }
@@ -1211,7 +1227,21 @@ def render(
scientific_review = _scientific_review_binding( scientific_review = _scientific_review_binding(
scientific_review_report, manifest_path, translations_dir, translations 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) output_dir.mkdir(parents=True, exist_ok=True)
for name, data in expected.items(): for name, data in expected.items():
_atomic_write(output_dir / name, data) _atomic_write(output_dir / name, data)
@@ -133,6 +133,7 @@ class ScientificReviewTests(unittest.TestCase):
target = (self.root / "rendered" / "zh-CN.srt").read_text(encoding="utf-8") target = (self.root / "rendered" / "zh-CN.srt").read_text(encoding="utf-8")
self.assertIn("暴露上方巩膜", target) self.assertIn("暴露上方巩膜", target)
self.assertNotIn("暴露上方巩膜组织", target) self.assertNotIn("暴露上方巩膜组织", target)
self.assertNotIn(items[2]["translation"], target)
def test_correction_cannot_change_protected_number_or_scientific_name(self) -> None: def test_correction_cannot_change_protected_number_or_scientific_name(self) -> None:
self.write_profile() self.write_profile()