TG Video Check

针对 Telegram 导出场景的视频损坏检测 CLI,重点识别“下载被跳过或中断后留下的半截视频文件”,并支持后续统计、导出和批量删除。

项目目标

这个工具当前解决的是比较具体的一类问题,不是通用媒体法医工具:

  • Telegram 导出目录里存在大量视频文件
  • 一部分文件扩展名、文件名都正常,但内容其实已经不完整
  • 目标是尽快筛出高置信度坏文件,而不是对所有视频做最重的完整解码
  • 结果要能落盘、复跑、导出,并且最终支持批量删除

当前能力

  • 递归扫描一个或多个目录中的常见视频扩展名,默认覆盖 mp4/mov/m4v/mkv/webm/avi/flv/f4v/ts/m2ts/mts/mpg/mpeg/mpe/mpv/m2v/wmv/asf/3gp/3g2/ogv/ogm/vob/qt/rm/rmvb/mxf/divx
  • 支持从 txt 文件批量导入目录列表
  • 使用 SQLite 落盘扫描状态,并基于 path + size + mtime 复用旧结果
  • 数据库存储已改为“扫描批次 + 文件实体”两层模型:同一个未变化文件的 stage 结果跨 run 只保存一份
  • 当同一组 roots + profile 的扫描上次未完成时,scan --resume 会直接续跑同一个 run,不再把 discovery 结果整轮复制一遍
  • 当后续递归扫描的 roots 完全覆盖旧历史 run,例如先扫 a\b 再扫 a 时,命中的旧 run 会在当前 scan --resume 里自动压扁进当前 run
  • stage1 使用 ffprobe 做容器级快速校验
  • scan 默认只执行 stage1,把需要详细复核的文件标记为 pending_stage2
  • verify-stage2 只对 pending_stage2 文件使用 ffmpeg 做定向采样或完整确认
  • 扫描过程中输出 discovery、stage1、stage2 的阶段进度;长耗时阶段会持续输出 heartbeat
  • 输出 JSONL / CSV 报告
  • 支持按扫描结果做 confirmed_broken 文件删除预览和真实删除

目录结构

当前项目已经放在仓库根目录,核心内容如下:

在别的机器上使用

最稳的方式是把整个项目目录复制到目标机器,然后在目标机器本地重新创建虚拟环境并安装项目。不要直接把本机 .venv 原样复制过去继续用。

前置条件

目标机器至少需要:

  • Windows
  • Python 3.12+
  • ffprobe 可用
  • ffmpeg 可用

先验证:

python --version
ffprobe -version
ffmpeg -version

如果上面任一命令不可用,先把运行环境补齐。

方式 1:复制项目目录后本地安装

把项目目录复制到目标机器,例如:

D:\tools\clean-broken-tgvideos

然后进入项目根目录执行:

python -m venv .venv
.\.venv\Scripts\python.exe -m ensurepip
.\.venv\Scripts\python.exe -m pip install -e .

验证入口是否正常:

.\.venv\Scripts\tgvideo-check.exe --help
.\.venv\Scripts\tgvideo-check.exe scan --help

方式 2:如果目标机器也装了 uv

uv sync
uv run tgvideo-check --help

这种方式更省事,但前提是目标机器本身已经能使用 uv。

迁移后的最小验收

不要一开始就扫正式目录,先用一个小目录做最小验收:

.\.venv\Scripts\tgvideo-check.exe scan "D:\small-test" --db ".\small-test.db" --export-jsonl ".\small-test-stage1.jsonl" --export-csv ".\small-test-stage1.csv"
.\.venv\Scripts\tgvideo-check.exe verify-stage2 --db ".\small-test.db" --export-jsonl ".\small-test-final.jsonl" --export-csv ".\small-test-final.csv"

如果能先正常看到 scan 的 discovery/stage1 进度和摘要,再正常执行 verify-stage2 并生成最终 db/jsonl/csv 文件,说明目标机器环境基本没问题。

基本扫描用法

直接传目录

tgvideo-check scan "D:\Exports\ChatA" "D:\Exports\ChatB" --profile tg-hdd

第一轮完成后,如果要继续做第二轮详细复核:

tgvideo-check verify-stage2 --db .\scan_state.db

用 txt 批量导入目录

