feat: add scientific subtitle review workflow

This commit is contained in:
2026-08-25 14:22:30 +08:00
parent 555ed525f6
commit fb5a4017f6
11 changed files with 1054 additions and 14 deletions
@@ -1131,14 +1131,52 @@ def _check_outputs(
return checksums
def _scientific_review_binding(
review_report: Path | None,
manifest_path: Path,
translations_dir: Path,
translations: dict[str, str],
) -> dict[str, Any] | None:
if review_report is None:
return None
review_report = review_report.expanduser().resolve()
value = _read_json(review_report)
if not isinstance(value, dict) or value.get("status") != "complete":
raise PipelineError("scientific review report is incomplete or malformed")
if value.get("human_expert_reviewed") is not False:
raise PipelineError("scientific review report must not claim human expert review")
if value.get("subtitle_manifest_sha256") != _sha256_bytes(manifest_path.read_bytes()):
raise PipelineError("scientific review report is bound to a different subtitle manifest")
declared_dir = value.get("reviewed_translations_dir")
if not isinstance(declared_dir, str) or Path(declared_dir).expanduser().resolve() != translations_dir.expanduser().resolve():
raise PipelineError("render translations do not match the scientific review report")
if value.get("reviewed_translation_sha256") != _sha256_json(translations):
raise PipelineError("reviewed translations changed after scientific review")
counts = value.get("counts")
if not isinstance(counts, dict) or counts.get("total") != len(translations):
raise PipelineError("scientific review report segment count is invalid")
return {
"report_path": str(review_report),
"report_sha256": _sha256_bytes(review_report.read_bytes()),
"method": value.get("review_method"),
"human_expert_reviewed": False,
"counts": counts,
"disclosure": value.get("disclosure"),
}
def _validation_report(
manifest: dict[str, Any], checksums: dict[str, str], font: str
manifest: dict[str, Any],
checksums: dict[str, str],
font: str,
scientific_review: dict[str, Any] | None = None,
) -> dict[str, Any]:
return {
"schema_version": SCHEMA_VERSION,
"structurally_valid": True,
"validation_scope": "structural_source_integrity",
"translation_quality_reviewed": False,
"translation_quality_reviewed": scientific_review is not None,
"scientific_review": scientific_review,
"target_language": manifest.get("target_language") or DEFAULT_TARGET_LANGUAGE,
"source_sha256": manifest["source"]["sha256"],
"source_ledger_sha256": manifest["source_ledger_sha256"],
@@ -1164,11 +1202,15 @@ def render(
translations_dir: Path,
output_dir: Path,
font: str = DEFAULT_FONT,
scientific_review_report: Path | None = None,
) -> Path:
manifest_path = manifest_path.expanduser().resolve()
output_dir = output_dir.expanduser().resolve()
manifest = validate_manifest(manifest_path)
translations = load_translations(manifest, translations_dir)
scientific_review = _scientific_review_binding(
scientific_review_report, manifest_path, translations_dir, translations
)
expected = _expected_outputs(manifest, translations, font)
output_dir.mkdir(parents=True, exist_ok=True)
for name, data in expected.items():
@@ -1177,7 +1219,7 @@ def render(
if destination_manifest.resolve() != manifest_path:
_atomic_write(destination_manifest, manifest_path.read_bytes())
checksums = _check_outputs(output_dir, expected, manifest_path)
report = _validation_report(manifest, checksums, font)
report = _validation_report(manifest, checksums, font, scientific_review)
report_path = output_dir / VALIDATION_NAME
_atomic_write(report_path, _json_bytes(report))
return report_path
@@ -1188,14 +1230,18 @@ def validate(
translations_dir: Path,
output_dir: Path,
font: str = DEFAULT_FONT,
scientific_review_report: Path | None = None,
) -> Path:
manifest_path = manifest_path.expanduser().resolve()
output_dir = output_dir.expanduser().resolve()
manifest = validate_manifest(manifest_path)
translations = load_translations(manifest, translations_dir)
scientific_review = _scientific_review_binding(
scientific_review_report, manifest_path, translations_dir, translations
)
expected = _expected_outputs(manifest, translations, font)
checksums = _check_outputs(output_dir, expected, manifest_path)
report = _validation_report(manifest, checksums, font)
report = _validation_report(manifest, checksums, font, scientific_review)
report_path = output_dir / VALIDATION_NAME
_atomic_write(report_path, _json_bytes(report))
return report_path
@@ -1243,6 +1289,11 @@ def _parser() -> argparse.ArgumentParser:
default=DEFAULT_FONT,
help="ASS font family (default: MiSans; subtitle styles use weight 700/Bold)",
)
command.add_argument(
"--scientific-review-report",
type=Path,
help="checksum-bound scientific review report for the reviewed translations",
)
return parser
@@ -1272,12 +1323,20 @@ def main(argv: Sequence[str] | None = None) -> int:
payload = {"ok": True, **next_translation_batch(args.manifest)}
elif args.command == "render":
result = render(
args.manifest, args.translations_dir, args.output_dir, args.font
args.manifest,
args.translations_dir,
args.output_dir,
args.font,
args.scientific_review_report,
)
payload = {"ok": True, "validation": str(result)}
else:
result = validate(
args.manifest, args.translations_dir, args.output_dir, args.font
args.manifest,
args.translations_dir,
args.output_dir,
args.font,
args.scientific_review_report,
)
payload = {"ok": True, "validation": str(result)}
print(json.dumps(payload, ensure_ascii=False, sort_keys=True))