feat: add scientific subtitle review workflow
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parents[1] / "scripts"
|
||||
|
||||
|
||||
def load(name: str, filename: str):
|
||||
spec = importlib.util.spec_from_file_location(name, SCRIPT_DIR / filename)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
pipeline = load("review_test_pipeline", "subtitle_pipeline.py")
|
||||
review = load("scientific_review", "scientific_review.py")
|
||||
|
||||
|
||||
class ScientificReviewTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temporary.name)
|
||||
source = self.root / "source.srt"
|
||||
source.write_text(
|
||||
"1\n00:00:00,000 --> 00:00:01,000\nInject 50 microliters.\n\n"
|
||||
"2\n00:00:01,100 --> 00:00:02,000\nExpose the superior sclera.\n\n"
|
||||
"3\n00:00:02,100 --> 00:00:03,000\nAAV8-GFP expression was detected.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.manifest_path = pipeline.prepare(source, self.root / "subtitles", "en")
|
||||
self.manifest = json.loads(self.manifest_path.read_text(encoding="utf-8"))
|
||||
self.translations = self.root / "translations"
|
||||
self.translations.mkdir()
|
||||
records = [
|
||||
{"id": self.manifest["segments"][0]["id"], "translation": "注射50微升"},
|
||||
{"id": self.manifest["segments"][1]["id"], "translation": "暴露上方巩膜组织"},
|
||||
{"id": self.manifest["segments"][2]["id"], "translation": "检测到AAV8-GFP表达"},
|
||||
]
|
||||
(self.translations / "batch-0001.json").write_text(
|
||||
json.dumps({"translations": records}, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
self.review_dir = self.root / "subtitles" / "scientific-review"
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temporary.cleanup()
|
||||
|
||||
def write_profile(self) -> None:
|
||||
self.review_dir.mkdir(parents=True, exist_ok=True)
|
||||
(self.review_dir / review.PROFILE_NAME).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"domains": [
|
||||
{
|
||||
"id": "experimental-animal-science",
|
||||
"label": "实验动物学",
|
||||
"relevance": "primary",
|
||||
}
|
||||
],
|
||||
"review_focus": ["剂量、解剖方向和载体名称"],
|
||||
"terminology": [
|
||||
{
|
||||
"source": "superior sclera",
|
||||
"preferred": "上方巩膜",
|
||||
"category": "anatomy",
|
||||
"note": "保持方向信息",
|
||||
}
|
||||
],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def test_profile_review_finalize_and_bound_render(self) -> None:
|
||||
first = review.next_batch(self.manifest_path, self.translations, self.review_dir)
|
||||
self.assertEqual(first["stage"], "domain_profile_required")
|
||||
self.assertLessEqual(len(first["profile"]["representative_samples"]), 18)
|
||||
self.write_profile()
|
||||
|
||||
pending = review.next_batch(self.manifest_path, self.translations, self.review_dir)
|
||||
self.assertEqual(pending["stage"], "scientific_review_required")
|
||||
items = pending["batch"]["items"]
|
||||
records = [
|
||||
{
|
||||
"id": items[0]["id"],
|
||||
"status": "approved",
|
||||
"translation": items[0]["translation"],
|
||||
"severity": "none",
|
||||
"category": "none",
|
||||
"reason": "",
|
||||
},
|
||||
{
|
||||
"id": items[1]["id"],
|
||||
"status": "corrected",
|
||||
"translation": "暴露上方巩膜",
|
||||
"severity": "low",
|
||||
"category": "anatomy",
|
||||
"reason": "删除原文没有的“组织”",
|
||||
},
|
||||
{
|
||||
"id": items[2]["id"],
|
||||
"status": "flagged",
|
||||
"translation": items[2]["translation"],
|
||||
"severity": "high",
|
||||
"category": "gene_protein_vector",
|
||||
"reason": "载体名称需保守保留,无法仅凭字幕确认",
|
||||
},
|
||||
]
|
||||
Path(pending["output_path"]).write_text(
|
||||
json.dumps({"reviews": records}, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
complete = review.next_batch(self.manifest_path, self.translations, self.review_dir)
|
||||
self.assertTrue(complete["done"])
|
||||
finalized = review.finalize(self.manifest_path, self.translations, self.review_dir)
|
||||
self.assertEqual(finalized["counts"]["corrected"], 1)
|
||||
self.assertEqual(finalized["counts"]["unresolved_high"], 1)
|
||||
|
||||
validation_path = pipeline.render(
|
||||
self.manifest_path,
|
||||
Path(finalized["reviewed_translations_dir"]),
|
||||
self.root / "rendered",
|
||||
scientific_review_report=Path(finalized["report"]),
|
||||
)
|
||||
validation = json.loads(validation_path.read_text(encoding="utf-8"))
|
||||
self.assertTrue(validation["translation_quality_reviewed"])
|
||||
self.assertFalse(validation["scientific_review"]["human_expert_reviewed"])
|
||||
target = (self.root / "rendered" / "zh-CN.srt").read_text(encoding="utf-8")
|
||||
self.assertIn("暴露上方巩膜", target)
|
||||
self.assertNotIn("暴露上方巩膜组织", target)
|
||||
|
||||
def test_correction_cannot_change_protected_number_or_scientific_name(self) -> None:
|
||||
self.write_profile()
|
||||
pending = review.next_batch(self.manifest_path, self.translations, self.review_dir)
|
||||
items = pending["batch"]["items"]
|
||||
records = []
|
||||
for item in items:
|
||||
translation = item["translation"]
|
||||
status = "approved"
|
||||
severity = "none"
|
||||
category = "none"
|
||||
reason = ""
|
||||
if "50" in translation:
|
||||
translation = translation.replace("50", "500")
|
||||
status = "corrected"
|
||||
severity = "high"
|
||||
category = "number_unit"
|
||||
reason = "unsafe numerical rewrite"
|
||||
records.append(
|
||||
{
|
||||
"id": item["id"],
|
||||
"status": status,
|
||||
"translation": translation,
|
||||
"severity": severity,
|
||||
"category": category,
|
||||
"reason": reason,
|
||||
}
|
||||
)
|
||||
output = Path(pending["output_path"])
|
||||
output.write_text(json.dumps({"reviews": records}, ensure_ascii=False), encoding="utf-8")
|
||||
with self.assertRaisesRegex(review.ReviewError, "protected numbers"):
|
||||
review.next_batch(self.manifest_path, self.translations, self.review_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user