Files
xwlb/docs/ARCHITECTURE.md
T
simon c67a4ce367 feat: 日志轮转(保留 14 天)
main.log 无轮转、已涨到 34MB,且每晚任务都会继续追加。

- logrotate/xwlb 模板 + scripts/install_logrotate.sh(幂等,装到 /etc/logrotate.d/xwlb)
- 每天轮转、保留 14 份、立即压缩 → main.log-YYYYMMDD.gz
- 必须用 copytruncate:脚本以 >>main.log 长开句柄,改名式轮转会
  让当晚日志写进归档、新文件空着
- journald 同步设 MaxRetentionSec=14day + SystemMaxUse=500M
- 安装脚本自带 logrotate -d 校验(这条立刻抓到"指令行尾注释"被当成参数值的错误)
- tests/test_logrotate.py 19 项,锁住保留天数、行尾注释、占位符替换

实测:34MB → 2.9MB,归档 32453 行与轮转前完全一致
2026-09-25 12:49:53 +08:00

30 KiB
Raw Blame History

xwlb 项目技术说明

一句话定位:把央视《新闻联播》完整版视频自动转成结构化单条新闻并入库的离线流水线。 本文基于仓库当前源码 + main.log(2026-06-01 ~ 2026-09-23)实测行为整理。缺陷清单见 BUGS.md。


