Initial MaterialSub release

Derived from pengchujin/jzsub at 222a90265d2a8797ca258eb1a980cee0863a8311; preserve the upstream MIT license and attribution.
This commit is contained in:
2026-08-25 13:07:03 +08:00
commit e27cc22cfb
24 changed files with 7620 additions and 0 deletions
@@ -0,0 +1,333 @@
from __future__ import annotations
import hashlib
import importlib.util
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest import mock
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "burn_subtitles.py"
SPEC = importlib.util.spec_from_file_location("burn_subtitles", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
burn = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(burn)
class BurnSubtitleValidationTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
self.subtitle = self.root / "bilingual.ass"
self.subtitle.write_bytes(b"[Script Info]\nTitle: test\n")
def tearDown(self) -> None:
self.temporary.cleanup()
def report(self, **overrides: object) -> dict[str, object]:
value: dict[str, object] = {
"structurally_valid": True,
"validation_scope": "structural_source_integrity",
"segment_count": 2,
"translation_count": 2,
"outputs": {
"bilingual.ass": hashlib.sha256(self.subtitle.read_bytes()).hexdigest()
},
}
value.update(overrides)
return value
def write_report(self, value: dict[str, object] | None = None) -> Path:
path = self.root / "validation.json"
path.write_text(
json.dumps(value if value is not None else self.report()),
encoding="utf-8",
)
return path
def test_accepts_matching_structural_validation_report(self) -> None:
self.write_report()
validated = burn._validate_validation_report(
self.subtitle, self.root / "validation.json"
)
self.assertEqual(validated["segment_count"], 2)
self.assertTrue(validated["structurally_valid"])
def test_missing_validated_font_fails_closed_with_override(self) -> None:
report = self.report(font="MiSans")
with mock.patch.object(burn, "_font_installed", return_value=False):
with self.assertRaisesRegex(burn.BurnError, "MiSans.*not found"):
burn._require_subtitle_font(report, allow_missing_font=False)
burn._require_subtitle_font(report, allow_missing_font=True)
with mock.patch.object(burn, "_font_installed", return_value=True):
burn._require_subtitle_font(report, allow_missing_font=False)
with mock.patch.object(burn, "_font_installed", return_value=None):
burn._require_subtitle_font(report, allow_missing_font=False)
def test_reports_without_a_font_skip_the_font_gate(self) -> None:
with mock.patch.object(burn, "_font_installed") as detect:
burn._require_subtitle_font(self.report(), allow_missing_font=False)
detect.assert_not_called()
def test_rejects_stale_ass_checksum(self) -> None:
report = self.write_report()
self.subtitle.write_bytes(self.subtitle.read_bytes() + b"stale")
with self.assertRaisesRegex(burn.BurnError, "bilingual.ass.*SHA-256"):
burn._validate_validation_report(self.subtitle, report)
def test_rejects_missing_structural_report(self) -> None:
missing = self.root / "missing-validation.json"
with self.assertRaisesRegex(burn.BurnError, "validation report.*does not exist"):
burn._validate_validation_report(self.subtitle, missing)
def test_rejects_report_without_structural_approval(self) -> None:
report = self.write_report(self.report(structurally_valid=False))
with self.assertRaisesRegex(burn.BurnError, "structurally_valid=true"):
burn._validate_validation_report(self.subtitle, report)
def test_rejects_wrong_validation_scope(self) -> None:
report = self.write_report(self.report(validation_scope="translation_review"))
with self.assertRaisesRegex(burn.BurnError, "structural_source_integrity"):
burn._validate_validation_report(self.subtitle, report)
def test_cli_accepts_validation_report_override(self) -> None:
args = burn._parser().parse_args(
[
"input.mp4",
"bilingual.ass",
"output.mp4",
"--validation-report",
"reviewed.json",
]
)
self.assertEqual(args.validation_report, Path("reviewed.json"))
def test_cli_accepts_citation_file(self) -> None:
args = burn._parser().parse_args(
["input.mp4", "bilingual.ass", "output.mp4", "--citation-file", "citation.txt"]
)
self.assertEqual(args.citation_file, Path("citation.txt"))
def test_citation_ass_is_top_left_and_spans_video(self) -> None:
rendered = burn._render_citation_ass(
"内容引自网络,仅供内部交流\n\n"
"Huang, C., Jiang, N., Zheng, X., Gu, H., Zhang, L., Ou, S.\n"
"A Protocol for Harvesting Single-cell Suspension from Mouse Corneas.\n"
"J. Vis. Exp. (230), e69844, doi:10.3791/69844 (2026).",
1920,
1080,
61.23,
)
self.assertIn("Style: Citation,Arial,24", rendered)
self.assertIn(r"\pos(40,30)\p1", rendered)
self.assertIn("m 0 0 l 720 0 720 132 0 132", rendered)
self.assertIn(r"\fs20\1c&HCCCCCC&", rendered)
self.assertIn("内容引自网络,仅供内部交流", rendered)
self.assertIn(r"\fs24\1c&HFFFFFF&", rendered)
self.assertEqual(rendered.count("Dialogue:"), 2)
self.assertIn("Dialogue: 10,0:00:00.00,0:01:01.23", rendered)
self.assertIn("doi:10.3791/69844", rendered)
def test_citation_ass_guards_untrusted_override_sequences(self) -> None:
rendered = burn._render_citation_ass(
r"Title {\pos(1,1)} and literal \N text",
1920,
1080,
5.0,
)
dialogue = rendered.rsplit("Dialogue: ", 1)[1]
self.assertNotIn(r"{\pos(1,1)}", dialogue)
self.assertIn("\\" + burn.ASS_WORD_JOINER + "pos", dialogue)
self.assertIn("\\" + burn.ASS_WORD_JOINER + "N", dialogue)
def test_encode_command_layers_citation_in_same_video_filter(self) -> None:
citation_ass = self.root / "citation.ass"
command, _ = burn._encode_command(
"ffmpeg",
self.root / "input.mkv",
self.subtitle,
self.root / "output.mp4",
{"index": 0},
[],
force=False,
crf=18,
preset="slow",
encoder="libx264",
citation_ass=citation_ass,
)
video_filter = command[command.index("-vf") + 1]
self.assertEqual(video_filter.count("subtitles=filename="), 2)
self.assertIn(str(citation_ass), video_filter)
def test_citation_receipt_binds_output_and_text_hashes(self) -> None:
output = self.root / "output.mp4"
output.write_bytes(b"video")
citation_file = self.root / "citation.txt"
citation_file.write_text("Formal citation.\n", encoding="utf-8")
receipt = burn._write_citation_receipt(output, citation_file)
value = json.loads(receipt.read_text(encoding="utf-8"))
self.assertEqual(value["output_file"], output.name)
self.assertEqual(value["output_sha256"], hashlib.sha256(b"video").hexdigest())
self.assertEqual(value["citation_sha256"], hashlib.sha256(citation_file.read_bytes()).hexdigest())
self.assertEqual(value["position"], "top-left")
def test_selects_libass_capable_ffmpeg_full_when_path_build_lacks_it(self) -> None:
default = self.root / "bin" / "ffmpeg"
full = self.root / "opt" / "ffmpeg-full" / "bin" / "ffmpeg"
default.parent.mkdir(parents=True)
full.parent.mkdir(parents=True)
default.write_text("", encoding="utf-8")
full.write_text("", encoding="utf-8")
with mock.patch.object(
burn,
"_ffmpeg_has_subtitles_filter",
side_effect=lambda path: Path(path) == full,
):
selected = burn._select_libass_ffmpeg(str(default), candidates=[full])
self.assertEqual(selected, str(full))
def test_progress_bar_is_compact_and_human_readable(self) -> None:
line = burn._format_progress(50, 71.5, 143.0, "0.68x")
self.assertEqual(
line,
"烧录 [██████████░░░░░░░░░░] 50% 01:11 / 02:23 0.68x",
)
self.assertLess(len(line), 80)
def test_encode_command_uses_machine_readable_quiet_progress(self) -> None:
command, _ = burn._encode_command(
"ffmpeg",
self.root / "input.mkv",
self.subtitle,
self.root / "output.mp4",
{"index": 0},
[],
force=False,
crf=18,
preset="slow",
encoder="libx264",
)
self.assertIn("-nostats", command)
self.assertEqual(command[command.index("-loglevel") + 1], "error")
self.assertEqual(command[command.index("-progress") + 1], "pipe:1")
def test_rejects_output_duration_mismatch(self) -> None:
input_video = {
"codec_type": "video",
"width": 320,
"height": 180,
"avg_frame_rate": "24/1",
}
output_probe = {
"format": {"format_name": "mov,mp4,m4a,3gp,3g2,mj2", "duration": "8.9"},
"streams": [
{
"codec_type": "video",
"codec_name": "h264",
"width": 320,
"height": 180,
"avg_frame_rate": "24/1",
"duration": "8.9",
}
],
}
with mock.patch.object(burn, "_probe", return_value=output_probe):
with self.assertRaisesRegex(burn.BurnError, "duration changed"):
burn._verify_output(
"ffprobe",
self.root / "output.mp4",
input_video,
False,
input_duration=10.0,
)
def test_end_to_end_burn_layers_subtitles_and_citation_with_receipt(self) -> None:
try:
ffmpeg, _ = burn._required_executables()
burn._require_libass_subtitles_filter(ffmpeg)
except burn.BurnError as exc:
self.skipTest(str(exc))
source = self.root / "source.mp4"
generated = subprocess.run(
[
ffmpeg,
"-hide_banner",
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
"color=c=blue:s=320x180:d=1:r=24",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
"-y",
str(source),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if generated.returncode != 0:
self.skipTest(f"could not generate integration fixture: {generated.stderr[-300:]}")
self.subtitle.write_text(
"[Script Info]\nScriptType: v4.00+\nPlayResX: 320\nPlayResY: 180\n\n"
"[V4+ Styles]\n"
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, "
"BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, "
"BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
"Style: Default,Arial,18,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,"
"100,100,0,0,1,1,0,2,10,10,10,1\n\n"
"[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
"Dialogue: 0,0:00:00.00,0:00:01.00,Default,,0,0,0,,Hello\n",
encoding="utf-8",
)
self.write_report()
citation_file = self.root / "citation.txt"
citation_file.write_text(
"内容引自网络,仅供内部交流\n\n"
"Authors.\nArticle title.\nJournal. doi:10.3791/test.\n",
encoding="utf-8",
)
output = self.root / "burned.mp4"
burn.burn_subtitles(
source,
self.subtitle,
output,
preset="ultrafast",
citation_file=citation_file,
)
self.assertGreater(output.stat().st_size, 0)
self.assertTrue(output.with_suffix(".mp4.citation.json").is_file())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,119 @@
from __future__ import annotations
import hashlib
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "citation_watermark.py"
SPEC = importlib.util.spec_from_file_location("citation_watermark", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
citation = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(citation)
class CitationWatermarkTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
self.manifest = self.root / "download-manifest.json"
self.manifest.write_text(
json.dumps(
{
"source": {
"url": "https://www.jove.com/v/69844/a-protocol-for-harvesting-single-cell-suspension-from-mouse-corneas"
}
}
),
encoding="utf-8",
)
def tearDown(self) -> None:
self.temporary.cleanup()
def test_attaches_exact_normalized_citation_without_url_by_default(self) -> None:
path = citation.attach_citation(
self.manifest,
"Huang, C., Jiang, N. A Protocol. J. Vis. Exp. doi:10.3791/69844 (2026).",
)
self.assertEqual(
path.read_text(encoding="utf-8"),
"Huang, C., Jiang, N. A Protocol. J. Vis. Exp. doi:10.3791/69844 (2026).\n",
)
manifest = json.loads(self.manifest.read_text(encoding="utf-8"))
record = manifest["citation_watermark"]
self.assertFalse(record["include_source_url"])
self.assertEqual(record["position"], "top-left")
self.assertEqual(record["citation_sha256"], hashlib.sha256(path.read_bytes()).hexdigest())
def test_optional_source_url_uses_manifest_canonical_url(self) -> None:
path = citation.attach_citation(
self.manifest,
"Formal citation.",
include_source_url=True,
)
self.assertIn("\nSource: https://www.jove.com/v/69844/", path.read_text(encoding="utf-8"))
def test_structured_citation_and_internal_notice_preserve_four_line_layout(self) -> None:
citation_text = (
"Huang, C., Jiang, N., Zheng, X., Gu, H., Zhang, L., Ou, S.\n"
"A Protocol for Harvesting Single-cell Suspension from Mouse Corneas.\n"
"J. Vis. Exp. (230), e69844, doi:10.3791/69844 (2026)."
)
path = citation.attach_citation(
self.manifest,
citation_text,
notice=citation.DEFAULT_INTERNAL_NOTICE,
)
self.assertEqual(
path.read_text(encoding="utf-8"),
"内容引自网络,仅供内部交流\n\n" + citation_text + "\n",
)
manifest = json.loads(self.manifest.read_text(encoding="utf-8"))
record = manifest["citation_watermark"]
self.assertEqual(record["layout"], "notice-plus-three-line-citation")
self.assertEqual(record["notice"], citation.DEFAULT_INTERNAL_NOTICE)
def test_cli_accepts_three_structured_fields_and_default_notice(self) -> None:
result = citation.main(
[
str(self.manifest),
"--authors",
"Authors",
"--title",
"Title",
"--publication",
"Journal. doi:test.",
"--notice",
]
)
self.assertEqual(result, 0)
self.assertEqual(
(self.root / "citation-watermark.txt").read_text(encoding="utf-8"),
"内容引自网络,仅供内部交流\n\nAuthors\nTitle\nJournal. doi:test.\n",
)
def test_refuses_signed_or_credential_like_source_url(self) -> None:
self.manifest.write_text(
json.dumps({"source": {"url": "https://cdn.example/video?token=secret"}}),
encoding="utf-8",
)
with self.assertRaisesRegex(citation.CitationError, "signed or credential"):
citation.attach_citation(
self.manifest,
"Formal citation.",
include_source_url=True,
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,214 @@
from __future__ import annotations
import importlib.util
import argparse
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import threading
import unittest
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "ingest_embedded_hls.py"
sys.path.insert(0, str(SCRIPT.parent))
SPEC = importlib.util.spec_from_file_location("ingest_embedded_hls", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
ingest = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(ingest)
class EmbeddedHlsTests(unittest.TestCase):
def test_resource_map_must_be_private(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "resources.json"
path.write_text(json.dumps({"schema_version": 1}), encoding="utf-8")
os.chmod(path, 0o644)
with self.assertRaisesRegex(ingest.IngestError, "chmod 600"):
ingest._read_private_resource_map(path)
os.chmod(path, 0o600)
self.assertEqual(
ingest._read_private_resource_map(path), {"schema_version": 1}
)
def test_private_map_can_be_the_only_job_entry_when_cleanup_is_enabled(self) -> None:
with tempfile.TemporaryDirectory() as directory:
output = Path(directory)
resource_map = output / "resource-map.json"
resource_map.write_text('{"schema_version": 1}', encoding="utf-8")
os.chmod(resource_map, 0o600)
self.assertEqual(
ingest._prepare_output_dir(
output,
resource_map,
resume=False,
cleanup_resource_map=True,
),
output,
)
with self.assertRaisesRegex(ingest.IngestError, "not empty"):
ingest._prepare_output_dir(
output,
resource_map,
resume=False,
cleanup_resource_map=False,
)
def test_authorization_query_only_propagates_to_same_origin(self) -> None:
root = "https://cdn.example/media/video.m3u8?Policy=secret&Key=pair"
same = ingest._authorized_resource_url(
"https://cdn.example/media/segment.ts?part=1", root
)
foreign = ingest._authorized_resource_url(
"https://captions.example/subtitle.vtt", root
)
self.assertIn("part=1", same)
self.assertIn("Policy=secret", same)
self.assertIn("Key=pair", same)
self.assertEqual(foreign, "https://captions.example/subtitle.vtt")
def test_rewrites_segments_and_aes128_key(self) -> None:
root = "https://cdn.example/hls/video.m3u8?Policy=secret"
source = """#EXTM3U
#EXT-X-KEY:METHOD=AES-128,URI="video.key"
#EXTINF:10,
segment-0.ts
#EXT-X-ENDLIST
"""
rewritten = ingest.rewrite_media_playlist(source, root)
self.assertIn(
'URI="https://cdn.example/hls/video.key?Policy=secret"', rewritten
)
self.assertIn(
"https://cdn.example/hls/segment-0.ts?Policy=secret", rewritten
)
def test_rejects_master_playlist(self) -> None:
source = """#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1000000
high.m3u8
"""
with self.assertRaisesRegex(ingest.IngestError, "master playlist"):
ingest.rewrite_media_playlist(source, "https://cdn.example/master.m3u8")
def test_rejects_drm_protection(self) -> None:
source = """#EXTM3U
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="key",KEYFORMAT="com.apple.streamingkeydelivery"
#EXTINF:10,
segment.ts
"""
with self.assertRaisesRegex(ingest.IngestError, "will not bypass DRM"):
ingest.rewrite_media_playlist(source, "https://cdn.example/video.m3u8")
@unittest.skipUnless(
shutil.which("ffmpeg") and shutil.which("ffprobe") and shutil.which("yt-dlp"),
"FFmpeg and yt-dlp are required for the local integration test",
)
def test_local_media_playlist_reaches_standard_translation_stage(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "source"
output = root / "output"
source.mkdir()
output.mkdir()
subprocess.run(
[
shutil.which("ffmpeg") or "ffmpeg",
"-hide_banner",
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
"color=c=blue:s=320x180:r=24:d=2",
"-f",
"lavfi",
"-i",
"sine=frequency=440:duration=2",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
"-c:a",
"aac",
"-hls_time",
"1",
"-hls_playlist_type",
"vod",
str(source / "media.m3u8"),
],
check=True,
)
(source / "captions.vtt").write_text(
"WEBVTT\n\n00:00:00.000 --> 00:00:01.500\nHello from the test video.\n",
encoding="utf-8",
)
handler = partial(SimpleHTTPRequestHandler, directory=str(source))
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
server_thread.start()
try:
port = server.server_address[1]
resource_map = root / "resource-map.json"
resource_map.write_text(
json.dumps(
{
"schema_version": 1,
"page_url": f"http://127.0.0.1:{port}/page",
"playlist_url": f"http://127.0.0.1:{port}/media.m3u8?Policy=test",
"title": "Embedded test",
"id": "fixture",
"duration_seconds": 2,
"width": 320,
"height": 180,
"subtitle": {
"url": f"http://127.0.0.1:{port}/captions.vtt",
"language": "en",
"kind": "manual",
"label": "English",
"format": "vtt",
},
}
),
encoding="utf-8",
)
os.chmod(resource_map, 0o600)
args = argparse.Namespace(
resource_map=resource_map,
output_dir=output,
target_lang="zh-CN",
deliver="full",
concurrent_fragments=2,
mp4_fallback=False,
resume=False,
cleanup_resource_map=False,
)
exit_code = ingest.execute(args)
self.assertEqual(exit_code, 3)
manifest = json.loads((output / "download-manifest.json").read_text())
self.assertEqual(manifest["status"], "bilingual_required")
self.assertTrue((output / "subtitles" / "subtitle-manifest.json").is_file())
self.assertEqual(
manifest["authentication"]["mode"],
"browser-confirmed-resource-map",
)
finally:
server.shutdown()
server.server_close()
server_thread.join(timeout=5)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,55 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import sys
import unittest
from unittest import mock
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "preflight.py"
sys.path.insert(0, str(SCRIPT.parent))
SPEC = importlib.util.spec_from_file_location("materialsub_preflight", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
preflight = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(preflight)
class PreflightTests(unittest.TestCase):
def dependencies(self):
return mock.patch.object(
preflight.shutil,
"which",
side_effect=lambda name: f"/bin/{name}" if name != "deno" else None,
)
def test_missing_font_requires_early_decision(self) -> None:
with self.dependencies(), mock.patch.object(
preflight.burn, "_required_executables", return_value=("/bin/ffmpeg", "/bin/ffprobe")
), mock.patch.object(
preflight.burn, "_require_libass_subtitles_filter"
), mock.patch.object(preflight.burn, "_font_installed", return_value=False):
exit_code, result = preflight.assess(
font="MiSans", allow_missing_font=False, youtube=False
)
self.assertEqual(exit_code, 3)
self.assertEqual(result["stage"], "font_decision_required")
def test_missing_font_can_accept_substitution_upfront(self) -> None:
with self.dependencies(), mock.patch.object(
preflight.burn, "_required_executables", return_value=("/bin/ffmpeg", "/bin/ffprobe")
), mock.patch.object(
preflight.burn, "_require_libass_subtitles_filter"
), mock.patch.object(preflight.burn, "_font_installed", return_value=False):
exit_code, result = preflight.assess(
font="MiSans", allow_missing_font=True, youtube=True
)
self.assertEqual(exit_code, 0)
self.assertTrue(result["substitution_allowed"])
self.assertTrue(any("Deno" in item for item in result["warnings"]))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,681 @@
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "subtitle_pipeline.py"
SPEC = importlib.util.spec_from_file_location("subtitle_pipeline", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
pipeline = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(pipeline)
def srt(cues: list[tuple[str, str, str]], newline: str = "\n") -> bytes:
blocks = []
for index, (start, end, text) in enumerate(cues, start=1):
blocks.append(f"{index}{newline}{start} --> {end}{newline}{text}")
return (newline + newline).join(blocks).encode("utf-8") + newline.encode("ascii")
class SubtitlePipelineTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
def tearDown(self) -> None:
self.temporary.cleanup()
def prepare_fixture(
self,
raw: bytes,
*,
segment_mode: str = "preserve",
source_language: str = "en",
video_size: tuple[int, int] | None = None,
) -> tuple[Path, dict]:
source = self.root / "downloaded.srt"
source.write_bytes(raw)
manifest_path = pipeline.prepare(
source, self.root / "work", source_language, segment_mode, video_size
)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
return manifest_path, manifest
def write_translations(
self,
manifest: dict,
*,
records: list[dict] | None = None,
filename: str = "translations.json",
) -> Path:
directory = self.root / "translations"
directory.mkdir(exist_ok=True)
if records is None:
records = [
{
"id": segment["id"],
"translation": f"中文 {index}",
}
for index, segment in enumerate(manifest["segments"], start=1)
]
(directory / filename).write_text(
json.dumps({"translations": records}, ensure_ascii=False),
encoding="utf-8",
)
return directory
def test_compact_batches_omit_model_visible_hashes(self) -> None:
manifest_path, manifest = self.prepare_fixture(
srt([("00:00:00,000", "00:00:01,000", "Hello world")])
)
self.assertEqual(manifest["translation_contract_version"], 4)
batch = json.loads(
Path(manifest["translation_batches"][0]["path"]).read_text(encoding="utf-8")
)
self.assertEqual(set(batch["items"][0]), {"id", "source"})
self.assertNotIn("source_sha256", json.dumps(batch))
self.assertEqual(batch["output_fields"], ["id", "translation"])
translations_dir = self.write_translations(manifest)
pipeline.render(manifest_path, translations_dir, self.root / "output")
def test_short_subtitles_fit_one_translation_batch(self) -> None:
cues = [
(f"00:00:{index:02d},000", f"00:00:{index:02d},900", f"Line {index}")
for index in range(25)
]
manifest_path, manifest = self.prepare_fixture(srt(cues))
first = pipeline.next_translation_batch(manifest_path)
self.assertFalse(first["done"])
self.assertEqual(first["remaining"], 1)
self.assertEqual(len(first["batch"]["items"]), 25)
self.assertEqual(first["batch"]["context"], {"before": [], "after": []})
self.assertNotIn("segments", first)
self.assertNotIn("cues", first)
self.assertNotIn("source_sha256", json.dumps(first))
output = Path(first["output_path"])
output.write_text(
json.dumps(
{"translations": [
{"id": item["id"], "translation": "中文"}
for item in first["batch"]["items"]
]},
ensure_ascii=False,
),
encoding="utf-8",
)
complete = pipeline.next_translation_batch(manifest_path)
self.assertTrue(complete["done"])
self.assertEqual(complete["remaining"], 0)
def test_long_subtitles_split_into_bounded_context_linked_batches(self) -> None:
total = pipeline.TRANSLATION_BATCH_SIZE * 2 + 5
cues = [
(
f"{index // 3600:02d}:{index // 60 % 60:02d}:{index % 60:02d},000",
f"{index // 3600:02d}:{index // 60 % 60:02d}:{index % 60:02d},900",
f"Line {index}",
)
for index in range(total)
]
manifest_path, manifest = self.prepare_fixture(srt(cues))
batches = manifest["translation_batches"]
self.assertEqual(len(batches), 3)
segment_ids = [segment["id"] for segment in manifest["segments"]]
batched = [batch_id for batch in batches for batch_id in batch["segment_ids"]]
self.assertEqual(batched, segment_ids)
second = json.loads(Path(batches[1]["path"]).read_text(encoding="utf-8"))
size = pipeline.TRANSLATION_BATCH_SIZE
context_span = pipeline.TRANSLATION_CONTEXT_SEGMENTS
self.assertEqual(len(second["items"]), size)
self.assertEqual(
[item["id"] for item in second["context"]["before"]],
segment_ids[size - context_span : size],
)
self.assertEqual(
[item["id"] for item in second["context"]["after"]],
segment_ids[2 * size : 2 * size + context_span],
)
remaining = len(batches)
while True:
pending = pipeline.next_translation_batch(manifest_path)
if pending["done"]:
break
self.assertEqual(pending["remaining"], remaining)
Path(pending["output_path"]).write_text(
json.dumps(
{"translations": [
{"id": item["id"], "translation": "中文"}
for item in pending["batch"]["items"]
]},
ensure_ascii=False,
),
encoding="utf-8",
)
remaining -= 1
self.assertEqual(remaining, 0)
pipeline.load_translations(manifest, self.root / "work" / "translation-output")
def test_ass_stacks_source_above_chinese_at_the_bottom(self) -> None:
manifest_path, manifest = self.prepare_fixture(
srt([
("00:00:00,000", "00:00:02,000", "Short"),
(
"00:00:02,100",
"00:00:05,000",
"A much longer source caption that wraps onto another display line while preserving its exact text",
),
])
)
translations_dir = self.write_translations(
manifest,
records=[
{"id": manifest["segments"][0]["id"], "translation": "短句"},
{"id": manifest["segments"][1]["id"], "translation": "这是一条会换行的较长中文字幕 用来验证位置固定"},
],
)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
rendered = output_dir.joinpath("bilingual.ass").read_text(encoding="utf-8")
text_events = [line for line in rendered.splitlines() if ",Bilingual," in line]
box_events = [line for line in rendered.splitlines() if ",BilingualBox," in line]
self.assertEqual(len(text_events), 2)
self.assertEqual(len(box_events), 2)
# One bottom-anchored stack: source at fs42 above Chinese at fs46.
for line in text_events + box_events:
self.assertIn(r"{\an2\pos(960,1030)\fs42}", line)
for line in text_events:
self.assertIn(r"\N{\fs46\1c&H00FFFF&}", line)
for line in box_events:
self.assertIn(r"\N{\fs46}", line)
self.assertNotIn(r"\1c", line)
def test_portrait_video_gets_matching_playres_and_narrower_wrapping(self) -> None:
manifest_path, manifest = self.prepare_fixture(
srt([
(
"00:00:00,000",
"00:00:03,000",
"A long landscape-width caption that must wrap much earlier on a portrait video",
)
]),
video_size=(1080, 1920),
)
self.assertEqual(manifest["video_size"], {"width": 1080, "height": 1920})
translations_dir = self.write_translations(
manifest,
records=[
{
"id": manifest["segments"][0]["id"],
"translation": "竖屏视频中的中文字幕必须按较窄的宽度换行",
}
],
)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
rendered = output_dir.joinpath("bilingual.ass").read_text(encoding="utf-8")
layout = pipeline._ass_layout(manifest)
self.assertEqual(layout["play_res_x"], round(1080 * 1080 / 1920))
self.assertEqual(layout["source_font_size"], 36)
self.assertEqual(layout["target_font_size"], 40)
self.assertEqual(layout["bottom_margin"], 120)
self.assertEqual(layout["position_y"], 960)
self.assertLess(layout["target_columns"], pipeline.TARGET_WRAP_COLUMNS)
self.assertLess(layout["source_columns"], pipeline.SOURCE_WRAP_COLUMNS)
self.assertIn(f"PlayResX: {layout['play_res_x']}", rendered)
self.assertIn("PlayResY: 1080", rendered)
self.assertIn(r"{\an2\pos(304,960)\fs36}", rendered)
self.assertIn(r"\N{\fs40\1c&H00FFFF&}", rendered)
chinese_srt = output_dir.joinpath("zh-CN.srt").read_text(encoding="utf-8")
chinese_lines = [line for line in chinese_srt.splitlines()[2:] if line]
self.assertGreater(len(chinese_lines), 1)
def clear_translations(self) -> None:
directory = self.root / "translations"
if directory.exists():
for path in directory.glob("*.json"):
path.unlink()
def test_raw_archive_and_source_text_are_exactly_preserved(self) -> None:
raw = b"\xef\xbb\xbf" + srt(
[
("00:00:00,100", "00:00:01,500", " Café & co. "),
("00:00:01,700", "00:00:03,000", "Line one\r\nLine two"),
],
newline="\r\n",
)
manifest_path, manifest = self.prepare_fixture(raw)
archive = Path(manifest["source"]["archive_path"])
self.assertEqual(archive.read_bytes(), raw)
self.assertTrue((self.root / "work" / "translation-output").is_dir())
self.assertEqual(manifest["cues"][0]["text"], " Café & co. ")
self.assertEqual(manifest["cues"][1]["text"], "Line one\nLine two")
original_path = Path(manifest["source"]["original_path"])
original_path.write_bytes(
srt([("00:00:00,000", "00:00:01,000", "Different source")])
)
with self.assertRaisesRegex(pipeline.PipelineError, "write-once source archive"):
pipeline.prepare(original_path, self.root / "work", "en")
self.assertEqual(archive.read_bytes(), raw)
batch_path = Path(manifest["translation_batches"][0]["path"])
batch = json.loads(batch_path.read_text(encoding="utf-8"))
self.assertEqual(batch["items"][0]["source"], " Café & co. ")
self.assertEqual(batch["output_fields"], ["id", "translation"])
self.assertNotIn("source", batch["output_fields"])
translations_dir = self.write_translations(manifest)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
pipeline.validate(manifest_path, translations_dir, output_dir)
source_output = output_dir.joinpath("source.srt").read_text(encoding="utf-8")
self.assertIn(" Café & co. ", source_output)
self.assertIn("Line one\nLine two", source_output)
report = json.loads(output_dir.joinpath("validation.json").read_text())
self.assertTrue(report["structurally_valid"])
self.assertEqual(report["validation_scope"], "structural_source_integrity")
self.assertEqual(report["font"], "MiSans")
self.assertEqual(report["font_weight"], 700)
self.assertFalse(report["translation_quality_reviewed"])
self.assertTrue(report["invariants"]["raw_source_sha256_locked"])
def test_ass_malicious_text_is_losslessly_escaped(self) -> None:
malicious = r"Literal {\pos(10,20)} \N \n \h } { 中文"
raw = srt([("00:00:00,000", "00:00:02,000", malicious)])
manifest_path, manifest = self.prepare_fixture(raw)
segment = manifest["segments"][0]
translation = r"中文 {\move(0,0,9,9)} \N"
translations_dir = self.write_translations(
manifest,
records=[
{
"id": segment["id"],
"source_sha256": segment["source_sha256"],
"translation": translation,
}
],
)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
self.assertEqual(
pipeline.ass_unescape_for_validation(pipeline.ass_escape(malicious)), malicious
)
self.assertEqual(
pipeline.ass_unescape_for_validation(pipeline.ass_escape(translation)), translation
)
rendered = output_dir.joinpath("bilingual.ass").read_text(encoding="utf-8")
self.assertNotIn(r"{\pos(10,20)}", rendered)
self.assertNotIn(r"{\move(0,0,9,9)}", rendered)
pipeline.validate(manifest_path, translations_dir, output_dir)
def test_ass_ffmpeg_guards_round_trip_all_reserved_sequences(self) -> None:
word_joiner = pipeline.ASS_WORD_JOINER
cases = [
r"{\pos(1,2)}\N",
r"literal \N, \n, and \h",
"opening { and closing } braces",
f"existing{word_joiner}word-joiner",
"\\" + word_joiner + "N",
"line one\nline two",
"Unicode 中文 👩\u200d🚀 e\u0301",
]
for source in cases:
with self.subTest(source=source):
encoded = pipeline.ass_escape(source)
self.assertEqual(pipeline.ass_unescape_for_validation(encoded), source)
self.assertEqual(pipeline.ass_escape("\\N"), "\\" + word_joiner + "N")
self.assertEqual(pipeline.ass_escape("\\n"), "\\" + word_joiner + "n")
self.assertEqual(pipeline.ass_escape("\\h"), "\\" + word_joiner + "h")
self.assertEqual(pipeline.ass_escape("{"), r"\{{}")
self.assertEqual(pipeline.ass_escape("\n"), r"\N")
with self.assertRaisesRegex(pipeline.PipelineError, "unguarded backslash"):
pipeline.ass_unescape_for_validation(r"\h")
def test_translation_contract_rejects_missing_duplicate_and_hash_mismatch(self) -> None:
raw = srt(
[
("00:00:00,000", "00:00:01,000", "One"),
("00:00:01,100", "00:00:02,000", "Two"),
]
)
_, manifest = self.prepare_fixture(raw)
first, second = manifest["segments"]
translations_dir = self.write_translations(
manifest,
records=[
{
"id": first["id"],
"source_sha256": first["source_sha256"],
"translation": "",
}
],
)
with self.assertRaisesRegex(pipeline.PipelineError, "missing translations"):
pipeline.load_translations(manifest, translations_dir)
self.clear_translations()
valid_first = {
"id": first["id"],
"source_sha256": first["source_sha256"],
"translation": "",
}
self.write_translations(manifest, records=[valid_first], filename="a.json")
self.write_translations(manifest, records=[valid_first], filename="b.json")
with self.assertRaisesRegex(pipeline.PipelineError, "duplicate translation ID"):
pipeline.load_translations(manifest, translations_dir)
self.clear_translations()
records = [
{**valid_first, "source_sha256": "0" * 64},
{
"id": second["id"],
"source_sha256": second["source_sha256"],
"translation": "",
},
]
self.write_translations(manifest, records=records)
with self.assertRaisesRegex(pipeline.PipelineError, "source SHA-256 mismatch"):
pipeline.load_translations(manifest, translations_dir)
def test_translation_contract_rejects_editable_source_and_controls(self) -> None:
manifest_path, manifest = self.prepare_fixture(
srt([("00:00:00,000", "00:00:01,000", "Source")])
)
del manifest_path
segment = manifest["segments"][0]
forbidden = {
"id": segment["id"],
"source_sha256": segment["source_sha256"],
"source": "rewritten",
"translation": "中文",
}
translations_dir = self.write_translations(manifest, records=[forbidden])
with self.assertRaisesRegex(pipeline.PipelineError, "forbidden/missing fields"):
pipeline.load_translations(manifest, translations_dir)
self.clear_translations()
controlled = {
"id": segment["id"],
"source_sha256": segment["source_sha256"],
"translation": "\n",
}
self.write_translations(manifest, records=[controlled])
with self.assertRaisesRegex(pipeline.PipelineError, "control character"):
pipeline.load_translations(manifest, translations_dir)
def test_unicode_layout_inserts_breaks_without_changing_source(self) -> None:
text = "👩\u200d🚀e\u0301 العربية 中文🙂 and-more-text"
chunks = pipeline.wrap_layout_chunks(text, 6)
self.assertEqual("".join(chunks), text)
self.assertTrue(any("👩\u200d🚀" in chunk for chunk in chunks))
manifest_path, manifest = self.prepare_fixture(
srt([("00:00:00,000", "00:00:03,000", text)]),
source_language="ar",
)
translations_dir = self.write_translations(manifest)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir, "Noto Sans")
pipeline.validate(manifest_path, translations_dir, output_dir, "Noto Sans")
def test_chinese_house_style_and_measured_background(self) -> None:
source = "This English subtitle is intentionally longer than forty-two columns but should remain on one display line"
manifest_path, manifest = self.prepare_fixture(
srt([("00:00:00,000", "00:00:03,000", source)])
)
segment = manifest["segments"][0]
translations_dir = self.write_translations(
manifest,
records=[
{
"id": segment["id"],
"source_sha256": segment["source_sha256"],
"translation": "你好,世界。",
}
],
)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
self.assertEqual(pipeline.normalize_target_caption("你好,世界。", "zh-CN"), "你好 世界")
self.assertEqual(pipeline.normalize_target_caption("版本 5.6,发布。", "zh-CN"), "版本 5.6 发布")
chinese_srt = output_dir.joinpath("zh-CN.srt").read_text(encoding="utf-8")
self.assertIn("你好 世界", chinese_srt)
self.assertNotIn("", chinese_srt)
self.assertNotIn("", chinese_srt)
source_srt = output_dir.joinpath("source.srt").read_text(encoding="utf-8")
source_lines = source_srt.splitlines()[2:]
self.assertGreater(max(map(len, source_lines)), 42)
self.assertLessEqual(max(map(len, source_lines)), pipeline.SOURCE_WRAP_COLUMNS)
rendered = output_dir.joinpath("bilingual.ass").read_text(encoding="utf-8")
self.assertIn("Style: Bilingual,MiSans,46", rendered)
self.assertIn("Style: BilingualBox,MiSans,46", rendered)
self.assertIn(",4,8,0,2,80,80,50,1", rendered)
self.assertIn("Dialogue: 0,", rendered)
self.assertNotIn(r"{\an7\p1}", rendered)
self.assertIn("Dialogue: 1,", rendered)
pipeline.validate(manifest_path, translations_dir, output_dir)
def test_background_uses_identical_text_layout_for_libass_measurement(self) -> None:
source = "日本語の字幅は Latin text と同じではありません"
manifest_path, manifest = self.prepare_fixture(
srt([("00:00:00,000", "00:00:03,000", source)]),
source_language="ja",
)
segment = manifest["segments"][0]
translations_dir = self.write_translations(
manifest,
records=[
{
"id": segment["id"],
"source_sha256": segment["source_sha256"],
"translation": "日文字形宽度与拉丁文字不同",
}
],
)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
rendered = output_dir.joinpath("bilingual.ass").read_text(encoding="utf-8")
self.assertIn("Style: BilingualBox,MiSans,46", rendered)
self.assertIn(",4,8,0,2,80,80,50,1", rendered)
self.assertNotIn(r"{\an7\p1}", rendered)
self.assertIn(
r"BilingualBox,,0,0,0,,{\an2\pos(960,1030)\fs42}"
+ source
+ r"\N{\fs46}日文字形宽度与拉丁文字不同",
rendered,
)
self.assertIn(
r"Bilingual,,0,0,0,,{\an2\pos(960,1030)\fs42}"
+ source
+ r"\N{\fs46\1c&H00FFFF&}日文字形宽度与拉丁文字不同",
rendered,
)
def test_smart_mode_groups_only_whole_adjacent_cues(self) -> None:
raw = srt(
[
("00:00:00,000", "00:00:00,900", "Hello"),
("00:00:01,000", "00:00:02,000", "world."),
("00:00:04,000", "00:00:05,000", "Separate."),
]
)
manifest_path, manifest = self.prepare_fixture(raw, segment_mode="smart")
self.assertEqual(len(manifest["segments"]), 3)
self.assertEqual(len(manifest["render_segments"]), 2)
self.assertEqual(
manifest["render_segments"][0]["cue_ids"],
[manifest["cues"][0]["id"], manifest["cues"][1]["id"]],
)
self.assertEqual(manifest["cues"][0]["text"], "Hello")
self.assertEqual(manifest["cues"][1]["text"], "world.")
covered = [cue_id for segment in manifest["render_segments"] for cue_id in segment["cue_ids"]]
self.assertEqual(covered, [cue["id"] for cue in manifest["cues"]])
translations_dir = self.write_translations(manifest)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
rendered = output_dir.joinpath("source.srt").read_text(encoding="utf-8")
self.assertIn("Hello\nworld.", rendered)
chinese = output_dir.joinpath("zh-CN.srt").read_text(encoding="utf-8")
self.assertIn("中文 1 中文 2", chinese)
pipeline.validate(manifest_path, translations_dir, output_dir)
def test_chinese_lines_use_the_full_shared_width_budget(self) -> None:
manifest_path, manifest = self.prepare_fixture(
srt([("00:00:00,000", "00:00:03,000", "A test line.")])
)
one_line = "这条二十五个汉字长度的中文字幕不应被折成两行显示"
self.assertEqual(len(one_line), 24)
translations_dir = self.write_translations(
manifest,
records=[{"id": manifest["segments"][0]["id"], "translation": one_line}],
)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
chinese_srt = output_dir.joinpath("zh-CN.srt").read_text(encoding="utf-8")
chinese_lines = [line for line in chinese_srt.splitlines()[2:] if line]
self.assertEqual(chinese_lines, [one_line])
def test_japanese_target_keeps_punctuation_and_names_outputs(self) -> None:
self.assertEqual(
pipeline.normalize_target_caption("こんにちは、世界。", "ja"),
"こんにちは、世界。",
)
source = self.root / "downloaded.srt"
source.write_bytes(srt([("00:00:00,000", "00:00:02,000", "Hello world.")]))
manifest_path = pipeline.prepare(
source, self.root / "work", "en", "preserve", None, "ja"
)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
self.assertEqual(manifest["target_language"], "ja")
batch = json.loads(
Path(manifest["translation_batches"][0]["path"]).read_text(encoding="utf-8")
)
self.assertEqual(batch["target_language"], "ja")
translations_dir = self.write_translations(
manifest,
records=[
{"id": manifest["segments"][0]["id"], "translation": "こんにちは、世界。"}
],
)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
ja_srt = output_dir.joinpath("ja.srt").read_text(encoding="utf-8")
self.assertIn("こんにちは、世界。", ja_srt)
self.assertFalse(output_dir.joinpath("zh-CN.srt").exists())
report = json.loads(output_dir.joinpath("validation.json").read_text())
self.assertEqual(report["target_language"], "ja")
pipeline.validate(manifest_path, translations_dir, output_dir)
def test_rejects_invalid_target_language_tag(self) -> None:
source = self.root / "downloaded.srt"
source.write_bytes(srt([("00:00:00,000", "00:00:01,000", "Hi")]))
with self.assertRaisesRegex(pipeline.PipelineError, "--target-language"):
pipeline.prepare(source, self.root / "work", "en", "preserve", None, "bad lang!!")
def test_sound_annotation_cues_are_excluded_from_translation(self) -> None:
self.assertTrue(pipeline.is_non_dialogue_annotation("[Music]"))
self.assertTrue(pipeline.is_non_dialogue_annotation("[Applause] [Laughter]"))
self.assertTrue(pipeline.is_non_dialogue_annotation("【音乐】"))
self.assertTrue(pipeline.is_non_dialogue_annotation("(拍手)"))
self.assertTrue(pipeline.is_non_dialogue_annotation("♪♪"))
self.assertTrue(pipeline.is_non_dialogue_annotation("♪ [upbeat music] ♪"))
self.assertFalse(pipeline.is_non_dialogue_annotation("[Applause] Thank you"))
self.assertFalse(pipeline.is_non_dialogue_annotation("Hello (world)"))
self.assertFalse(pipeline.is_non_dialogue_annotation("「こんにちは」"))
self.assertFalse(pipeline.is_non_dialogue_annotation("Plain dialogue."))
raw = srt(
[
("00:00:00,000", "00:00:01,000", "[Music]"),
("00:00:01,100", "00:00:02,000", "Real dialogue starts."),
("00:00:02,100", "00:00:03,000", ""),
("00:00:03,100", "00:00:04,000", "[Applause] Thanks everyone."),
]
)
manifest_path, manifest = self.prepare_fixture(raw, segment_mode="smart")
texts = [cue["text"] for cue in manifest["cues"]]
self.assertEqual(texts, ["Real dialogue starts.", "[Applause] Thanks everyone."])
self.assertEqual(len(manifest["segments"]), 2)
translations_dir = self.write_translations(manifest)
output_dir = self.root / "output"
pipeline.render(manifest_path, translations_dir, output_dir)
rendered = output_dir.joinpath("source.srt").read_text(encoding="utf-8")
self.assertNotIn("[Music]", rendered)
self.assertNotIn("", rendered)
pipeline.validate(manifest_path, translations_dir, output_dir)
def test_annotation_only_subtitles_raise_no_dialogue(self) -> None:
raw = srt(
[
("00:00:00,000", "00:00:01,000", "[Music]"),
("00:00:01,100", "00:00:02,000", "【背景音乐】"),
]
)
source = self.root / "downloaded.srt"
source.write_bytes(raw)
with self.assertRaises(pipeline.NoDialogueError):
pipeline.prepare(source, self.root / "work", "en")
def test_smart_mode_closes_groups_exactly_at_sentence_boundaries(self) -> None:
raw = srt(
[
("00:00:00,000", "00:00:01,000", "This is the first"),
("00:00:01,100", "00:00:02,000", "half of a sentence."),
("00:00:02,100", "00:00:03,000", "Next sentence starts"),
("00:00:03,100", "00:00:04,000", "and keeps going"),
]
)
_, manifest = self.prepare_fixture(raw, segment_mode="smart")
cues = manifest["cues"]
groups = [segment["cue_ids"] for segment in manifest["render_segments"]]
self.assertEqual(
groups,
[
[cues[0]["id"], cues[1]["id"]],
[cues[2]["id"], cues[3]["id"]],
],
)
def test_smart_mode_clamps_rolling_caption_overlap(self) -> None:
raw = srt(
[
("00:00:00,000", "00:00:04,000", "First sentence."),
("00:00:02,000", "00:00:06,000", "Second sentence."),
("00:00:05,000", "00:00:07,000", "Third sentence."),
]
)
_, manifest = self.prepare_fixture(raw, segment_mode="smart")
self.assertEqual(len(manifest["segments"]), 3)
self.assertEqual(manifest["cues"][0]["end_ms"], 4000)
self.assertEqual(manifest["render_segments"][0]["end_ms"], 2000)
self.assertEqual(manifest["render_segments"][1]["end_ms"], 5000)
for current, following in zip(manifest["render_segments"], manifest["render_segments"][1:]):
self.assertLessEqual(current["end_ms"], following["start_ms"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,335 @@
from __future__ import annotations
import importlib.util
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "verify_delivery.py"
SPEC = importlib.util.spec_from_file_location("verify_delivery", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
delivery = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(delivery)
class VerifyDeliveryTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
self.source = self.root / "video.intermediate.mkv"
self.source.write_bytes(b"video")
self.subtitle = self.root / "video.source-srt.en.srt"
self.subtitle.write_text("1\n00:00:00,000 --> 00:00:01,000\nHello\n")
self.manifest = self.root / "download-manifest.json"
self.manifest.write_text(
json.dumps(
{
"status": "downloaded",
"output_directory": str(self.root),
"artifacts": {
"intermediate": {"path": self.source.name},
"subtitle": {
"source_srt": {"path": self.subtitle.name},
},
},
}
),
encoding="utf-8",
)
def tearDown(self) -> None:
self.temporary.cleanup()
def test_subtitled_job_with_only_translation_inputs_is_incomplete(self) -> None:
inputs = self.root / "subtitles" / "translation-input"
inputs.mkdir(parents=True)
batch = inputs / "batch-0001.json"
batch.write_text("{}", encoding="utf-8")
outputs = self.root / "subtitles" / "translation-output"
outputs.mkdir()
(self.root / "subtitles" / "subtitle-manifest.json").write_text(
json.dumps(
{
"translation_batches": [{"path": str(batch)}],
"translation_output_dir": str(outputs),
}
),
encoding="utf-8",
)
result = delivery.assess_delivery(self.manifest)
self.assertFalse(result["complete"])
self.assertEqual(result["stage"], "translation_required")
self.assertIn("batch-0001.json", result["missing"])
def test_video_deliverable_completes_with_untranslated_subtitle(self) -> None:
self.manifest.write_text(
json.dumps(
{
"status": "video_complete",
"deliverable": "video",
"output_directory": str(self.root),
"artifacts": {
"intermediate": {"path": self.source.name},
"subtitle": {
"language": "en",
"source_srt": {"path": self.subtitle.name},
},
},
}
),
encoding="utf-8",
)
result = delivery.assess_delivery(self.manifest)
self.assertTrue(result["complete"])
self.assertEqual(result["stage"], "video_complete")
def test_subs_deliverable_needs_no_video_file(self) -> None:
self.source.unlink()
self.manifest.write_text(
json.dumps(
{
"status": "subs_complete",
"deliverable": "subs",
"output_directory": str(self.root),
"artifacts": {
"intermediate": None,
"subtitle": {
"language": "en",
"source_srt": {"path": self.subtitle.name},
},
},
}
),
encoding="utf-8",
)
result = delivery.assess_delivery(self.manifest)
self.assertTrue(result["complete"])
self.assertEqual(result["stage"], "subs_complete")
def test_bilingual_subs_deliverable_stops_at_render(self) -> None:
inputs = self.root / "subtitles" / "translation-input"
inputs.mkdir(parents=True)
batch = inputs / "batch-0001.json"
batch.write_text("{}", encoding="utf-8")
outputs = self.root / "subtitles" / "translation-output"
outputs.mkdir()
(outputs / "batch-0001.json").write_text("{}", encoding="utf-8")
(self.root / "subtitles" / "subtitle-manifest.json").write_text(
json.dumps(
{
"translation_batches": [{"path": str(batch)}],
"translation_output_dir": str(outputs),
}
),
encoding="utf-8",
)
self.manifest.write_text(
json.dumps(
{
"status": "bilingual_required",
"deliverable": "bilingual-subs",
"output_directory": str(self.root),
"artifacts": {
"intermediate": None,
"subtitle": {
"language": "en",
"source_srt": {"path": self.subtitle.name},
},
},
}
),
encoding="utf-8",
)
pending = delivery.assess_delivery(self.manifest)
self.assertFalse(pending["complete"])
self.assertEqual(pending["stage"], "render_required")
rendered = self.root / "subtitles" / "rendered"
rendered.mkdir()
(rendered / "bilingual.ass").write_text("[Script Info]\n", encoding="utf-8")
(rendered / "validation.json").write_text("{}", encoding="utf-8")
result = delivery.assess_delivery(self.manifest)
self.assertTrue(result["complete"])
self.assertEqual(result["stage"], "bilingual_subs_complete")
def test_full_delivery_uses_manifest_bilingual_filename(self) -> None:
inputs = self.root / "subtitles" / "translation-input"
inputs.mkdir(parents=True)
batch = inputs / "batch-0001.json"
batch.write_text("{}", encoding="utf-8")
outputs = self.root / "subtitles" / "translation-output"
outputs.mkdir()
(outputs / "batch-0001.json").write_text("{}", encoding="utf-8")
(self.root / "subtitles" / "subtitle-manifest.json").write_text(
json.dumps(
{
"translation_batches": [{"path": str(batch)}],
"translation_output_dir": str(outputs),
}
),
encoding="utf-8",
)
rendered = self.root / "subtitles" / "rendered"
rendered.mkdir()
(rendered / "bilingual.ass").write_text("[Script Info]\n", encoding="utf-8")
(rendered / "validation.json").write_text("{}", encoding="utf-8")
expected = self.root / "双语字幕版「测试视频」.mp4"
self.manifest.write_text(
json.dumps(
{
"status": "bilingual_required",
"deliverable": "full",
"output_directory": str(self.root),
"delivery_names": {
"cover": "封面.jpg",
"bilingual_video": expected.name,
},
"artifacts": {
"intermediate": {"path": self.source.name},
"subtitle": {
"language": "en",
"source_srt": {"path": self.subtitle.name},
},
},
}
),
encoding="utf-8",
)
(self.root / "legacy.bilingual.mp4").write_bytes(b"legacy")
pending = delivery.assess_delivery(self.manifest)
self.assertFalse(pending["complete"])
self.assertEqual(pending["missing"], [str(expected.resolve())])
expected.write_bytes(b"burned")
result = delivery.assess_delivery(self.manifest)
self.assertTrue(result["complete"])
self.assertEqual(result["burned_video"], str(expected.resolve()))
def test_declared_citation_requires_matching_burn_receipt(self) -> None:
inputs = self.root / "subtitles" / "translation-input"
inputs.mkdir(parents=True)
batch = inputs / "batch-0001.json"
batch.write_text("{}", encoding="utf-8")
outputs = self.root / "subtitles" / "translation-output"
outputs.mkdir()
(outputs / "batch-0001.json").write_text("{}", encoding="utf-8")
(self.root / "subtitles" / "subtitle-manifest.json").write_text(
json.dumps(
{
"translation_batches": [{"path": str(batch)}],
"translation_output_dir": str(outputs),
}
),
encoding="utf-8",
)
rendered = self.root / "subtitles" / "rendered"
rendered.mkdir()
(rendered / "bilingual.ass").write_text("[Script Info]\n", encoding="utf-8")
(rendered / "validation.json").write_text("{}", encoding="utf-8")
expected = self.root / "双语字幕版「测试视频」.mp4"
expected.write_bytes(b"burned")
citation_file = self.root / "citation-watermark.txt"
citation_file.write_text("Formal citation.\n", encoding="utf-8")
citation_hash = hashlib.sha256(citation_file.read_bytes()).hexdigest()
manifest_value = {
"deliverable": "full",
"output_directory": str(self.root),
"delivery_names": {"bilingual_video": expected.name},
"citation_watermark": {
"enabled": True,
"citation_file": citation_file.name,
"citation_sha256": citation_hash,
"position": "top-left",
},
"artifacts": {
"intermediate": {"path": self.source.name},
"subtitle": {"source_srt": {"path": self.subtitle.name}},
},
}
self.manifest.write_text(json.dumps(manifest_value), encoding="utf-8")
with self.assertRaisesRegex(delivery.DeliveryError, "burn receipt is missing"):
delivery.assess_delivery(self.manifest)
receipt = expected.with_suffix(expected.suffix + ".citation.json")
receipt.write_text(
json.dumps(
{
"schema_version": 1,
"output_file": expected.name,
"output_sha256": hashlib.sha256(expected.read_bytes()).hexdigest(),
"citation_sha256": citation_hash,
"position": "top-left",
}
),
encoding="utf-8",
)
result = delivery.assess_delivery(self.manifest)
self.assertTrue(result["complete"])
expected.write_bytes(b"changed")
with self.assertRaisesRegex(delivery.DeliveryError, "output_sha256"):
delivery.assess_delivery(self.manifest)
def test_no_dialogue_subtitle_counts_as_video_only(self) -> None:
self.manifest.write_text(
json.dumps(
{
"status": "video_only_complete",
"output_directory": str(self.root),
"artifacts": {
"intermediate": {"path": self.source.name},
"subtitle": {
"language": "en",
"dialogue": False,
"source_srt": {"path": self.subtitle.name},
},
},
}
),
encoding="utf-8",
)
result = delivery.assess_delivery(self.manifest)
self.assertTrue(result["complete"])
self.assertEqual(result["stage"], "video_only_complete")
def test_video_only_job_requires_the_video_file_on_disk(self) -> None:
self.manifest.write_text(
json.dumps(
{
"status": "downloaded",
"output_directory": str(self.root),
"artifacts": {
"intermediate": {"path": self.source.name},
"subtitle": None,
},
}
),
encoding="utf-8",
)
result = delivery.assess_delivery(self.manifest)
self.assertTrue(result["complete"])
self.assertEqual(result["stage"], "video_only_complete")
self.source.unlink()
with self.assertRaisesRegex(delivery.DeliveryError, "video artifact"):
delivery.assess_delivery(self.manifest)
if __name__ == "__main__":
unittest.main()