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 行与轮转前完全一致
30 KiB
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 |
|
| 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 且不写空行),其余分片照常入库。