diff --git a/.github/workflows/lint-check.yml b/.github/workflows/lint-check.yml index ea51323b..e6930272 100644 --- a/.github/workflows/lint-check.yml +++ b/.github/workflows/lint-check.yml @@ -22,8 +22,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 - with: - fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1cbc8c44..5d2d2f3e 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -16,8 +16,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 - with: - fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index cc655207..6f944269 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -23,8 +23,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 - with: - fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5142329d..262d995d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,8 +25,6 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 - with: - fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 diff --git a/.gitignore b/.gitignore index b86d0a41..b4d7b9c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,37 @@ +# Python 和工具缓存 __pycache__ .pytest_cache .mypy_cache .coverage + +# 虚拟环境 .venv -htmlcov + +# 构建输出 dist Annotations2Sub.egg-info + +# 覆盖率报告 +htmlcov + +# 杂项 测试用例/ -src/tests/garbage/* -!src/tests/garbage/stub -report *.mp4 *.mkv *.webm *.xml *.ass + +src/tests/garbage/* +!src/tests/garbage/stub + +report *.lnk *.ps1 *.pyz *.dot *.prof + mypy.ini uv.lock .python-version diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index cbcffaa9..93f71607 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -461,7 +461,6 @@ subtitles_string = AnnotationsXmlStringToSubtitlesString(xml_string) with open('output.ass', 'w', encoding='utf-8') as f: f.write(subtitles_string) -# If you're concerned about licensing issues, you can contact me for an alternative license. ``` ### Comparison with Similar Software @@ -502,3 +501,7 @@ Metadata database of videos with annotations. [Annotations2Sub-Lite](https://a2s.liutao.page/) A web version made by AI. + +[Ar tonelico 系列 中文字幕 (Rain Shimotsuki 注释字幕还原) 合集](https://www.bilibili.com/video/BV1Ff4y1t7Dj) + +This project is a derivative of this project. diff --git a/src/Annotations2Sub/Annotations.py b/src/Annotations2Sub/Annotations.py index eab090bf..66d07753 100644 --- a/src/Annotations2Sub/Annotations.py +++ b/src/Annotations2Sub/Annotations.py @@ -2,6 +2,7 @@ import datetime as dt import math +import re from datetime import datetime from typing import List, Optional, Union from xml.etree.ElementTree import Element @@ -192,55 +193,36 @@ def ParseAnnotationColor(colorString: str) -> Color: return Color(red=r, green=g, blue=b) def ParseTime(timeString: str) -> datetime: - def parseFloat(string: str) -> float: - def cleanInt(string: str) -> str: - string = string.replace("s", "") - string = string.replace("-", "") - string = string.replace("%", "") - - if string == "NaN": - return "0" - if string == "aN": - return "0" - if "#" in string: - return "0" - return string - - if string == "": - return 0 - if string == "4294967294": - return 0 - if string == "&": - return 0 - if string == "NaN": - return 0 - - part = string.split(".") - part = list(map(cleanInt, part)) - string = part[0] - if len(part) > 1: - string = string + "." + part[1] - return float(string) - - if timeString == "": - return datetime.strptime("0", "%S") - if timeString == "never": - return datetime.strptime("0", "%S") - if timeString == "undefined": - return datetime.strptime("0", "%S") - parts = timeString.split(":") seconds = 0.0 for part in parts: - time = parseFloat(part) + time = ParseFloat(part) seconds = 60 * seconds + abs(time) return datetime.fromtimestamp(seconds, dt.timezone.utc).replace(tzinfo=None) def ParseFloat(string: str) -> float: - string = string.replace(",", ".") - return float(string) + def parseFloat(string: str) -> float: + match = re.match( + r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?", string.lstrip() + ) + if match != None: + number = float(match.group(0)) + return number + return 0.0 + + try: + number = float(string) + except ValueError: + number = parseFloat(string) + + if math.isnan(number): + return 0.0 + if number > 2147483647: + return 0.0 + + return number _id = each.get("id", "") if _id == "": @@ -304,8 +286,6 @@ def ParseFloat(string: str) -> float: if w < 0: w = 0 - if math.isnan(w): - w = 0 author = each.get("author", "") diff --git a/src/Annotations2Sub/__init__.py b/src/Annotations2Sub/__init__.py index 4f272a44..548dc866 100644 --- a/src/Annotations2Sub/__init__.py +++ b/src/Annotations2Sub/__init__.py @@ -8,6 +8,7 @@ 此工具可以帮助您将 YouTube 注释转换为 ASS 字幕文件, 您可以播放或添加到视频中. """ + """ xml. etree. @@ -97,6 +98,10 @@ 随着时间流逝, 本项目所依赖的外部服务已逐渐变得不可用, 现已移除相关功能. 感谢 Invidious 和 Internet Archive 所提供的帮助. +--- + +又是时光流逝 Vibe Code 已成现实, Agents 已成长的他妈都不认识. + --- - 注释(Annotations): YouTube 的功能 - SSA(Sub Station Alpha): 字幕格式 diff --git a/src/Annotations2Sub/cli.py b/src/Annotations2Sub/cli.py index c5fe2f04..8d5d6dcb 100644 --- a/src/Annotations2Sub/cli.py +++ b/src/Annotations2Sub/cli.py @@ -19,7 +19,7 @@ def Run(args=None) -> int: - """命令行应用的实现 + """命令行界面的实现 参数应当是 `list(str)`, 当参数为 `None` 时 `argparse` 会从 `sys.argv` 解析参数. diff --git a/src/Annotations2Sub/color.py b/src/Annotations2Sub/color.py index 81a2e614..0ad4023a 100644 --- a/src/Annotations2Sub/color.py +++ b/src/Annotations2Sub/color.py @@ -1,33 +1,23 @@ # -*- coding: utf-8 -*- +from dataclasses import dataclass + from Annotations2Sub.i18n import _ +@dataclass class Color: - def __init__( - self, - red: int = 0, - green: int = 0, - blue: int = 0, - ): - if red > 255: - raise ValueError(_('"red" 必须在 0-255 之间')) - if green > 255: - raise ValueError(_('"green" 必须在 0-255 之间')) - if blue > 255: - raise ValueError(_('"blue" 必须在 0-255 之间')) - self.red = red - self.green = green - self.blue = blue + red: int = 0 + green: int = 0 + blue: int = 0 +@dataclass class Alpha: - def __init__(self, alpha: int = 0): - if alpha > 255: - raise ValueError(_('"alpha" 必须在 0-255 之间')) - self.alpha = alpha + alpha: int = 0 +@dataclass class Rgba: def __init__(self, color: Color = Color(), alpha: Alpha = Alpha()): self.red = color.red diff --git a/src/README.md b/src/README.md index f43ed35c..9fe1f12c 100644 --- a/src/README.md +++ b/src/README.md @@ -1,22 +1,38 @@ # Annotations2Sub Source Code -## 快速背景 +## 概述 -本仓库是一个用于将旧版 YouTube Annotations 的 XML 文件转换为 ASS 字幕文件的命令行工具. 包的入口点是控制台脚本 `Annotations2Sub`(在 `pyproject.toml` -> `project.scripts` 中定义). 主要代码位于 `src/Annotations2Sub/`, 测试代码位于 `src/tests/`. +Annotations2Sub 是一个将旧版 YouTube Annotations 的 XML 文件转换为 ASS 字幕文件的命令行工具. 入口点是控制台脚本 `Annotations2Sub`. 主要代码位于 `src/Annotations2Sub/`, 测试代码位于 `src/tests/`. + +## 技术栈 + +- Python 3.7+ +- 无外部依赖 +- 使用 [uv](https://github.com/astral-sh/uv) 管理工具链, 使用 setuptools 打包, pytest 测试, mypy 类型检查, isort 和 black 进行代码格式化. ## 组织结构 -- 目的: 读取 YouTube Annotations XML 文件并生成 ASS 字幕文件. 用户用法见 `README.md`: `Annotations2Sub `. -- 流程: 解析(`Annotations.py`)、转换(`convert.py`)、输出(`subtitles/*`). +- 流程: 解析(`Annotations.py`)、转换(`convert.py`)和输出(`subtitles/*`). +- 入口: `src/Annotations2Sub/_main.py` 或 `src/Annotations2Sub/__main__.py`. - 核心模块: - - `src/Annotations2Sub/_main.py` 或 `src/Annotations2Sub/__main__.py` —— 程序入口. - - `src/Annotations2Sub/Annotations.py` —— XML 解析和Annotations数据结构. - - `src/Annotations2Sub/convert.py` —— 主要的转换逻辑和数据变换. - - `src/Annotations2Sub/subtitles/` —— 字幕格式、样式、事件和绘图辅助. + - `src/Annotations2Sub/Annotations.py` : XML 解析和Annotations数据结构. + - `src/Annotations2Sub/convert.py` : 主要转换逻辑. + - `src/Annotations2Sub/subtitles/` : 字幕格式、样式、事件和绘图辅助. + - `src/Annotations2Sub/cli.py` : 用户界面. +- 测试: + - `src/tests/test_Baseline.py` : 回归测试. + - `src/tests/test_cli.py` : 集成测试. + - `src/tests/test_addendum.py` : 以上两个测试未覆盖的测试. + - `src/tests/unittest/` : 其他测试. + +## 项目特有的模式 + +- 测试用例是 Youtube Annotations, 使用 `src/tests/testCase/` 下的 `.test` 文件作为输入, 同时包含以 `.ass.test`、`.transform.ass.test` 等后缀的期望输出文件. +- gettext `.po`/`.mo` 文件在 `src/Annotations2Sub/locales/`. 如有用户可见字符串变更, 请更新 `.po` 文件并重新生成 `.mo`. ## 如何运行、测试和代码检查 -- 推荐使用 [uv](https://github.com/astral-sh/uv) 进行依赖管理和运行. +- 使用 [uv](https://github.com/astral-sh/uv) 进行依赖管理. `uv sync` @@ -38,20 +54,7 @@ `black .` -## 项目特有的模式和约定 - -- 测试用例是 Youtube Annotations, 使用 `src/tests/testCase/` 下的 `.test` 文件作为输入, 同时包含以 `.ass.test`、`.transform.ass.test` 等后缀的期望输出文件. -- 添加类型注解并保持 mypy 检查通过. -- 使用 isort 和 black 进行代码格式化. -- 本地化: gettext `.po`/`.mo` 文件在 `src/Annotations2Sub/locales/`. 如有用户可见字符串变更, 请更新 `.po` 文件并重新生成 `.mo`. - -## 集成点与外部依赖 - -- 无外部依赖. -- 构建/打包使用 setuptools. -- CI 会上传覆盖率到 Codecov. - -## 调试注释行为 +## 调试 Annotations 行为 使用[youtube_annotations_hack](https://github.com/USED255/youtube_annotations_hack)来预览正确的注释行为. @@ -61,7 +64,7 @@ ## 问题咨询 -- 在 GitHub 提 [issue](https://github.com/USED255/Annotations2Sub/issues). +请在 GitHub 提 [issue](https://github.com/USED255/Annotations2Sub/issues). ## 您也可以看看 @@ -71,7 +74,7 @@ - https://github.com/weizhenye/ASS/wiki/ASS-字幕格式规范 - 整理过的关于 ASS 字幕文件的格式以及渲染行为的文档. + 整理好的关于 ASS 字幕文件的格式以及渲染行为的文档. - https://github.com/USED255/youtube_annotations_hack diff --git a/src/tests/test_addendum.py b/src/tests/test_addendum.py new file mode 100644 index 00000000..e74ba4b1 --- /dev/null +++ b/src/tests/test_addendum.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +# Baseline 和 cli 测试未覆盖的部分 + +import gettext +import os +import sys + +import pytest + +import Annotations2Sub.__main__ +from Annotations2Sub import Annotation, Subtitles +from Annotations2Sub.i18n import internationalization +from Annotations2Sub.subtitles import Event, Style +from Annotations2Sub.utils import Err1, Warn1 + + +def test_internationalization_FileNotFoundError(): + def f(*args, **kwargs): + raise FileNotFoundError + + m = pytest.MonkeyPatch() + m.setattr(gettext, "translation", f) + + assert internationalization() + + m.undo() + + +def test_internationalization_win32(): + m = pytest.MonkeyPatch() + m.setattr(sys, "platform", "win32") + m.setattr(os, "getenv", lambda x: None) + + assert internationalization() + + m.undo() + + +def test_repr_Annotation(): + assert repr(Annotation()) == str(Annotation()) + + +def test_eq_Annotation(): + assert Annotation() == Annotation() + + +def test_repr_Style(): + assert repr(Style()) == str(Style()) + + +def test_eq_Style(): + assert Style() == Style() + + +def test_repr_Event(): + assert repr(Event()) == str(Event()) + + +def test_eq_Event(): + assert Event() == Event() + + +def test_repr_Sub(): + assert repr(Subtitles()) == str(Subtitles()) + + +def test_eq_Sub(): + assert Subtitles() == Subtitles() + + +def test_Err1(): + Err1("Test") + + +def test_Warn1(): + Warn1("Test") + + +def test_main(): + with pytest.raises(SystemExit): + Annotations2Sub.__main__.main() diff --git a/src/tests/test_other.py b/src/tests/test_other.py deleted file mode 100644 index 1387d8b1..00000000 --- a/src/tests/test_other.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -# import Annotations2Sub.__main__ diff --git a/src/tests/unittest/test_Annotation.py b/src/tests/unittest/test_Annotation.py index 53ecc1cc..9f2023d1 100644 --- a/src/tests/unittest/test_Annotation.py +++ b/src/tests/unittest/test_Annotation.py @@ -21,14 +21,6 @@ def test_str_Annotation(): ) -def test_repr_Annotation(): - assert repr(Annotation()) == str(Annotation()) - - -def test_eq_Annotation(): - assert Annotation() == Annotation() - - def test_Parse(): filePath = os.path.join(testCasePath, "annotations.xml.test") with open(filePath, "r", encoding="utf-8") as f: diff --git a/src/tests/unittest/test_Color.py b/src/tests/unittest/test_Color.py index 0a1df975..420f31d4 100644 --- a/src/tests/unittest/test_Color.py +++ b/src/tests/unittest/test_Color.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -import pytest - from Annotations2Sub.color import Alpha, Color, Rgba @@ -12,25 +10,11 @@ def test_Color(): assert color.blue == 0 -def test_Color_ValueError(): - with pytest.raises(ValueError): - Color(256, 0, 0) - with pytest.raises(ValueError): - Color(0, 256, 0) - with pytest.raises(ValueError): - Color(0, 0, 256) - - def test_Alpha(): alpha = Alpha(0) assert alpha.alpha == 0 -def test_Alpha_ValueError(): - with pytest.raises(ValueError): - Alpha(256) - - def test_Rgba(): rgba = Rgba(Color(0, 0, 0), Alpha(0)) assert rgba.red == 0 diff --git a/src/tests/unittest/test_Subtitles.py b/src/tests/unittest/test_Subtitles.py index fbb4e452..2baf7ccb 100644 --- a/src/tests/unittest/test_Subtitles.py +++ b/src/tests/unittest/test_Subtitles.py @@ -15,14 +15,6 @@ def test_str_Style(): ) -def test_repr_Style(): - assert repr(Style()) == str(Style()) - - -def test_eq_Style(): - assert Style() == Style() - - def test_Event(): assert Event() @@ -31,22 +23,12 @@ def test_str_Event(): assert str(Event()) == "Dialogue: 0,00:00:00.00,00:00:00.00,Default,,0,0,0,,\n" -def test_repr_Event(): - assert repr(Event()) == str(Event()) - - -def test_eq_Event(): - assert Event() == Event() - - def test_Sub(): assert Subtitles() def test_str_Sub(): - assert ( - str(Subtitles()) - == """[Script Info] + assert str(Subtitles()) == """[Script Info] ScriptType: v4.00+ Title: Default File @@ -58,15 +40,6 @@ def test_str_Sub(): Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text """ - ) - - -def test_repr_Sub(): - assert repr(Subtitles()) == str(Subtitles()) - - -def test_eq_Sub(): - assert Subtitles() == Subtitles() def test_DrawCommand(): diff --git a/src/tests/unittest/test_i18n.py b/src/tests/unittest/test_i18n.py index 51a912b4..4c143b50 100644 --- a/src/tests/unittest/test_i18n.py +++ b/src/tests/unittest/test_i18n.py @@ -1,11 +1,5 @@ # -*- coding: utf-8 -*- -import gettext -import os -import sys - -import pytest - from Annotations2Sub.i18n import internationalization @@ -13,28 +7,6 @@ def test_internationalization(): assert internationalization() -def test_internationalization_FileNotFoundError(): - def f(*args, **kwargs): - raise FileNotFoundError - - m = pytest.MonkeyPatch() - m.setattr(gettext, "translation", f) - - assert internationalization() - - m.undo() - - -def test_internationalization_win32(): - m = pytest.MonkeyPatch() - m.setattr(sys, "platform", "win32") - m.setattr(os, "getenv", lambda x: None) - - assert internationalization() - - m.undo() - - def test_gettext(): _ = internationalization() assert _("警告: ") == "警告: " diff --git a/src/tests/unittest/test_utils.py b/src/tests/unittest/test_utils.py index 1e794967..1c7c4743 100644 --- a/src/tests/unittest/test_utils.py +++ b/src/tests/unittest/test_utils.py @@ -38,14 +38,6 @@ def test_Info(): Info("Test") -def test_Err1(): - Err1("Test") - - -def test_Warn1(): - Warn1("Test") - - def test_Err2(): Err2("Test") diff --git "a/\346\226\207\346\241\243.md" "b/\346\226\207\346\241\243.md" index f90846d0..84adc23b 100644 --- "a/\346\226\207\346\241\243.md" +++ "b/\346\226\207\346\241\243.md" @@ -462,7 +462,6 @@ subtitles_string = AnnotationsXmlStringToSubtitlesString(xml_string) with open('output.ass', 'w', encoding='utf-8') as f: f.write(subtitles_string) -# 如果你担心许可证问题, 可以联系我给你其他许可证. ``` ### 与同类软件的对比 @@ -503,3 +502,7 @@ Annotations2Sub: [Annotations2Sub-Lite](https://a2s.liutao.page/) AI 做的网页版本. + +[Ar tonelico 系列 中文字幕 (Rain Shimotsuki 注释字幕还原) 合集](https://www.bilibili.com/video/BV1Ff4y1t7Dj) + +本项目是此项目的衍生项目.