docs: expand usage guide and localize csv output

This commit is contained in:
kim
2026-03-30 10:59:36 +08:00
parent 79370ed542
commit 4054cc87bb
3 changed files with 264 additions and 11 deletions
+204 -6
View File
@@ -1,22 +1,122 @@
## TG Video Check ## TG Video Check
针对 Telegram 导出场景的视频损坏检测 CLI,优先识别被中断下载导致的半截视频文件。 针对 Telegram 导出场景的视频损坏检测 CLI,重点识别“下载被跳过或中断后留下的半截视频文件”,并支持后续统计、导出和批量删除。
### 项目目标
这个工具当前解决的是比较具体的一类问题,不是通用媒体法医工具:
- Telegram 导出目录里存在大量视频文件
- 一部分文件扩展名、文件名都正常,但内容其实已经不完整
- 目标是尽快筛出高置信度坏文件,而不是对所有视频做最重的完整解码
- 结果要能落盘、复跑、导出,并且最终支持批量删除
### 当前能力 ### 当前能力
- 递归扫描一个或多个目录中的常见视频扩展名 - 递归扫描一个或多个目录中的常见视频扩展名
- 支持从 `txt` 文件批量导入目录列表
- 使用 SQLite 落盘扫描状态,并基于 `path + size + mtime` 复用旧结果 - 使用 SQLite 落盘扫描状态,并基于 `path + size + mtime` 复用旧结果
- stage1 使用 `ffprobe` 做容器级快速校验 - stage1 使用 `ffprobe` 做容器级快速校验
- stage2 只对可疑文件使用 `ffmpeg` 做定向采样或完整确认 - stage2 只对可疑文件使用 `ffmpeg` 做定向采样或完整确认
- 输出 JSONL / CSV 报告 - 输出 JSONL / CSV 报告
- 支持按扫描结果做 `confirmed_broken` 文件删除预览和真实删除
### 基本用法 ### 目录结构
当前项目已经放在仓库根目录,核心内容如下:
- [pyproject.toml](D:\PyCharm_projects\clean-broken-tgvideos\pyproject.toml):项目入口和依赖定义
- [src/tgvideo_check/cli.py](D:\PyCharm_projects\clean-broken-tgvideos\src\tgvideo_check\cli.py):CLI 主入口
- [src/tgvideo_check/store.py](D:\PyCharm_projects\clean-broken-tgvideos\src\tgvideo_check\store.py):SQLite 状态落盘
- [src/tgvideo_check/reporting.py](D:\PyCharm_projects\clean-broken-tgvideos\src\tgvideo_check\reporting.py):JSONL / CSV 导出
- [tests/](D:\PyCharm_projects\clean-broken-tgvideos\tests):单元测试、集成测试、性能 smoke
### 在别的机器上使用
最稳的方式是把整个项目目录复制到目标机器,然后在目标机器本地重新创建虚拟环境并安装项目。不要直接把本机 `.venv` 原样复制过去继续用。
#### 前置条件
目标机器至少需要:
- Windows
- Python `3.12+`
- `ffprobe` 可用
- `ffmpeg` 可用
先验证:
```powershell ```powershell
uv run tgvideo-check scan D:\Exports\ChatA D:\Exports\ChatB --profile tg-hdd python --version
ffprobe -version
ffmpeg -version
``` ```
常用参数: 如果上面任一命令不可用,先把运行环境补齐。
#### 方式 1:复制项目目录后本地安装
把项目目录复制到目标机器,例如:
```text
D:\tools\clean-broken-tgvideos
```
然后进入项目根目录执行:
```powershell
python -m venv .venv
.\.venv\Scripts\python.exe -m ensurepip
.\.venv\Scripts\python.exe -m pip install -e .
```
验证入口是否正常:
```powershell
.\.venv\Scripts\tgvideo-check.exe --help
.\.venv\Scripts\tgvideo-check.exe scan --help
```
#### 方式 2:如果目标机器也装了 uv
```powershell
uv sync
uv run tgvideo-check --help
```
这种方式更省事,但前提是目标机器本身已经能使用 `uv`。
#### 迁移后的最小验收
不要一开始就扫正式目录,先用一个小目录做最小验收:
```powershell
.\.venv\Scripts\tgvideo-check.exe scan "D:\small-test" --db ".\small-test.db" --export-jsonl ".\small-test.jsonl" --export-csv ".\small-test.csv"
```
如果能正常看到扫描摘要,并生成 `db/jsonl/csv` 三个文件,说明目标机器环境基本没问题。
### 基本扫描用法
#### 直接传目录
```powershell
tgvideo-check scan "D:\Exports\ChatA" "D:\Exports\ChatB" --profile tg-hdd
```
#### 用 txt 批量导入目录
```powershell
tgvideo-check scan --paths-file .\dirs.txt --db .\scan_state.db --export-jsonl .\results.jsonl --export-csv .\results.csv
```
#### 混合使用
```powershell
tgvideo-check scan "D:\extra-folder" --paths-file .\dirs.txt --db .\scan_state.db
```
### 常用参数
- `--db`:指定 SQLite 状态库 - `--db`:指定 SQLite 状态库
- `--paths-file`:从 txt 文件读取目录列表,一行一个目录 - `--paths-file`:从 txt 文件读取目录列表,一行一个目录
@@ -24,8 +124,9 @@ uv run tgvideo-check scan D:\Exports\ChatA D:\Exports\ChatB --profile tg-hdd
- `--stage1-workers` / `--stage2-workers`:覆盖 profile 默认 worker 数 - `--stage1-workers` / `--stage2-workers`:覆盖 profile 默认 worker 数
- `--queue-limit`:限制单阶段批次大小,避免把待处理文件一次性堆进内存 - `--queue-limit`:限制单阶段批次大小,避免把待处理文件一次性堆进内存
- `--full-decode-confirm`:对 stage2 强制使用完整解码确认 - `--full-decode-confirm`:对 stage2 强制使用完整解码确认
- `--resume / --no-resume`:控制是否复用旧结果
`--paths-file` 规则: ### `--paths-file` 规则
- 按 `UTF-8` / `UTF-8 BOM` 读取 - 按 `UTF-8` / `UTF-8 BOM` 读取
- 一行一个目录 - 一行一个目录
@@ -33,6 +134,87 @@ uv run tgvideo-check scan D:\Exports\ChatA D:\Exports\ChatB --profile tg-hdd
- 行内空格、中文、方括号、感叹号等字符按字面值保留 - 行内空格、中文、方括号、感叹号等字符按字面值保留
- 如果整行外面包了一层单引号或双引号,会自动去掉外层引号 - 如果整行外面包了一层单引号或双引号,会自动去掉外层引号
示例 `dirs.txt`:
```text
D:\TG Export A
"D:\带空格 和 [特殊字符] 的目录"
D:\普通目录
```
### 小目录测试建议
如果要先验证工具效果,不要一上来扫正式目录。更稳的做法是先准备一个小样本目录,例如:
- 3 到 5 个确认正常的视频
- 2 到 3 个你故意截断的坏视频
- 5 到 10 个真实导出样本
推荐测试命令:
```powershell
tgvideo-check scan "D:\small-test" --db ".\small-test.db" --export-jsonl ".\small-test.jsonl" --export-csv ".\small-test.csv"
```
重点看:
- `confirmed_broken` 是否命中了你故意造坏的样本
- `passed_stage2` 里是否混进了你明知已坏的文件
- 是否误伤了你确认正常的视频
### 状态说明
导出的 `final_status` / `final_status_zh` 含义如下:
| 状态码 | 中文说明 | 含义 |
|---|---|---|
| `confirmed_broken` | 明确损坏 | 高置信度确认文件损坏,适合进入删除候选 |
| `suspicious` | 可疑 | 有异常迹象,但证据还不足以直接当作明确损坏 |
| `passed_stage1` | 第一轮通过 | 容器级快速校验通过,未进入第二轮 |
| `passed_stage2` | 第二轮通过 | 进入第二轮后仍通过采样或确认校验 |
| `skipped` | 已跳过 | 预留状态 |
| `command_error` | 命令异常 | `ffprobe` / `ffmpeg` 执行失败,不应直接视为媒体损坏 |
### 原因码说明
导出的 `reason_code` / `reason_code_zh` 含义如下:
| 原因码 | 中文说明 | 典型含义 |
|---|---|---|
| `ffprobe_exit_nonzero` | ffprobe 返回非零退出码,通常表示容器或头部校验失败 | 第一轮就明显坏了 |
| `ffmpeg_exit_nonzero` | ffmpeg 返回非零退出码,通常表示采样或解码失败 | 第二轮校验失败 |
| `invalid_nal_unit_size` | 视频流 NAL 单元尺寸异常,常见于截断文件 | 码流被截断或破损 |
| `truncating_packet` | 视频包被截断,常见于下载中断或文件不完整 | 明显的半截文件特征 |
| `decode_slice_header_error` | 视频切片头解析失败,通常表示码流损坏 | 解码阶段发现流损坏 |
| `corrupt_input_packet` | 输入数据包损坏,码流存在明显异常 | 包级别损坏 |
| `hard_error_keyword` | 命中高置信度损坏错误关键词 | 第一轮错误信息直接确认坏 |
| `soft_warning_keyword` | 命中软告警关键词,因此升级到第二轮验证 | 第一轮不够致命,但值得复核 |
| `missing_video_stream` | 没有检测到视频流 | 文件不是有效视频 |
| `missing_format_name` | 无法识别容器格式 | 容器头部或格式识别失败 |
| `invalid_duration` | 时长字段异常,因此升级到第二轮验证 | 容器信息不完整或异常 |
| `high_risk_extension` | 高风险扩展名,因此升级到第二轮验证 | 根据 profile 规则升级复核 |
| `passed_stage1` | 容器级快速校验通过 | 第一轮结束即可放行 |
| `passed_stage2` | 第二轮采样或确认校验通过 | 第二轮也没发现问题 |
| `command_error` | 外部命令执行异常 | 环境问题,不应直接删文件 |
| `missing_command` | 缺少 ffprobe 或 ffmpeg 外部命令 | 机器环境不完整 |
| `invalid_probe_output` | ffprobe 输出不是可解析的 JSON | 外部命令输出异常 |
| `timeout` | 命令执行超时 | 文件过大、设备慢或进程卡住 |
### CSV 与 Office
当前 CSV 导出采用 `UTF-8 BOM`,并附带中文说明列:
- `final_status_zh`
- `reason_code_zh`
这是为了尽量兼容 Windows 上的 Office / Excel 默认打开行为,减少中文乱码问题。
如果你已经打开过旧版 CSV,建议:
1. 关闭旧文件
2. 用当前版本重新导出 CSV
3. 再重新用 Office 打开新文件
### 删除已确认损坏文件 ### 删除已确认损坏文件
先预览: 先预览:
@@ -61,9 +243,25 @@ tgvideo-check prune --db .\scan_state.db --delete-status confirmed_broken,suspic
但这不是默认推荐,因为 `suspicious` 仍然代表边界文件,不应默认直接删。 但这不是默认推荐,因为 `suspicious` 仍然代表边界文件,不应默认直接删。
删除输出字段说明:
- `matched`:命中删除条件的文件数
- `deleted`:本次实际删除成功的文件数
- `missing`:数据库里有记录,但磁盘上已经不存在的文件数
- `failed`:尝试删除但失败的文件数
- `bytes`:命中的文件总大小
- `Deleted bytes`:本次实际删除成功的总大小
推荐删除流程:
1. 先跑 `prune --dry-run`
2. 看 `matched` 和 `Reasons`
3. 必要时打开 CSV 抽查几条
4. 确认没问题后再执行 `--no-dry-run`
### 性能 Smoke ### 性能 Smoke
仓库内提供了 `tests/test_perf_smoke.py`,默认跳过,不把环境波动硬编码成普通单测。 仓库内提供了 [tests/test_perf_smoke.py](D:\PyCharm_projects\clean-broken-tgvideos\tests\test_perf_smoke.py),默认跳过,不把环境波动硬编码成普通单测。
手动运行方式: 手动运行方式:
+51 -2
View File
@@ -15,6 +15,38 @@ class ReportRow:
size: int size: int
FINAL_STATUS_ZH: dict[str, str] = {
"confirmed_broken": "明确损坏",
"suspicious": "可疑",
"passed_stage1": "第一轮通过",
"passed_stage2": "第二轮通过",
"skipped": "已跳过",
"command_error": "命令异常",
}
REASON_CODE_ZH: dict[str, str] = {
"ffprobe_exit_nonzero": "ffprobe 返回非零退出码,通常表示容器或头部校验失败",
"ffmpeg_exit_nonzero": "ffmpeg 返回非零退出码,通常表示采样或解码失败",
"invalid_nal_unit_size": "视频流 NAL 单元尺寸异常,常见于截断文件",
"truncating_packet": "视频包被截断,常见于下载中断或文件不完整",
"decode_slice_header_error": "视频切片头解析失败,通常表示码流损坏",
"corrupt_input_packet": "输入数据包损坏,码流存在明显异常",
"hard_error_keyword": "命中高置信度损坏错误关键词",
"soft_warning_keyword": "命中软告警关键词,因此升级到第二轮验证",
"missing_video_stream": "没有检测到视频流",
"missing_format_name": "无法识别容器格式",
"invalid_duration": "时长字段异常,因此升级到第二轮验证",
"high_risk_extension": "高风险扩展名,因此升级到第二轮验证",
"passed_stage1": "容器级快速校验通过",
"passed_stage2": "第二轮采样或确认校验通过",
"command_error": "外部命令执行异常",
"missing_command": "缺少 ffprobe 或 ffmpeg 外部命令",
"invalid_probe_output": "ffprobe 输出不是可解析的 JSON",
"timeout": "命令执行超时",
}
def summarize_reason_counts(rows: list[ReportRow]) -> dict[str, int]: def summarize_reason_counts(rows: list[ReportRow]) -> dict[str, int]:
summary: dict[str, int] = {} summary: dict[str, int] = {}
for row in rows: for row in rows:
@@ -44,18 +76,35 @@ def export_jsonl(rows: list[ReportRow], destination: Path) -> None:
def export_csv_summary(rows: list[ReportRow], destination: Path) -> None: def export_csv_summary(rows: list[ReportRow], destination: Path) -> None:
with destination.open("w", encoding="utf-8", newline="") as handle: with destination.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter( writer = csv.DictWriter(
handle, handle,
fieldnames=["final_status", "reason_code", "path", "size"], fieldnames=[
"final_status",
"final_status_zh",
"reason_code",
"reason_code_zh",
"path",
"size",
],
) )
writer.writeheader() writer.writeheader()
for row in rows: for row in rows:
writer.writerow( writer.writerow(
{ {
"final_status": row.final_status, "final_status": row.final_status,
"final_status_zh": describe_final_status_zh(row.final_status),
"reason_code": row.reason_code, "reason_code": row.reason_code,
"reason_code_zh": describe_reason_code_zh(row.reason_code),
"path": str(row.path), "path": str(row.path),
"size": row.size, "size": row.size,
} }
) )
def describe_final_status_zh(status: str) -> str:
return FINAL_STATUS_ZH.get(status, status)
def describe_reason_code_zh(reason_code: str) -> str:
return REASON_CODE_ZH.get(reason_code, reason_code)
+9 -3
View File
@@ -56,8 +56,14 @@ def test_export_jsonl_and_csv_write_expected_content(tmp_path: Path) -> None:
export_csv_summary(sample_rows(), csv_path) export_csv_summary(sample_rows(), csv_path)
jsonl_text = jsonl_path.read_text(encoding="utf-8") jsonl_text = jsonl_path.read_text(encoding="utf-8")
csv_text = csv_path.read_text(encoding="utf-8") csv_text = csv_path.read_text(encoding="utf-8-sig")
csv_bytes = csv_path.read_bytes()
assert '"path":"a.mp4"' in jsonl_text assert '"path":"a.mp4"' in jsonl_text
assert "final_status,reason_code,path,size" in csv_text assert csv_bytes.startswith(b"\xef\xbb\xbf")
assert "confirmed_broken,ffprobe_exit_nonzero,a.mp4,100" in csv_text assert (
"final_status,final_status_zh,reason_code,reason_code_zh,path,size"
in csv_text
)
assert "confirmed_broken,明确损坏,ffprobe_exit_nonzero" in csv_text
assert "ffprobe 返回非零退出码" in csv_text