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) self.assertNotIn(items[2]["translation"], 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()