tgvideo-check scan --paths-file .\dirs.txt --db .\scan_state.db --export-jsonl .\results.jsonl --export-csv .\results.csv

完成第一轮后输出第二轮最终结果:

tgvideo-check verify-stage2 --db .\scan_state.db --export-jsonl .\results.final.jsonl --export-csv .\results.final.csv

混合使用

tgvideo-check scan "D:\extra-folder" --paths-file .\dirs.txt --db .\scan_state.db

常用参数

  • --db:指定 SQLite 状态库
  • --paths-file:从 txt 文件读取目录列表,一行一个目录
  • --export-jsonl / --export-csv:导出结果报告
  • scan --stage1-workers:覆盖第一轮 worker 数
  • verify-stage2 --stage2-workers:覆盖第二轮 worker 数
  • --extensions:覆盖默认视频扩展名列表
  • --queue-limit:限制单阶段批次大小,避免把待处理文件一次性堆进内存
  • verify-stage2 --full-decode-confirm:对第二轮强制使用完整解码确认
  • scan --resume / --no-resume:控制是否复用旧的第一轮结果
  • compact-db:清理同一组 roots + profile 下的重复历史 run,并可选执行 VACUUM
  • 打开旧版数据库时会自动迁移到新的实体去重存储模型

命令边界:

  • scan:只做第一轮快速筛查
  • verify-stage2:只做第二轮详细复核
  • inspect-stage1:排查第一轮崩溃前最后写入的 ffprobe 原始输出

默认扩展名列表:

mp4,mov,m4v,mkv,webm,avi,flv,f4v,ts,m2ts,mts,mpg,mpeg,mpe,mpv,m2v,wmv,asf,3gp,3g2,ogv,ogm,vob,qt,rm,rmvb,mxf,divx

说明:

  • tg-hdd / tg-fast 默认不再因为文件后缀直接升级到第二轮,只有 ffprobe 已经发现异常信号时才进入 pending_stage2
  • 如果你希望对全部已支持格式都走更激进的二次校验,使用 --profile deep-verify
  • 自动压扁历史 run 只在 scan 的递归模式下触发;如果旧 run 中有文件 identity 已变化,则该旧 run 不会被自动删除

--paths-file 规则

  • 按 UTF-8 / UTF-8 BOM 读取
  • 一行一个目录
  • 空行自动跳过
  • 行内空格、中文、方括号、感叹号等字符按字面值保留
  • 如果整行外面包了一层单引号或双引号,会自动去掉外层引号

示例 dirs.txt:

D:\TG Export A
"D:\带空格 和 [特殊字符] 的目录"
D:\普通目录

小目录测试建议

如果要先验证工具效果,不要一上来扫正式目录。更稳的做法是先准备一个小样本目录,例如:

  • 3 到 5 个确认正常的视频
  • 2 到 3 个你故意截断的坏视频
  • 5 到 10 个真实导出样本

推荐测试命令:

tgvideo-check scan "D:\small-test" --db ".\small-test.db" --export-jsonl ".\small-test-stage1.jsonl" --export-csv ".\small-test-stage1.csv"
tgvideo-check verify-stage2 --db ".\small-test.db" --export-jsonl ".\small-test-final.jsonl" --export-csv ".\small-test-final.csv"

重点看:

  • confirmed_broken 是否命中了你故意造坏的样本
  • pending_stage2 数量是否符合你对可疑样本的预期
  • passed_stage2 里是否混进了你明知已坏的文件
  • 是否误伤了你确认正常的视频

状态说明

导出的 final_status / final_status_zh 含义如下:

状态码 中文说明 含义
confirmed_broken 明确损坏 高置信度确认文件损坏,适合进入删除候选
suspicious 可疑 有异常迹象,但证据还不足以直接当作明确损坏
pending_stage2 待第二轮复核 第一轮认为值得进一步做详细验证
passed_stage1 第一轮通过 容器级快速校验通过,未进入第二轮
passed_stage2 第二轮通过 进入第二轮后仍通过采样或确认校验
skipped 已跳过 预留状态
command_error 命令异常 ffprobe / ffmpeg 执行失败,不应直接视为媒体损坏

Stage1 崩溃排查

