From 90583d1dcad3a69e3ba2c4a70b88a56d86fdb543 Mon Sep 17 00:00:00 2001 From: cat-shark Date: Thu, 13 Aug 2026 22:09:55 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=B8=BA=E5=85=A8=E9=83=A8=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E8=A1=A5=E5=85=85=E4=B8=AD=E6=96=87=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E5=B9=B6=E5=8A=A0=E5=85=A5=20AGENTS=20=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 7 +++++++ pyproject.toml | 5 +++++ tests/test_ass_node.py | 11 +++++++++++ wov_node_ass/__init__.py | 5 ++++- wov_node_ass/__main__.py | 19 +++++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 389132d..63da0e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,3 +10,10 @@ ## 输出 - `ass_uri`:生成的双目 ASS 文件。 + +## 代码注释规范 + +- 本仓库所有源码(Python、TOML 等支持注释的文件)必须配有详细中文注释,说明模块职责、SRT 解析与左右眼 ASS 生成逻辑,确保后续维护人员可以快速理解代码工作原理。 +- 新增或修改代码时,必须同步补充或更新对应注释;不得删除已有注释。 +- 测试代码同样必须配有中文注释,说明每条测试验证的行为。 +- JSON 数据文件(`node.manifest.json`)不支持注释,字段语义以 `wov-sdk` 的 `NodeManifest` 模型注释和本文档输入/输出说明为准。 diff --git a/pyproject.toml b/pyproject.toml index 4a963a4..fe23e7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ +# WOV ASS 节点配置:使用 uv 管理环境与依赖。 [project] name = "wov-node-ass" version = "0.1.0" @@ -5,16 +6,20 @@ description = "WOV SRT to dual-eye ASS node" requires-python = ">=3.11" dependencies = ["wov-sdk"] +# 本地路径依赖 wov-sdk。 [tool.uv.sources] wov-sdk = { path = "../wov-sdk" } +# 开发依赖:pytest 与覆盖率工具。 [dependency-groups] dev = ["pytest", "pytest-cov"] +# pytest 配置:强制 100% 行覆盖率。 [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] addopts = "--cov=wov_node_ass --cov-report=term-missing --cov-fail-under=100" +# 仅打包节点包本身。 [tool.setuptools] packages = ["wov_node_ass"] diff --git a/tests/test_ass_node.py b/tests/test_ass_node.py index 24a4592..e3e0fa9 100644 --- a/tests/test_ass_node.py +++ b/tests/test_ass_node.py @@ -1,3 +1,8 @@ +"""SRT 转 ASS 节点测试。 + +覆盖 SRT 解析、ASS 写入、畸形输入、缺失输入和入口点启动等真实代码路径。 +""" + import runpy from pathlib import Path @@ -18,6 +23,7 @@ SAMPLE_SRT = """ def test_parse_and_write(tmp_path) -> None: + """验证多行字幕会被解析并通过左右眼样式写出。""" entries = parse_srt(SAMPLE_SRT) assert len(entries) == 2 assert entries[0][2] == r"第一行\N第二行" @@ -33,6 +39,7 @@ def test_parse_and_write(tmp_path) -> None: def test_parse_malformed(tmp_path) -> None: + """验证畸形 SRT 不会抛出异常且返回空条目或忽略坏行。""" source = tmp_path / "bad.srt" source.write_text("1\nnot a time line\n", encoding="utf-8") assert parse_srt(source.read_text(encoding="utf-8")) == [] @@ -42,6 +49,7 @@ def test_parse_malformed(tmp_path) -> None: def test_invoke_success(tmp_path) -> None: + """验证成功调用会按指定分辨率输出 ASS 产物。""" source = tmp_path / "in.srt" source.write_text(SAMPLE_SRT, encoding="utf-8") response = invoke( @@ -58,6 +66,7 @@ def test_invoke_success(tmp_path) -> None: def test_invoke_missing_input(tmp_path) -> None: + """验证缺少 cn_srt_uri 时返回失败。""" response = invoke( InvokeRequest( run_id="run_2", @@ -70,6 +79,7 @@ def test_invoke_missing_input(tmp_path) -> None: def test_invoke_missing_file(tmp_path) -> None: + """验证 SRT 文件不存在时返回失败。""" response = invoke( InvokeRequest( run_id="run_3", @@ -82,6 +92,7 @@ def test_invoke_missing_file(tmp_path) -> None: def test_entrypoint(monkeypatch) -> None: + """验证 python -m wov_node_ass 会加载 ASS 节点 manifest。""" module_path = Path(__file__).resolve().parent.parent / "wov_node_ass" / "__main__.py" captured = {} diff --git a/wov_node_ass/__init__.py b/wov_node_ass/__init__.py index 4727b90..db56bc9 100644 --- a/wov_node_ass/__init__.py +++ b/wov_node_ass/__init__.py @@ -1 +1,4 @@ -"""WOV SRT to dual-eye ASS node.""" +"""WOV SRT 转 VR 双眼 ASS 字幕节点。 + +把翻译后的中文 SRT 转换为左右眼并排显示的 ASS 字幕,供 VR 播放器使用。 +""" diff --git a/wov_node_ass/__main__.py b/wov_node_ass/__main__.py index ac960d0..e7dc19b 100644 --- a/wov_node_ass/__main__.py +++ b/wov_node_ass/__main__.py @@ -1,3 +1,9 @@ +"""SRT 转 ASS 节点入口。 + +解析标准 SRT 后生成 ASS 文件,其中同一句字幕同时输出 LeftEye 与 RightEye +两个样式,分别落在屏幕左右两半,形成 VR 双眼叠加效果。 +""" + from __future__ import annotations import json @@ -8,7 +14,9 @@ from wov_sdk.server import run_node def _ass_header(resolution: str) -> str: + """生成 ASS 文件头:脚本信息、左右眼样式和事件格式。""" width, height = resolution.lower().split("x", 1) + # 左眼样式占左半边,右眼样式占右半边,各留 50px 内边距。 left_margin = 50 right_margin = int(width) - 50 return f"""[Script Info] @@ -31,21 +39,27 @@ Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text def parse_srt(text: str) -> list[tuple[str, str, str]]: + """把 SRT 文本解析为 (开始时间, 结束时间, 文本) 条目列表。""" entries: list[tuple[str, str, str]] = [] lines = text.splitlines() index = 0 while index < len(lines): + # 跳过序号前的空行,兼容文件开头有换行的情况。 if not lines[index].strip(): index += 1 continue + # 跳过序号行,直接读取下一行时间轴。 index += 1 if index >= len(lines): break time_line = lines[index].strip() index += 1 + # 时间轴必须包含分隔符,否则按畸形输入跳过。 if " --> " not in time_line: continue + # SRT 使用逗号毫秒,ASS 使用点号,需要转换。 start, end = [part.replace(",", ".") for part in time_line.split(" --> ")] + # 连续读取非空行作为字幕文本,多行用 ASS 换行符 \N 连接。 text_lines: list[str] = [] while index < len(lines) and lines[index].strip(): text_lines.append(lines[index]) @@ -56,14 +70,17 @@ def parse_srt(text: str) -> list[tuple[str, str, str]]: def write_ass(entries: list[tuple[str, str, str]], output_path: Path, resolution: str) -> None: + """把解析后的条目写入 ASS 文件,每个条目输出左右眼两行 Dialogue。""" lines = [_ass_header(resolution)] for start, end, text in entries: + # an2 对齐到屏幕中央偏下,保证双眼字幕视线自然。 lines.append(f"Dialogue: 0,{start},{end},LeftEye,,0,0,0,,{{\\an2}}{text}") lines.append(f"Dialogue: 0,{start},{end},RightEye,,0,0,0,,{{\\an2}}{text}") output_path.write_text("\n".join(lines) + "\n", encoding="utf-8") def invoke(request: InvokeRequest) -> InvokeResponse: + """把 cn_srt_uri 指向的 SRT 转为 dual_eye.ass 产物。""" srt_uri = request.inputs.get("cn_srt_uri") if not srt_uri: return InvokeResponse(status="failed", error="cn_srt_uri is required") @@ -76,12 +93,14 @@ def invoke(request: InvokeRequest) -> InvokeResponse: output_dir = Path(request.output_dir) output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / "dual_eye.ass" + # 分辨率默认 3840x1920,覆盖常见 VR 视频尺寸。 resolution = str(request.params.get("resolution", "3840x1920")) write_ass(entries, output_path, resolution) return InvokeResponse(status="completed", outputs={"ass_uri": str(output_path)}) def main() -> None: + """加载节点清单并以本模块的 invoke 处理器启动服务。""" manifest_path = Path(__file__).resolve().parent.parent / "node.manifest.json" with open(manifest_path, "r", encoding="utf-8") as f: manifest = NodeManifest.from_dict(json.load(f))