docs: 为全部代码补充中文注释并加入 AGENTS 注释规范
This commit is contained in:
@@ -10,3 +10,10 @@
|
|||||||
## 输出
|
## 输出
|
||||||
|
|
||||||
- `ass_uri`:生成的双目 ASS 文件。
|
- `ass_uri`:生成的双目 ASS 文件。
|
||||||
|
|
||||||
|
## 代码注释规范
|
||||||
|
|
||||||
|
- 本仓库所有源码(Python、TOML 等支持注释的文件)必须配有详细中文注释,说明模块职责、SRT 解析与左右眼 ASS 生成逻辑,确保后续维护人员可以快速理解代码工作原理。
|
||||||
|
- 新增或修改代码时,必须同步补充或更新对应注释;不得删除已有注释。
|
||||||
|
- 测试代码同样必须配有中文注释,说明每条测试验证的行为。
|
||||||
|
- JSON 数据文件(`node.manifest.json`)不支持注释,字段语义以 `wov-sdk` 的 `NodeManifest` 模型注释和本文档输入/输出说明为准。
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# WOV ASS 节点配置:使用 uv 管理环境与依赖。
|
||||||
[project]
|
[project]
|
||||||
name = "wov-node-ass"
|
name = "wov-node-ass"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -5,16 +6,20 @@ description = "WOV SRT to dual-eye ASS node"
|
|||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["wov-sdk"]
|
dependencies = ["wov-sdk"]
|
||||||
|
|
||||||
|
# 本地路径依赖 wov-sdk。
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
wov-sdk = { path = "../wov-sdk" }
|
wov-sdk = { path = "../wov-sdk" }
|
||||||
|
|
||||||
|
# 开发依赖:pytest 与覆盖率工具。
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = ["pytest", "pytest-cov"]
|
dev = ["pytest", "pytest-cov"]
|
||||||
|
|
||||||
|
# pytest 配置:强制 100% 行覆盖率。
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
pythonpath = ["."]
|
pythonpath = ["."]
|
||||||
addopts = "--cov=wov_node_ass --cov-report=term-missing --cov-fail-under=100"
|
addopts = "--cov=wov_node_ass --cov-report=term-missing --cov-fail-under=100"
|
||||||
|
|
||||||
|
# 仅打包节点包本身。
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
packages = ["wov_node_ass"]
|
packages = ["wov_node_ass"]
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""SRT 转 ASS 节点测试。
|
||||||
|
|
||||||
|
覆盖 SRT 解析、ASS 写入、畸形输入、缺失输入和入口点启动等真实代码路径。
|
||||||
|
"""
|
||||||
|
|
||||||
import runpy
|
import runpy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -18,6 +23,7 @@ SAMPLE_SRT = """
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_and_write(tmp_path) -> None:
|
def test_parse_and_write(tmp_path) -> None:
|
||||||
|
"""验证多行字幕会被解析并通过左右眼样式写出。"""
|
||||||
entries = parse_srt(SAMPLE_SRT)
|
entries = parse_srt(SAMPLE_SRT)
|
||||||
assert len(entries) == 2
|
assert len(entries) == 2
|
||||||
assert entries[0][2] == r"第一行\N第二行"
|
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:
|
def test_parse_malformed(tmp_path) -> None:
|
||||||
|
"""验证畸形 SRT 不会抛出异常且返回空条目或忽略坏行。"""
|
||||||
source = tmp_path / "bad.srt"
|
source = tmp_path / "bad.srt"
|
||||||
source.write_text("1\nnot a time line\n", encoding="utf-8")
|
source.write_text("1\nnot a time line\n", encoding="utf-8")
|
||||||
assert parse_srt(source.read_text(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:
|
def test_invoke_success(tmp_path) -> None:
|
||||||
|
"""验证成功调用会按指定分辨率输出 ASS 产物。"""
|
||||||
source = tmp_path / "in.srt"
|
source = tmp_path / "in.srt"
|
||||||
source.write_text(SAMPLE_SRT, encoding="utf-8")
|
source.write_text(SAMPLE_SRT, encoding="utf-8")
|
||||||
response = invoke(
|
response = invoke(
|
||||||
@@ -58,6 +66,7 @@ def test_invoke_success(tmp_path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_invoke_missing_input(tmp_path) -> None:
|
def test_invoke_missing_input(tmp_path) -> None:
|
||||||
|
"""验证缺少 cn_srt_uri 时返回失败。"""
|
||||||
response = invoke(
|
response = invoke(
|
||||||
InvokeRequest(
|
InvokeRequest(
|
||||||
run_id="run_2",
|
run_id="run_2",
|
||||||
@@ -70,6 +79,7 @@ def test_invoke_missing_input(tmp_path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_invoke_missing_file(tmp_path) -> None:
|
def test_invoke_missing_file(tmp_path) -> None:
|
||||||
|
"""验证 SRT 文件不存在时返回失败。"""
|
||||||
response = invoke(
|
response = invoke(
|
||||||
InvokeRequest(
|
InvokeRequest(
|
||||||
run_id="run_3",
|
run_id="run_3",
|
||||||
@@ -82,6 +92,7 @@ def test_invoke_missing_file(tmp_path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_entrypoint(monkeypatch) -> 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"
|
module_path = Path(__file__).resolve().parent.parent / "wov_node_ass" / "__main__.py"
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
"""WOV SRT to dual-eye ASS node."""
|
"""WOV SRT 转 VR 双眼 ASS 字幕节点。
|
||||||
|
|
||||||
|
把翻译后的中文 SRT 转换为左右眼并排显示的 ASS 字幕,供 VR 播放器使用。
|
||||||
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
"""SRT 转 ASS 节点入口。
|
||||||
|
|
||||||
|
解析标准 SRT 后生成 ASS 文件,其中同一句字幕同时输出 LeftEye 与 RightEye
|
||||||
|
两个样式,分别落在屏幕左右两半,形成 VR 双眼叠加效果。
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -8,7 +14,9 @@ from wov_sdk.server import run_node
|
|||||||
|
|
||||||
|
|
||||||
def _ass_header(resolution: str) -> str:
|
def _ass_header(resolution: str) -> str:
|
||||||
|
"""生成 ASS 文件头:脚本信息、左右眼样式和事件格式。"""
|
||||||
width, height = resolution.lower().split("x", 1)
|
width, height = resolution.lower().split("x", 1)
|
||||||
|
# 左眼样式占左半边,右眼样式占右半边,各留 50px 内边距。
|
||||||
left_margin = 50
|
left_margin = 50
|
||||||
right_margin = int(width) - 50
|
right_margin = int(width) - 50
|
||||||
return f"""[Script Info]
|
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]]:
|
def parse_srt(text: str) -> list[tuple[str, str, str]]:
|
||||||
|
"""把 SRT 文本解析为 (开始时间, 结束时间, 文本) 条目列表。"""
|
||||||
entries: list[tuple[str, str, str]] = []
|
entries: list[tuple[str, str, str]] = []
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
index = 0
|
index = 0
|
||||||
while index < len(lines):
|
while index < len(lines):
|
||||||
|
# 跳过序号前的空行,兼容文件开头有换行的情况。
|
||||||
if not lines[index].strip():
|
if not lines[index].strip():
|
||||||
index += 1
|
index += 1
|
||||||
continue
|
continue
|
||||||
|
# 跳过序号行,直接读取下一行时间轴。
|
||||||
index += 1
|
index += 1
|
||||||
if index >= len(lines):
|
if index >= len(lines):
|
||||||
break
|
break
|
||||||
time_line = lines[index].strip()
|
time_line = lines[index].strip()
|
||||||
index += 1
|
index += 1
|
||||||
|
# 时间轴必须包含分隔符,否则按畸形输入跳过。
|
||||||
if " --> " not in time_line:
|
if " --> " not in time_line:
|
||||||
continue
|
continue
|
||||||
|
# SRT 使用逗号毫秒,ASS 使用点号,需要转换。
|
||||||
start, end = [part.replace(",", ".") for part in time_line.split(" --> ")]
|
start, end = [part.replace(",", ".") for part in time_line.split(" --> ")]
|
||||||
|
# 连续读取非空行作为字幕文本,多行用 ASS 换行符 \N 连接。
|
||||||
text_lines: list[str] = []
|
text_lines: list[str] = []
|
||||||
while index < len(lines) and lines[index].strip():
|
while index < len(lines) and lines[index].strip():
|
||||||
text_lines.append(lines[index])
|
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:
|
def write_ass(entries: list[tuple[str, str, str]], output_path: Path, resolution: str) -> None:
|
||||||
|
"""把解析后的条目写入 ASS 文件,每个条目输出左右眼两行 Dialogue。"""
|
||||||
lines = [_ass_header(resolution)]
|
lines = [_ass_header(resolution)]
|
||||||
for start, end, text in entries:
|
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},LeftEye,,0,0,0,,{{\\an2}}{text}")
|
||||||
lines.append(f"Dialogue: 0,{start},{end},RightEye,,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")
|
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def invoke(request: InvokeRequest) -> InvokeResponse:
|
def invoke(request: InvokeRequest) -> InvokeResponse:
|
||||||
|
"""把 cn_srt_uri 指向的 SRT 转为 dual_eye.ass 产物。"""
|
||||||
srt_uri = request.inputs.get("cn_srt_uri")
|
srt_uri = request.inputs.get("cn_srt_uri")
|
||||||
if not srt_uri:
|
if not srt_uri:
|
||||||
return InvokeResponse(status="failed", error="cn_srt_uri is required")
|
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 = Path(request.output_dir)
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
output_path = output_dir / "dual_eye.ass"
|
output_path = output_dir / "dual_eye.ass"
|
||||||
|
# 分辨率默认 3840x1920,覆盖常见 VR 视频尺寸。
|
||||||
resolution = str(request.params.get("resolution", "3840x1920"))
|
resolution = str(request.params.get("resolution", "3840x1920"))
|
||||||
write_ass(entries, output_path, resolution)
|
write_ass(entries, output_path, resolution)
|
||||||
return InvokeResponse(status="completed", outputs={"ass_uri": str(output_path)})
|
return InvokeResponse(status="completed", outputs={"ass_uri": str(output_path)})
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
"""加载节点清单并以本模块的 invoke 处理器启动服务。"""
|
||||||
manifest_path = Path(__file__).resolve().parent.parent / "node.manifest.json"
|
manifest_path = Path(__file__).resolve().parent.parent / "node.manifest.json"
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||||
manifest = NodeManifest.from_dict(json.load(f))
|
manifest = NodeManifest.from_dict(json.load(f))
|
||||||
|
|||||||
Reference in New Issue
Block a user