如果 scan 在第一轮中途抛异常,例如卡在某个文件的 ffprobe JSON 解析阶段,不需要手动打开 SQLite 客户端。直接用内建命令查看最近一次扫描里,已经写入 stage1_results 但仍停在 pending_stage1 的记录:

tgvideo-check inspect-stage1 --db "C:\Users\Administrator\Documents\clean-tgvideo-data\scan_state.db"

常用变体:

  • --run-id 12:指定要检查的扫描批次
  • --limit 20:多看几条最近记录
  • --all:如果默认模式没有命中,就直接查看最近保存的 Stage1 原始结果,不再只限 pending_stage1

输出重点:

  • path=:最值得怀疑的文件路径
  • stdout_len=:ffprobe 标准输出长度;很短通常表示只吐出了半截 JSON
  • stdout_head=:标准输出开头预览,便于快速看出是不是残缺 JSON
  • stderr_head=:标准错误预览,常能直接看到 moov atom not found、error reading header 等真实媒体错误

压缩重复扫描数据库

如果你之前在同一批目录上反复 scan,导致同一组 roots + profile 下面堆积了多份历史 run,可以保留一个目标 run,把其余重复 run 删掉:

先预览:

tgvideo-check compact-db --db .\scan_state.db

真实执行:

tgvideo-check compact-db --db .\scan_state.db --no-dry-run

如果要明确保留某个 run:

tgvideo-check compact-db --db .\scan_state.db --run-id 12 --no-dry-run

说明:

  • 默认保留最新 run;如果你在做中断恢复,通常就保留当前正在续跑的那个 run
  • 这个命令只清理同一组 roots + profile 下的重复历史 run,不会顺手删除别的扫描任务
  • 真实删除时还会顺手清掉没有任何 run 引用的孤儿实体与孤儿 stage 结果
  • 默认会在真实删除后执行 VACUUM,把 SQLite 文件物理收缩下来

旧库迁移

如果你手头已经有旧版 scan_state.db,不需要手工导表。当前版本在第一次打开数据库时会自动完成:

  • 将旧版按 file_id 存储的 stage1_results / stage2_results 迁移成按文件实体存储
  • 对同一个 path + size + mtime + extension 的重复 stage 结果做去重
  • 保留各个 run 自己的状态行,只把可共享的检测结果合并

如果你还想顺手收缩物理文件,迁移后再跑一次:

tgvideo-check compact-db --db .\scan_state.db --no-dry-run

原因码说明

导出的 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 规则直接升级到第二轮验证 主要出现在显式使用 deep-verify 这类激进配置时
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 打开新文件

删除已确认损坏文件

先预览:

tgvideo-check prune --db .\scan_state.db

默认行为:

  • 默认 --dry-run
  • 默认只处理 confirmed_broken
  • 输出匹配数量、删除数量、总字节数、原因统计

确认后执行真实删除:

tgvideo-check prune --db .\scan_state.db --no-dry-run

如果后续你明确要扩大删除范围,可以传:

tgvideo-check prune --db .\scan_state.db --delete-status confirmed_broken,suspicious

但这不是默认推荐,因为 suspicious 仍然代表边界文件,不应默认直接删。

删除输出字段说明:

  • matched:命中删除条件的文件数
  • deleted:本次实际删除成功的文件数
  • missing:数据库里有记录,但磁盘上已经不存在的文件数
  • failed:尝试删除但失败的文件数
  • bytes:命中的文件总大小
  • Deleted bytes:本次实际删除成功的总大小

推荐删除流程:

  1. 先跑 prune --dry-run
  2. 看 matched 和 Reasons
  3. 必要时打开 CSV 抽查几条
  4. 确认没问题后再执行 --no-dry-run

性能 Smoke

仓库内提供了 tests/test_perf_smoke.py,默认跳过,不把环境波动硬编码成普通单测。

手动运行方式:

$env:TGVIDEO_RUN_PERF_SMOKE='1'
uv run pytest tests/test_perf_smoke.py -q

这个 smoke 目前只做两件事:

  • 生成一批最小测试视频
  • 分别跑 stage1_workers=1 和 stage1_workers=2,确认基线采集流程可执行

它不是严格 benchmark;真正的默认参数取舍,仍应基于真实 HDD 数据集人工复核。

S
Description
清理因点击跳过导致的tg视频损坏
Readme 266 KiB
Languages
Python 100%