feat: add scientific subtitle review workflow
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user