1. 端到端数据流

                    main.py(今天)
                         │  start_date = end_date = %Y%m%d
                         ▼
   ┌──────────────────────────────────────────────────────────────┐
   │ ① 链接发现  getVideo5.get_all_video_links(start, end)         │
   │   xwlb_urls()          → https://tv.cctv.com/lm/xwlb/day/     │
   │                          YYYYMMDD.shtml (按天,含端点)        │
   │   get_xwlb_video_link()→ 正则/文本匹配「完整版《新闻联播》」     │
   │                          的 <a>,得到 VID 详情页 URL           │
   │                          例 tv.cctv.com/2024/10/30/VID...shtml │
   ├──────────────────────────────────────────────────────────────┤
   │ ② 下载与抽音  download_and_extract_audio(url, date, dir)       │
   │   yt-dlp  format=best[ext=mp4]/best → xwlb_video/YYYYMMDD.mp4  │
   │   ffmpeg  -c:a libmp3lame -q:a 0 -map a → 同名 .mp3            │
   ├──────────────────────────────────────────────────────────────┤
   │ ③ 转写落库  audioRead.process_long_audio(mp3, out_dir, date)   │
   │   convert_mp3_to_wav()        16kHz / 单声道 / 16bit → input.wav│
   │   split_audio_by_smart_silence(audio_split.*,默认 700ms/-40)  │
   │       → 贪心合并为 ≤3 分钟分片 chunk_i.wav(实测 11 片/天)      │
   │   for each chunk:                                             │
   │       transcribe_audio()  models.asr_model(paraformer-...v2) │
   │            └─ merge_transcripts()  句级结果空格拼接             │
   │            └─ analyze_and_correct_text() models.correct_model   │
   │                 (默认 qwen3.7-max;受数值事实守卫约束)        │
   │       → 先删后插 xwlb_daily(news_days, daily_sub_id=i, ...)    │
   │       → os.remove(chunk)                                       │
   ├──────────────────────────────────────────────────────────────┤
   │ ④ 切分入库  newsProcess.news_to_db(date)                       │
   │   读 xwlb_daily.news_improve 按 daily_sub_id 升序 → '\n' 拼接   │
   │   models.split_model(deepseek-chat,json_object)             │
   │       prompt:切分为独立新闻 + 每条起标题(含"国内/国际/联播快讯")│
   │   → 解析 [{news_id, news_title, news_content}]                 │
   │   → 删除该日期旧记录 → 批量 INSERT xwlb_daily_ext               │
   ├───────────────────────────────────────────────────────────────┤
   │ ⑤ 清理  cleanup.maybe_cleanup_after_run(date, day_ok)          │
   │    当天全部成功才清空 xwlb_video/*.mp3|*.mp4、audio_processing/*.wav │
   └───────────────────────────────────────────────────────────────┘

两道 LLM 的分工

阶段 接入点 模型 输入 输出 落库位置
校对 routes.correct(默认 dashscope) models.correct_model(默认 qwen3.7-max) 单分片 ASR 原始文本 "修正后全文" xwlb_daily.news_improve
切分 routes.split(默认 deepseek) models.split_model(默认 deepseek-chat) 当天全部分片拼接文本 JSON 数组 xwlb_daily_ext 多行

模型名(models.*)与接入点(endpoints.* + routes.*)统一在 config.yml 管理, 换模型或换供应商都不需要动代码(详见 第 4 节)。


2. 模块职责与函数索引

文件 关键函数 说明
main.py — 入口:当天日期 → process_videos
main_videos.py get_missing_dates(start, end) 反查 xwlb_daily 缺哪些日期,逐日补跑(文件内示例区间写死为 2025-01-01~2025-10-25)
newsRedo.py _parse_date / main 手动重跑某天:ext>5 条→跳过;xwlb_daily 有行→只做 AI 切分;无行→跑全流程。支持 yyyymmdd 与 yyyy-mm-dd
getVideo5.py xwlb_urls 生成按天页面 URL 列表
get_xwlb_video_link 解析单日页面找完整版链接(返回值有缺陷,见 BUGS B4)
get_all_video_links 逐日聚合
download_and_extract_audio yt-dlp + ffmpeg
process_videos(start, end) 主流程编排:下载 → 转写 → 切分入库 → 成功后清理中间产物
config.py load_config / get / get_int / get_bool / get_list 加载 config.yml,环境变量可覆盖(XWLB_CONFIG_FILE 换文件)
model(role) 取 models.asr_model / correct_model / split_model
endpoint / route / endpoint_for 接入点与路由:routes.<role> → endpoints.<name>,配置不全时抛可读错误
openai_url(role) / api_key_env(role) / api_key(role) OpenAI 兼容请求地址拼接、该环节密钥的变量名 / 变量值
apply_dashscope_endpoints 把 dashscope 接入点地址写入 SDK(env + 模块属性,不依赖 import 顺序)
validate_endpoints / required_secrets / missing_secrets / check_secrets 启动校验;必需密钥按路由推导(只要求用到的那几家)
video_dir / audio_dir / db_config 目录解析(相对项目根)、数据库参数(口令来自 .env)
log_summary 启动打印生效配置与各环节接入点(不含敏感项)并校验
cleanup.py maybe_cleanup_after_run(date, day_ok) 当天结束后按 cleanup.* 决定是否清理
cleanup_intermediates 删除 *.mp3/*.mp4 与 *.wav,返回删除数/字节数;也可命令行单独执行
audioRead.py convert_mp3_to_wav mp3 → 16k 单声道 wav(采样率见 asr.sample_rate)
split_audio_by_fixed_duration 定长切分(当前未启用)
split_audio_by_smart_silence 实际使用:静音切分 + ≤3 分钟合并
transcribe_audio 单分片 ASR,只返回文本(失败返回 '';校对已解耦)
_extract_llm_text 兼容 output.text 与 output.choices[0].message.content 两种返回形态
merge_transcripts 句列表 → 空格拼接
text_correction qwen 校对:显式 timeout + 重试,失败抛异常
_number_signature / _number_drift 数值事实守卫:中文/阿拉伯数字归一后比较数值签名,检测校对是否篡改事实
analyze_and_correct_text 失败一律回退原文;数值事实被改动时同样回退原文;llm_correct.enabled=0 可关闭
analyze_text 通用 qwen 文本分析(当前未使用)
_upsert_daily_chunk 按 (news_days, daily_sub_id) 先删后插,实现幂等
process_long_audio 单日音频全流程 + 逐片写 xwlb_daily + 清理分片临时文件;返回 {'chunks','ok','failed'}
newsProcess.py normalize_date YYYYMMDD / YYYY-MM-DD 统一为后者
get_news_improve_by_date 取当天 news_improve 拼接(过滤空白行);无内容返回 None
extract_news_rows 宽解析 LLM 返回(剥 ``` 围栏 / raw_decode / 形态归一 / 字段校验),失败抛 ValueError
news_to_db(target_date, force=False) 切分入库:失败重试一次 → 按日期替换写入 xwlb_daily_ext;返回 written/skipped/failed
deepseek.py DeepSeekAPI(role=...) OpenAI 兼容客户端:地址/密钥变量名来自接入点配置;3 次重试、429/5xx 退避;400/401 不重试
deepseek_text(text, prompt, ...) 切分便捷入口(routes.split,json_object 模式)
openai_chat(text, prompt, role='correct', ...) 通用对话入口:供非 DashScope 供应商的校对使用(不强制 JSON)
env.py _load_dotenv 只加载敏感项(口令/Key):XWLB_ENV_FILE → 项目内 .env → 上级 .env,已存在的不覆盖
missing_secrets / check_secrets 校验 MYSQL_PASSWORD / DASHSCOPE_API_KEY / DEEPSEEK_API_KEY 是否齐全
mysql_handler.py MySQLDB 项目内置的数据库层(原位于父项目 utils/),接口与原实现一致,另加 execute() / insert_many() / query_one() / 上下文管理器;连接失败立即抛错并给出修复指引
mysqlHandle.py — 转发项目内 mysql_handler.MySQLDB
tests/test_news_parse.py — B3 解析逻辑回归测试(含生产日志中的真实坏返回)
tests/test_fidelity_guard.py — 数值事实守卫回归测试(含 qwen 真实篡改样例)
tests/test_config.py — 配置加载、环境变量优先级、敏感项与 config.yml 隔离
tests/test_cleanup.py — 清理开关组合(用临时目录与临时 config.yml,不触碰真实文件)

MySQLDB 接口约定(调用方依赖,重写时必须保持一致)

db = MySQLDB()                                              # 无参:地址来自 config.yml,口令来自 .env
rows = db.query_data(table="t", columns="c1,c2", where="k = %s", params=(v,))  # → list[dict]
new_id = db.insert_data("t", {"col": val})                  # → lastrowid
db.update_data("t", {"col": val}, "k = %s", (v,))           # → rowcount
db.close()                                                  # 可重复调用

where 子句允许携带 order by(调用方已在用,如 newsProcess.py)。 项目内置实现另提供:execute(sql, params) → rowcount(DELETE/DDL)、insert_many(table, rows)(单事务批量)、query_one(...) → dict|None、以及 with MySQLDB() as db:。

3. 数据库结构

xwlb_daily — 音频分片级原文

字段 类型 说明
nid int PK auto_increment
news_days date 日期
daily_sub_id int 分片序号(当前 = chunk 下标 i,0 起;无唯一约束)
news_raw text ASR 原始文本
news_improve text qwen 校对后文本(当前实际等于 raw)
news_title text 恒为空串(原设计留给分片标题)

xwlb_daily_ext — 单条新闻(最终产物)

字段 类型 说明
extid int PK auto_increment
news_date date 日期
sub_id tinyint 新闻序号(来自 LLM 的 news_id,1 起)
news_title varchar(256) 截断到 256
news_content text 正文

建议补的约束:xwlb_daily 唯一索引 (news_days, daily_sub_id);xwlb_daily_ext 唯一索引 (news_date, sub_id)。


4. 配置项(分层:敏感在 .env,其余在 config.yml)

分层原则:口令 / API Key 只放 .env(不入版本库);其余全部放 config.yml(可入版本库), 包含各供应商的 base url(endpoints.*)与每个环节走哪条线(routes.*)。 优先级:进程环境变量 > config.yml > 代码内置默认值 (便于临时试验,如 DASHSCOPE_LLM_MODEL=qwen-max python audioRead.py 20260904)。

.env 查找顺序:XWLB_ENV_FILE 指定路径 → 项目目录内 .env → 上一级 / 上两级 .env(兼容旧 djapi 布局)。 已存在的进程环境变量优先,不会被 .env 覆盖。启动时打印实际来源,例如:

已加载敏感配置 /home/pi/project/xwlb/.env(注入 11 个变量)
配置来源: /home/pi/project/xwlb/config.yml | MySQL myquant@localhost:13306/myquant | 模型 asr=... correct=... split=... | 校对=开启 | 完成后清理=开启

4.1 .env —— 只有敏感项

变量 必需 用途
MYSQL_PASSWORD ✅ 数据库口令
DASHSCOPE_API_KEY 按路由 ASR + 文本校对(由 endpoints.dashscope.api_key_env 指定)
DEEPSEEK_API_KEY 按路由 新闻切分 + 标题(由 endpoints.deepseek.api_key_env 指定)
其他 *_API_KEY 按路由 换供应商后在 endpoints.<新接入点>.api_key_env 里声明什么名字,这里就放什么
XWLB_ENV_FILE 指定其他 .env 路径
XWLB_CONFIG_FILE 指定其他 config.yml 路径

密钥变量名不写死在代码里:config.required_secrets() 按 routes 反查用到的接入点, 只有被使用的供应商才要求配 Key;缺失时 config.check_secrets() 直接 ERROR,而不是等到 401 才发现。

4.2 config.yml —— 非敏感项(含各环节模型)

配置项 默认 用途
mysql.host / port / user / database localhost / 13306 / myquant / myquant 数据库地址;经 SSH 隧道时端口为 13306(autossh.sh 转本地 13306 → 远端 3306)
mysql.tunnel.enabled / script / systemd_unit / wait_seconds / connect_timeout 1 / autossh.sh / xwlb-tunnel.service / 30 / 2 隧道自愈:端口不通时的处理(见 tunnel.py)
paths.video_dir / audio_dir xwlb_video / audio_processing 相对项目根,也可写绝对路径
models.asr_model paraformer-realtime-v2 语音识别模型
models.correct_model qwen3.8-flash ASR 文本校对模型
models.split_model deepseek-flash 新闻切分 + 标题模型(API 实际只提供 deepseek-flash / deepseek-v4-pro)
endpoints.<名>.kind — 协议类型:dashscope(用 SDK)/ openai(OpenAI 兼容 /chat/completions)
endpoints.<名>.base_url / chat_completions_path https://api.deepseek.com/v1 / /chat/completions openai 类型的地址(拼接成完整请求 URL)
endpoints.<名>.http_base_url / websocket_base_url https://dashscope.aliyuncs.com/api/v1 / wss://.../api-ws/v1/inference dashscope 类型:文本生成走 HTTP、实时 ASR 走 WS
endpoints.<名>.api_key_env DASHSCOPE_API_KEY 等 该供应商密钥在 .env 中的变量名
endpoints.<名>.extra_body — 供应商特有请求体参数,如关闭思维链。参数名因厂商而异(通义 enable_thinking、DeepSeek thinking),写错会被静默忽略
routes.asr / correct / split dashscope / qwen / deepseek 每个环节走哪个接入点(换供应商改这里)
llm_correct.extra_body / llm_split.extra_body — 环节级覆盖接入点的 extra_body(优先级更高)
asr.sample_rate / language_hints 16000 / [zh, en] ASR 输入要求
audio_split.min_silence_ms / silence_thresh_db / keep_silence_ms / max_chunk_ms 700 / -40 / 400 / 180000 静音切分参数
llm_correct.enabled 1 校对开关:0 = 直接把 ASR 原文作为 news_improve(开启时仍受数值事实守卫保护)
llm_correct.max_retries / timeout / max_tokens / temperature / top_p 2 / 120 / 8000 / 0.1 / 0.5 校对调用参数
llm_split.max_tokens / retry_max_tokens / temperature / timeout / max_retries 60000 / 80000 / 0.5 / 180 / 3 切分调用参数;推理模型的 reasoning token 也计入 max_tokens,实测波动 14k~24k,给少了会截断甚至返回空
cleanup.after_daily_run 1 当天全程任务结束后是否自动清理中间产物
cleanup.only_on_success 1 仅当天全部成功才清理;0 = 无论成败都清理
cleanup.remove_video / remove_audio 1 / 1 分别控制删除 *.mp3/*.mp4 与 *.wav

改模型只改 models.* 三行即可,不需要动代码。

推理模型(thinking)的取舍(实测数据见 REPORT_raw_vs_improve.md): 校对是机械任务,关掉思维链省约 13 倍 token 且输出几乎不变; 切分关掉会丢 4.7% 正文、少切 7 条新闻,必须保留思考。

4.3 环境准备

# 0) 端口隧道(本机访问 13306;不通时执行)
bash autossh.sh          # autossh -M 0 -fN -L 13306:localhost:3306 tunnel@doorcome.cn

# 1) 虚拟环境(本机 Python 为 externally-managed,必须用 venv)
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# 注:Python 3.13 已移除标准库 audioop,pydub 需要 requirements 中的 audioop-lts

# 2) 配置
cp .env.example .env     # 只填 3 个敏感项:MYSQL_PASSWORD / DASHSCOPE_API_KEY / DEEPSEEK_API_KEY
vim config.yml           # 非敏感项与模型(已随仓库提供,按需修改)

# 3) 自检(四个测试都不依赖网络与真实 API)
.venv/bin/python tests/test_config.py                # 配置加载与优先级
.venv/bin/python tests/test_fidelity_guard.py        # 数值事实守卫
.venv/bin/python tests/test_news_parse.py            # LLM 返回解析
.venv/bin/python tests/test_cleanup.py               # 清理开关组合(用临时目录,不动真实文件)
.venv/bin/python -c "import config; config.log_summary()"
.venv/bin/python -c "from mysqlHandle import MySQLDB; d=MySQLDB(); print(d.query_one('xwlb_daily','COUNT(*) c')); d.close()"

系统依赖:ffmpeg(/usr/bin/ffmpeg,抽音必需)。


5. 外部调用参数(实测/代码)

调用 参数要点 实测耗时
央视页面 requests,UA 伪装,Referer: https://tv.cctv.com/,timeout 10s 秒级
yt-dlp best[ext=mp4]/best,输出 YYYYMMDD.mp4 30 分钟视频 / 104MB,约 20s1min
ffmpeg -c:a libmp3lame -q:a 0 -map a -y 十几秒
pydub 切分 静音 700ms / 阈值 -40dBFS / 保留 400ms / 单片 ≤180s 秒级,得 11 片
paraformer ASR wav / 16k / language_hints=['zh','en'] 约 30~45s/片
qwen 校对 max_tokens=8000(可配)、temperature=0.1、top_p=0.5、timeout=90s、重试 2 次 改造后实测 20~32s/片且有真实产出;改造前 30~300s 且必然拿到空结果(57 次超时)
deepseek-chat json_object、max_tokens=20000(重试时 32000)、temperature=0.5、timeout 60s、3 重试 实测 27~35s

单日耗时:改造前日志实测 20:34:02 → 21:24:50,约 51 分钟(其中相当部分耗在"必然失败"的校对调用上)。 按改造后实测单片耗时(ASR 3045s + 校对 2032s)估算,11 片约 1014 分钟,加上下载、切分与片段处理,单日约 **2535 分钟**。


6. 存储布局

xwlb/
├── main.py / main_videos.py / newsRedo.py      # 入口
├── getVideo5.py / audioRead.py                 # 采集 + 转写
├── newsProcess.py / deepseek.py                # LLM 切分
├── config.yml / config.py                      # 非敏感配置 + 加载器(含各环节模型)
├── env.py                                      # 敏感配置(.env)加载
├── cleanup.py                                  # 中间产物清理(也是手动清理入口)
├── mysql_handler.py / mysqlHandle.py           # 数据库层(已内置,不再依赖父项目)
├── .env / .env.example                         # 敏感项(口令/Key) / 模板
├── requirements.txt
├── xwlb_video/       YYYYMMDD.mp3(及 .mp4)    # 当天任务成功后自动清空
├── audio_processing/ input_YYYYMMDD.wav + chunk_*.wav   # 当天任务成功后自动清空
├── docs/             BUGS.md / ARCHITECTURE.md
├── tests/            test_config.py / test_cleanup.py / test_fidelity_guard.py / test_news_parse.py
├── .venv/            项目虚拟环境(不入版本库)
├── autossh.sh        SSH 隧道:本地 13306 → 远端 3306
└── main.log          历史生产日志 34MB(证据保留,无轮转)

命名约定:中间产物以 YYYYMMDD 为键(无横线),数据库字段用 YYYY-MM-DD(有横线); 两者由 newsProcess.normalize_date 与 newsRedo._parse_date 转换(数据库连接字面量两种格式均被接受)。 临时文件策略(改造后):分片 chunk_*.wav 与 input_YYYYMMDD.wav 在 finally 中删除; 当天全程任务成功结束后由 cleanup.maybe_cleanup_after_run() 清空 xwlb_video/*.mp3|*.mp4 与 audio_processing/*.wav (失败时默认保留以便重跑,见 config.yml 的 cleanup.*;手动清理:python cleanup.py [--dry-run])。


7. 入口脚本对照

场景 命令 行为
日常(当天) python main.py 抓当天 → 下载 → 转写 → 切分(已处理则切分阶段自动跳过)
指定区间 python getVideo5.py 20260901 20260907 逐日全流程(参数校验:8 位、start ≤ end);已存在 mp3 的日期跳过下载
补缺失日 python main_videos.py 先查 xwlb_daily 缺失日期再逐日全流程(区间写在文件内,按需修改)
重跑某天 python newsRedo.py 20260923 已有精编记录则跳过;有原文只重做切分;否则全流程
强制重切 python newsRedo.py 20260923 --force 忽略"已处理"判据,重新切分并覆盖该日 xwlb_daily_ext
只重做切分 python newsProcess.py 2026-09-23 [--force] 直接对指定日期执行切分入库
只重做转写 python audioRead.py 20260923 用已有 xwlb_video/20260923.mp3 重跑分割+识别+落库(按分片覆盖,可安全重跑)
手动清理 python cleanup.py [--dry-run] 清空 `xwlb_video/*.mp3
定时任务(生产入口) bash scripts/run_daily.sh [YYYYMMDD] 全链路 + 失败重试(21:30 / 22:00);由 systemd/xwlb-daily.timer 每天 21:00 触发
这天跑到哪一步 python scripts/day_status.py <日期> 退出码 0=已有精编 / 1=识别完成只缺切分 / 2=需重跑全链路
安装定时任务 bash scripts/install_systemd.sh 安装单元、启用定时器与隧道服务
安装日志轮转 bash scripts/install_logrotate.sh 装 /etc/logrotate.d/xwlb(每天/保留14天/压缩)与 journald 上限(14 天)
校验轮转配置 sudo logrotate -d /etc/logrotate.d/xwlb 干跑,不实际轮转
校对效果评估 python tools/compare_raw_improve.py --md <文件> 统计 news_raw vs news_improve 差异分类与 token 开销
合并方案评估 python tools/experiment_merge_correct_split.py <日期> 实测"校对+切分合并成一次调用"的覆盖率与事实漂移
配置自检 python tests/test_config.py 校验 config.yml 加载、优先级与敏感项隔离
清理自检 python tests/test_cleanup.py 校验清理开关组合(临时目录,不动真实文件)
解析自检 python tests/test_news_parse.py 校验 LLM 返回解析逻辑

tunnel.py —— 隧道自愈(所有入口共用)

函数 作用
port_open(host, port, timeout) TCP 层探测端口,不涉及 MySQL 握手
is_local(host) 是否本机地址(localhost/127.0.0.1/::1)→ 决定"隧道"这个概念是否适用
systemd_unit_active(unit) systemd 单元是否 active(systemctl 不可用时安全返回 False)
ensure_tunnel(...) 探测 → 不通则按分支等待 systemd 或执行 autossh.sh → 如实返回 ST_* 状态

挂在 MySQLDB.connect():只在连接失败时介入,所以正常路径没有任何额外开销; _ensure_connection() 的重连也会走同一条路径,长流程中途断隧道同样能恢复。


7.1 部署:systemd 定时任务与重试语义

生产入口不是 main.py 而是 scripts/run_daily.sh,由 systemd/xwlb-daily.timer 每天 21:00 触发。

21:00  xwlb-daily.timer → xwlb-daily.service → scripts/run_daily.sh
         ├─ flock 防重入(已有实例则退出码 2)
         ├─ 检查 127.0.0.1:13306(ss 快速判断);不通则调用 tunnel.py 自愈
         │     (唯一实现:systemd 单元 active 就等它重连,否则执行 autossh.sh)
         ├─ 第 1 次尝试:getVideo5.py <今天> <今天>,上限 40 分钟
         │     成功 → 退出 0
         │     失败 ↓
         ├─ 等到 21:30 → scripts/day_status.py 判定阶段
         │     退出码 0(已有精编)→ 什么都不做
         │     退出码 1(有分片 **且有 .asr_complete_<日期> 标记**)→ 只跑 newsProcess.py(省掉全部 ASR)
         │     退出码 2(其余)→ 重跑全链路
         └─ 22:00 再同样重试一次;仍失败 → 退出 1(systemd 记录为 failed)
单元 关键设置 原因
xwlb-daily.timer OnCalendar=*-*-* 21:00:00、Persistent=true 关机错过时刻则开机补跑
xwlb-daily.service Type=oneshot、TimeoutStartSec=10800 默认 90s 会把"等待 + 重试"的服务砍掉
xwlb-tunnel.service autossh + Restart=always + ExitOnForwardFailure=yes 隧道是数据库前置条件;xwlb-daily 通过 Wants=/After= 依赖它

退出码约定:0 成功 / 1 重试用尽仍失败 / 2 已有实例在运行。

日志(保留 14 天):任务日志 main.log 由 logrotate 每天轮转为 main.log-YYYYMMDD.gz 保留 14 份(logrotate/xwlb 模板 → scripts/install_logrotate.sh 安装);systemd journal 通过 /etc/systemd/journald.conf.d/xwlb.conf 设 MaxRetentionSec=14day + SystemMaxUse=500M。 轮转必须用 copytruncate——脚本以 >>main.log 长开句柄, 改名式轮转会让当晚日志写进归档、新文件空着。

为什么必须有 state/.asr_complete_<日期> 标记:识别中途卡死(B19)会留下部分分片, 若只按"库里有没有分片"判断,重试会走"只重跑切分",把半天内容当成完整一天入库且不报错。 标记由 audioRead.process_long_audio 在全部识别成功时写入,失败时主动删除。

标记刻意放在 state/ 而不是 audio_processing/,也不随 cleanup.py 删除: 它是"该日识别已完成"的长期凭证。若随中间产物一起清掉, 就无法区分"已完成并被清理"与"识别只跑了一半、切分却照样写出了 ext"这两种状态。 配合 process_videos 的改动——识别不完整时不执行切分——四种状态才互不混淆:

有 state/ 标记 有 xwlb_daily_ext 含义 重试动作
✅ ❌ 识别完成、切分未完成 只重跑切分
✅ ✅ 已完成 无需动作
❌ ❌ 识别未完成或未开始 重跑全链路
❌ ✅ 只可能来自人工干预 视为已完成

8. 当前环境状态(改造后,本机实测)

项 状态 说明
Python / venv ✅ .venv(3.13.5) 依赖已装齐(含 audioop-lts 以支持 3.13 下的 pydub)
数据库 ✅ 已连通 SSH 隧道 localhost:13306 → 远端 MariaDB 10.11;xwlb_daily 7973 行、xwlb_daily_ext 11460 行
MySQLDB ✅ 已内置 mysql_handler.py,接口与原父项目实现一致
.env ✅ 已精简 只留 3 个敏感项(口令/Key);地址、模型等已迁至 config.yml
config.yml ✅ 已就位 含 MySQL 地址、目录、三个模型、切分参数、清理开关
ffmpeg ✅ /usr/bin/ffmpeg 可用
下载/输出目录 ✅ 基于项目目录 默认 ./xwlb_video、./audio_processing(config.yml: paths.*)
中间产物清理 ✅ 已启用 当天成功结束后自动清空 mp3/mp4/wav;实测清掉 15 个文件 / 474.3 MB
版本管理 ❌ 仍非 git 仓库 已提供 .gitignore,建议后续 git init

端到端验证:在测试日期 1900-01-01 上用裁剪后的真实音频完整跑过「分割 → ASR → 校对 → xwlb_daily → DeepSeek 切分 → xwlb_daily_ext」,全部成功,测试数据已清理。详见 BUGS.md 的验证记录。


9. 时序图(单日,改造后)

20:34  抓页面 → 找到 VID 页(失败日期明确跳过)
20:35  yt-dlp 下载 ~100MB mp4 → ffmpeg 抽 mp3(已存在则跳过下载)
20:35  mp3 → input_YYYYMMDD.wav(16k) → 静音切分为 ~11 片
20:36  chunk_0  ASR(30~45s) → 校对(20~32s;失败或改动数值事实则用原文) → 覆盖写 xwlb_daily ┐
  ...                                                                          │ 约 10~14 min
21:0x  chunk_10 同上 → 删除该分片                                              ┘
21:0x  拼接 11 行 news_improve(~6900 字)→ DeepSeek(27~35s)
21:0x  解析 JSON(失败则强化提示词重试一次)→ 删除当日旧记录 → 批量写入 xwlb_daily_ext ×N
21:0x  清理中间产物:mp3/mp4/wav 全部删除(仅当本日全部成功;失败则保留供重跑)

每个分片顺序执行,无并发;单个分片识别失败只影响该片(记 ERROR 且不写空行),其余分片照常入库。