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 行与轮转前完全一致
xwlb —— 央视《新闻联播》自动转写与结构化入库流水线
把央视网《新闻联播》完整版视频自动抓取下来,转写成文字,再交给大模型切分成一条条独立新闻(含标题、正文)写进 MySQL/MariaDB。
全流程一条命令跑完,可幂等重跑、失败可定位、跑完自动清理中间产物。
内容版权归中央广播电视总台所有。本项目仅用于个人学习与研究用途,请勿用于商业分发。
目录
一、功能
1.1 主流程(python main.py 一条命令走完)
| 步骤 | 做什么 | 产出 |
|---|---|---|
| ① 链接发现 | 抓 tv.cctv.com/lm/xwlb/day/YYYYMMDD.shtml,匹配出当天「完整版《新闻联播》」的 VID 详情页 |
视频页 URL |
| ② 下载与抽音 | yt-dlp 下载 mp4,ffmpeg 抽出 mp3(已存在则跳过下载) | xwlb_video/YYYYMMDD.mp4 / .mp3 |
| ③ 转写落库 | mp3 → 16kHz 单声道 wav → 按静音切成 ~11 片 → 逐片语音识别 → LLM 校对 → 写入 xwlb_daily |
分片级原文 + 校对文本 |
| ④ 结构化精编 | 把当天全部分片文本拼起来交给 DeepSeek,切分成独立新闻并起标题 | xwlb_daily_ext 多行(标题 + 正文) |
| ⑤ 自动清理 | 当天全部成功后才清空 mp3/mp4/wav | 释放磁盘(实测单日约 121MB) |
1.2 工程能力
- 幂等重跑:
xwlb_daily按(日期, 分片号)先删后插;xwlb_daily_ext按日期整体替换。同一天跑多少遍结果都一致,不会产生重复行。 - 绝不丢已识别文本:校对环节任何失败(超时、限流、返回空)都自动回退 ASR 原文,不会因为"润色"把整片文字丢掉。
- 数值事实守卫:LLM 校对若改动了数字/年份/届次/规划期等事实(实测出现过
2027→2024、十五五→十四五、第十一届→第九届),该分片整片回退 ASR 原文。 - 不写脏数据:识别为空的分片不写库;失败当天不删除已有数据,留给下次重跑。
- 可重跑的分步入口:只重做转写、只重做切分、强制重切,都有独立命令。
- 配置分层:口令/Key 在
.env,其余(含三个模型名与各供应商 base url)在config.yml;换模型、换供应商都不用动代码。 - 失败说得清楚:缺 Key、连不上库(含隧道提示)、模型返回非 JSON 等都有明确日志与修复指引。
- 116 项离线自检:配置、清理、事实守卫、LLM 返回解析、隧道自愈、日志轮转六个测试套件,不依赖网络与真实 API。
1.3 当前数据规模(2026-09-25 实测)
| 表 | 行数 | 覆盖天数 | 日期范围 |
|---|---|---|---|
xwlb_daily(分片级原文) |
7,973 | 725 | 2024-09-26 ~ 2026-09-23 |
xwlb_daily_ext(单条新闻) |
11,494 | 523 | 2024-09-26 ~ 2026-09-23 |
单日耗时约 25~35 分钟(下载 + 抽音 + 11 片识别 + 校对 + 切分),全程顺序执行、无并发。
二、技术架构
2.1 数据流
main.py(当天)
│ start_date = end_date = %Y%m%d
▼
┌───────────────────────────────────────────────────────────────┐
│ ① 链接发现 getVideo5.get_all_video_links(start, end) │
│ xwlb_urls() → tv.cctv.com/lm/xwlb/day/YYYYMMDD.shtml
│ get_xwlb_video_link() → 匹配「完整版《新闻联播》」得到 VID 页 │
├───────────────────────────────────────────────────────────────┤
│ ② 下载与抽音 download_and_extract_audio(url, date, dir) │
│ yt-dlp best[ext=mp4]/best → xwlb_video/YYYYMMDD.mp4 │
│ ffmpeg -c:a libmp3lame -q:a 0 → xwlb_video/YYYYMMDD.mp3 │
├───────────────────────────────────────────────────────────────┤
│ ③ 转写落库 audioRead.process_long_audio(mp3, out_dir, date) │
│ convert_mp3_to_wav() 16kHz / 单声道 → input_YYYYMMDD.wav│
│ split_audio_by_smart_silence(700ms, -40dBFS, keep 400ms) │
│ → 贪心合并为 ≤3 分钟分片 chunk_i.wav(实测约 11 片/天) │
│ for each chunk: │
│ transcribe_audio() dashscope paraformer ASR │
│ analyze_and_correct_text() LLM 校对 + 数值事实守卫 │
│ _upsert_daily_chunk() → xwlb_daily(先删后插) │
├───────────────────────────────────────────────────────────────┤
│ ④ 切分入库 newsProcess.news_to_db(date) │
│ 读当天 news_improve 按分片号拼接 → DeepSeek(json_object) │
│ → 解析 [{news_id, news_title, news_content}] │
│ → 删除该日期旧记录 → 批量写入 xwlb_daily_ext │
├───────────────────────────────────────────────────────────────┤
│ ⑤ 清理 cleanup.maybe_cleanup_after_run(date, day_ok) │
│ 成功才清空 xwlb_video/*.mp3|*.mp4 与 audio_processing/*.wav │
└───────────────────────────────────────────────────────────────┘
2.2 技术栈
| 层 | 选型 | 用途 |
|---|---|---|
| 语言/运行 | Python 3.13 + .venv |
本机为 externally-managed 环境,必须用虚拟环境 |
| 网页抓取 | requests + beautifulsoup4 |
解析央视按天页面,定位完整版视频 |
| 视频下载 | yt-dlp |
下载 mp4 |
| 音频处理 | ffmpeg(系统依赖)+ pydub + audioop-lts |
抽音、16k 单声道转换、静音切分(3.13 已移除 audioop,需补丁包) |
| 语音识别 | 阿里云 DashScope paraformer-realtime-v2 |
中文长音频转写(接入点可配) |
| 文本校对 | 阿里云 DashScope qwen3.7-max |
修正 ASR 的错别字/断句(受事实守卫约束) |
| 新闻切分 | DeepSeek deepseek-chat(json_object) |
切分为独立新闻 + 起标题 |
| LLM 接入 | 可配置的 endpoints + routes | base url / 协议类型 / 密钥变量名都在 config.yml,可换供应商 |
| 数据库 | MySQL / MariaDB 10.11 + mysql-connector-python |
结果入库 |
| 配置 | PyYAML(config.yml)+ 自研 dotenv 解析(.env) |
敏感/非敏感分开 |
| 远程访问 | autossh |
本地 13306 → 远端 3306 隧道 |
2.3 目录结构
xwlb/
├── main.py # 每日入口(当天)
├── getVideo5.py # 链接发现 + 下载 + 主流程编排
├── audioRead.py # 转写 + 校对 + 分片写库
├── newsProcess.py # DeepSeek 切分 + 写 xwlb_daily_ext
├── deepseek.py # DeepSeek 客户端(重试 / 退避 / json_object)
├── cleanup.py # 中间产物清理(自动钩子 + 手动命令)
├── config.py / config.yml# 非敏感配置加载器 / 配置本体(含三个模型名)
├── env.py # 敏感配置(.env)加载与缺失检查
├── mysql_handler.py # 数据库层(项目内置,不再依赖父项目)
├── mysqlHandle.py # 转发 mysql_handler.MySQLDB
├── newsRedo.py # 手动重跑某天(跳过 / 只切分 / 全流程)
├── main_videos.py # 补缺失日期(区间写在文件内)
├── .env / .env.example # 敏感项 / 模板
├── requirements.txt
├── autossh.sh # SSH 隧道脚本
├── xwlb_video/ # 中间产物:YYYYMMDD.mp4 / .mp3(成功后清空)
├── audio_processing/ # 中间产物:input_*.wav / chunk_*.wav(成功后清空)
├── docs/ # ARCHITECTURE.md(技术说明)、BUGS.md(缺陷清单)
└── tests/ # 4 个离线测试套件
2.4 数据库结构
xwlb_daily —— 音频分片级文本(一天的完整版被切成约 11 片,每片一行)
| 字段 | 类型 | 说明 |
|---|---|---|
nid |
int, PK, auto_increment | 自增主键 |
news_days |
date | 日期(YYYY-MM-DD) |
daily_sub_id |
int | 分片序号,从 0 开始 |
news_raw |
text | ASR 原始识别文本 |
news_improve |
text | 校对后文本(关闭校对或守卫回退时等于 news_raw) |
news_title |
text | 保留字段(当前流程未使用) |
表中无
(news_days, daily_sub_id)唯一索引,幂等性由代码「先删后插」保证。
建表语句(新环境可直接执行;库名默认 myquant)
CREATE TABLE IF NOT EXISTS xwlb_daily (
nid int(11) NOT NULL AUTO_INCREMENT,
news_days date NOT NULL,
daily_sub_id int(11) NOT NULL,
news_raw text NOT NULL,
news_improve text NOT NULL,
news_title text NOT NULL,
PRIMARY KEY (nid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS xwlb_daily_ext (
extid int(11) NOT NULL AUTO_INCREMENT,
news_date date NOT NULL,
sub_id tinyint(4) NOT NULL,
news_title varchar(256) NOT NULL,
news_content text NOT NULL,
PRIMARY KEY (extid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
xwlb_daily_ext —— 最终产物:单条新闻
| 字段 | 类型 | 说明 |
|---|---|---|
extid |
int, PK, auto_increment | 自增主键 |
news_date |
date | 日期(YYYY-MM-DD) |
sub_id |
tinyint | 当天第几条新闻,从 1 开始 |
news_title |
varchar(256) | 新闻标题(模型生成) |
news_content |
text | 新闻正文 |
2.5 三个模型的职责
| 环节 | 模型配置项 | 走哪个接入点(routes.*) |
默认模型 | 输入 → 输出 |
|---|---|---|---|---|
| 语音识别 | models.asr_model |
asr → dashscope |
paraformer-realtime-v2 |
单片 wav → 该片文本 |
| 文本校对 | models.correct_model |
correct → dashscope |
qwen3.7-max |
单片 ASR 文本 → 校对后文本(同一分片粒度) |
| 新闻切分 | models.split_model |
split → deepseek |
deepseek-chat |
当天全文 → JSON 数组(标题 + 正文) |
模型名与接入点是分开配置的:换供应商时改 routes + endpoints,再同步改模型名,代码不动(见 3.4 更换 LLM 供应商)。
为什么要两道 LLM:ASR 只保证"字"对上,标点、断句、同音错字仍需修;而切分属于理解任务——需要判断新闻边界、给每条起标题,所以交给擅长长文本理解的 DeepSeek。两道 LLM 的分工边界清晰,任一道失败都不会污染另一道的结果。
2.6 关键设计取舍
| 取舍 | 做法 | 原因 |
|---|---|---|
| 校对会改错事实 | 数值事实守卫:数值签名不一致就整片回退原文 | 宁保留可信的 ASR 原文,也不接受"通顺但错误"的文本 |
| 重跑会重复写库 | xwlb_daily 先删后插;xwlb_daily_ext 按日期替换 |
无需唯一索引即可幂等,重跑不产生脏数据 |
| 分片识别失败 | 记 ERROR、不写空行、其余分片照常 | 空行曾导致下游拼出残缺文本(历史库中有 101 行空白) |
| 切分返回非 JSON | 剥代码围栏 + raw_decode 兜底 + 强化提示词重试一次 |
历史上有 22 天因此得到 0 条精编 |
| 中间产物占用磁盘 | 当天全部成功才清理,失败保留 | 成功即无价值,可释放磁盘;失败则保留供重跑 |
| 顺序执行、不并发 | 逐片串行 | 单机负载可控、日志与失败定位简单;单日 25~35 分钟已满足离线需求 |
三、使用说明
3.1 环境要求
- Linux(本机 Raspberry Pi / Debian)
- Python 3.11 ~ 3.13(本项目在 3.13.5 上验证)
- 系统依赖:
ffmpeg;autossh(远程数据库时) - 一个可用的 MySQL/MariaDB 库(两张表需已存在,见 2.4)
- 阿里云 DashScope API Key(语音识别 + 校对)、DeepSeek API Key(切分)
3.2 安装
cd /home/pi/project/xwlb
# 1) 端口隧道:本地 13306 → 远端 3306(数据库不在本机时需要,通了可跳过)
bash autossh.sh
# 2) 虚拟环境(本机 Python 为 externally-managed,必须用 venv)
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# 3) 配置
cp .env.example .env # 填 3 个敏感项
vim config.yml # 核对数据库地址、目录、模型
Python 3.13 已移除标准库
audioop,pydub需要requirements.txt中的audioop-lts,已自动按版本条件安装。
3.3 配置
(1).env —— 只放敏感项(已被 .gitignore 排除)
MYSQL_PASSWORD=...
DASHSCOPE_API_KEY=sk-...
DEEPSEEK_API_KEY=sk-...
(2)config.yml —— 其余全部配置
mysql: { host: localhost, port: 13306, user: myquant, database: myquant }
paths: { video_dir: xwlb_video, audio_dir: audio_processing }
models:
asr_model: paraformer-realtime-v2 # 语音识别
correct_model: qwen3.7-max # ASR 文本校对
split_model: deepseek-chat # 新闻切分 + 标题
# 接入点(base url):换供应商 / 换区域 / 走代理只改这一段
endpoints:
dashscope:
kind: dashscope # dashscope=用 SDK,openai=OpenAI 兼容
http_base_url: https://dashscope.aliyuncs.com/api/v1 # 文本生成走 HTTP
websocket_base_url: wss://dashscope.aliyuncs.com/api-ws/v1/inference # 实时 ASR 走 WS
api_key_env: DASHSCOPE_API_KEY # 密钥在 .env 里的变量名
deepseek:
kind: openai
base_url: https://api.deepseek.com/v1
chat_completions_path: /chat/completions
api_key_env: DEEPSEEK_API_KEY
# 每个环节走哪个接入点(值是 endpoints 下的名字)
routes: { asr: dashscope, correct: dashscope, split: deepseek }
llm_correct:
enabled: 1 # 1=开启校对(默认);0=直接用 ASR 原文
cleanup:
after_daily_run: 1 # 当天成功结束后自动清理中间产物
only_on_success: 1 # 仅成功才清理;0=无论成败都清理
remove_video: 1 # 删除 mp3 / mp4
remove_audio: 1 # 删除 wav
优先级:进程环境变量 > config.yml > 代码内置默认值。
所以临时试验可以不改文件:
DASHSCOPE_LLM_MODEL=qwen-max .venv/bin/python audioRead.py 20260904
LLM_CORRECT_ENABLED=0 .venv/bin/python getVideo5.py 20260904 20260904
XWLB_ROUTE_CORRECT=deepseek .venv/bin/python newsProcess.py 2026-09-04 # 临时换校对供应商
启动时会打印生效配置(不含敏感值),含每个环节实际用的接入点:
配置来源: /home/pi/project/xwlb/config.yml | MySQL myquant@localhost:13306/myquant |
模型 asr=paraformer-realtime-v2 correct=qwen3.7-max split=deepseek-chat | 校对=开启 | 完成后清理=开启
接入点 asr -> dashscope kind=dashscope wss://dashscope.aliyuncs.com/api-ws/v1/inference(密钥变量 DASHSCOPE_API_KEY)
接入点 correct -> dashscope kind=dashscope https://dashscope.aliyuncs.com/api/v1(密钥变量 DASHSCOPE_API_KEY)
接入点 split -> deepseek kind=openai https://api.deepseek.com/v1/chat/completions(密钥变量 DEEPSEEK_API_KEY)
3.4 更换 LLM 供应商
供应商不写死在代码里:地址在 endpoints,走哪条线在 routes,密钥变量名在 api_key_env。
| 想做什么 | 改哪里 |
|---|---|
| 换区域(如 DashScope 国际站) | endpoints.dashscope 的两个 url |
| 走公司网关 / 代理 / 自建推理 | 改对应接入点的 base_url(OpenAI 兼容) |
| 换校对供应商(如月之暗面、智谱、Kimi) | ① routes.correct 指向新接入点 ② 在 endpoints 加一段 ③ 改 models.correct_model ④ .env 加对应 Key |
| 换切分供应商 | 同上,改 routes.split + models.split_model |
| 换 ASR 供应商 | ⚠️ 实时语音识别只有 kind: dashscope 一种适配器;换厂商需要新增适配器(换区域/网关仍可,改 url 即可) |
示例:把校对从通义千问换成月之暗面
routes: { asr: dashscope, correct: moonshot, split: deepseek }
endpoints:
moonshot:
kind: openai
base_url: https://api.moonshot.cn/v1
chat_completions_path: /chat/completions
api_key_env: MOONSHOT_API_KEY
models:
correct_model: kimi-k2-0905-preview
# .env
MOONSHOT_API_KEY=sk-...
要点:
kind只有两种:dashscope(用 dashscope SDK)和openai(OpenAI 兼容的/chat/completions,覆盖 DeepSeek / Kimi / 智谱 / vLLM / 各类网关)。- 请求地址 =
base_url+chat_completions_path,所以不带版本号的网关地址(如https://my-gateway/llm)也能直接填。 - 缺
base_url、routes指向不存在的接入点、kind拼错,都会在启动日志与调用时报出可读错误,不会静默走错地址。 - 换供应商不影响数值事实守卫:无论哪家的校对结果,改动数字/年份/届次一样会被拦下并回退 ASR 原文(已用 DeepSeek 作为校对供应商实测)。
- 运行时必需哪些 Key 是按路由推导的:只有被
routes用到的接入点才要求配 Key,未被使用的不会报缺失。
3.4.1 推理模型(thinking)注意
现在的模型多是"推理模型":回答前先生成一大段思维链,推理 token 按输出计费。
实测(详见 docs/REPORT_raw_vs_improve.md):
| 环节 | 关掉思考 | 开着思考 | 结论 |
|---|---|---|---|
| 校对 | 312 token / 4~6 秒,输出几乎一致 | 5,932 token | 关(省约 13 倍,机械任务不需要推理) |
| 切分 | 4,656 token,正文覆盖率 95.34%、20 条 | 19,077 token,覆盖率 99.67%、27 条 | 开(关掉会丢 4.7% 正文、少切 7 条) |
关思考的参数名因供应商而异,写错会被静默忽略(不报错但也不生效),所以配在接入点上:
endpoints:
qwen: { extra_body: {enable_thinking: false} } # 通义/百炼兼容模式
deepseek: { extra_body: {thinking: {type: disabled}} } # DeepSeek 推理模型
也可在 llm_correct / llm_split 段写 extra_body 覆盖(优先级更高)。
使用推理模型时 llm_split.max_tokens 要给足(现为 60000):实测推理 token 在
14k~24k 之间波动,上限 20000 会截断,甚至整个回复为空。
3.5 运行
| 场景 | 命令 | 说明 |
|---|---|---|
| 日常(当天) | .venv/bin/python main.py |
抓当天 → 下载 → 转写 → 切分 → 清理 |
| 指定某天 | .venv/bin/python getVideo5.py 20260904 20260904 |
日期格式 YYYYMMDD |
| 指定区间 | .venv/bin/python getVideo5.py 20260901 20260907 |
逐日全流程 |
| 补缺失日期 | .venv/bin/python main_videos.py |
反查库中缺失日期再补跑(区间写在文件内,需按需修改) |
| 重跑某天 | .venv/bin/python newsRedo.py 2026-09-04 |
已有精编 → 跳过;有原文 → 只重做切分;都没有 → 全流程 |
| 强制重切 | .venv/bin/python newsRedo.py 2026-09-04 --force |
忽略已有记录,重新切分并替换该日 xwlb_daily_ext |
| 只重做切分 | .venv/bin/python newsProcess.py 2026-09-04 [--force] |
不碰音频,直接用库中文本重切 |
| 只重做转写 | .venv/bin/python audioRead.py 20260904 |
用已有 xwlb_video/20260904.mp3 重跑分割+识别+落库 |
| 手动清理 | .venv/bin/python cleanup.py [--dry-run] |
清空两个中间产物目录,--dry-run 只列不删 |
注意:默认
cleanup.remove_video: 1,当天成功后 mp3 会被删掉。因此要重做某天的转写(audioRead.py)时,需要先重新下载该天的 mp3——最简单是把cleanup.remove_video临时设为0,或直接跑getVideo5.py重下再重跑。
3.6 自检与测试
六个测试套件共 116 项断言,全部离线运行(不消耗 API 额度、不触碰真实文件):
.venv/bin/python tests/test_config.py # 37 项:配置加载、优先级、接入点/路由、敏感项隔离
.venv/bin/python tests/test_cleanup.py # 10 项:清理开关五种组合(临时目录)
.venv/bin/python tests/test_fidelity_guard.py # 14 项:数值事实守卫(含真实篡改样例)
.venv/bin/python tests/test_news_parse.py # 16 项:LLM 返回解析(含真实坏返回)
.venv/bin/python tests/test_tunnel.py # 20 项:隧道自愈各分支(临时端口,不碰真实隧道)
.venv/bin/python tests/test_logrotate.py # 19 项:轮转指令、保留天数、行尾注释、占位符替换
其中隧道测试不会碰真实的 13306 与 autossh.sh:它用临时 config.yml + 临时端口的假脚本, 验证"端口已通时绝不执行脚本""systemd 托管时只等待不抢端口"等关键约定。
连通性自检:
# 打印生效配置并校验 Key 是否齐全
.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()"
3.7 定时任务(systemd)
已配置为 systemd 定时器:每天 21:00 触发全链路,失败后 21:30、22:00 各自动重试一次。
bash scripts/install_systemd.sh # 安装/更新单元(幂等,可反复执行)
systemctl list-timers xwlb-daily.timer # 看下次触发时间
journalctl -u xwlb-daily -f # 实时看日志(也在 main.log)
sudo systemctl start xwlb-daily.service # 立即试跑一次
sudo systemctl disable --now xwlb-daily.timer # 临时停用
涉及三个单元(源码在 systemd/):
| 单元 | 作用 |
|---|---|
xwlb-daily.timer |
每天 21:00 触发;Persistent=true,树莓派关机错过时刻会在开机后补跑 |
xwlb-daily.service |
oneshot,调用 scripts/run_daily.sh;TimeoutStartSec=10800 以容忍内部等待重试 |
xwlb-tunnel.service |
autossh 隧道托管(开机自启、断线自动重连),xwlb-daily 依赖它 |
重试不是简单地"再跑一遍":
- 只缺切分(识别已全部完成)→ 只重跑切分环节,省掉全部 ASR 调用(约十几分钟与相应费用)
- 识别没跑完 → 重跑全链路
- 判断依据是识别全部完成后写的标记
state/.asr_complete_<日期>; 只看"库里有没有分片"是不够的——识别中途卡死也会留下部分分片, 那样会被误判成"只缺切分",把半天内容当成完整一天入库(scripts/day_status.py专门防这个) - 识别不完整的那一天不会执行切分(不发布半天的 ext),宁可让重试补全,
也不产生"看起来完整"的一天;分片原文仍在
xwlb_daily里,不会丢 - 标记放在
state/且不随清理删除:它是长期凭证,删了就分不清 "已完成"与"识别只跑了一半但切分照样写了 ext" (cleanup只清xwlb_video/*.mp3|*.mp4与audio_processing/*.wav) - 单次尝试有 40 分钟上限(
XWLB_ATTEMPT_TIMEOUT),卡死的 ASR 不会拖垮整晚 - 有文件锁防重入,手动执行与定时触发叠在一起也不会重复烧 ASR
退出码:0 成功;1 重试用尽仍失败(会出现在 systemctl --failed);2 已有实例在运行。
用 cron 也行,但那样拿不到"超时上限、重试、防重入、开机补跑"这些保障:
40 20 * * * cd /home/pi/project/xwlb && .venv/bin/python main.py >> main.log 2>&1
3.8 数据库隧道自愈
数据库在内网,靠 autossh.sh 把本地 13306 转发到远端 3306。隧道断掉时,
任何入口都会自己把它拉起来——不只是定时任务,手动跑 main.py / getVideo5.py /
newsProcess.py / day_status.py 都一样(实现挂在数据库连接层 MySQLDB.connect())。
行为(tunnel.py,所有入口共用一套逻辑):
| 情况 | 动作 |
|---|---|
| 端口可连 | 什么都不做(正常路径零开销,不会多起进程) |
| 端口不通,且 systemd 隧道单元 active | 只等待它自动重连——不另起 autossh 抢同一个端口(那会让 systemd 单元因端口被占反复重启失败) |
| 端口不通,systemd 单元未在运行 | 执行 config.yml 里 mysql.tunnel.script(默认 autossh.sh),最多等 30 秒 |
| 仍不通 | 如实报错,不再假装成功 |
python tunnel.py # 手动探测 + 按需修复,退出码 0=通 / 1=仍不通
python tunnel.py --dry-run # 只看会做什么,不执行
配置:
mysql:
tunnel:
enabled: 1
script: autossh.sh # 相对项目根
systemd_unit: xwlb-tunnel.service # 该单元 active 时只等待,不抢端口
wait_seconds: 30
connect_timeout: 2
(直连远端数据库时,host 不是本机地址 → 判定为"不适用隧道",不会去跑 autossh.sh。)
3.9 日志轮转
bash scripts/install_logrotate.sh # 安装/更新(幂等,可反复执行)
两处一起收口,都是保留 14 天:
| 日志 | 位置 | 策略 |
|---|---|---|
任务日志 main.log |
项目根(>>main.log 追加) |
logrotate:每天轮转、保留 14 份、立即压缩 → main.log-20260925.gz |
| systemd journal | /var/log/journal |
journald:MaxRetentionSec=14day + SystemMaxUse=500M |
实测效果:34MB 的 main.log 轮转后成 2.9MB 压缩归档,归档行数与轮转前完全一致(32453 行),磁盘立即回收。
配置里有两个容易踩的点:
- 必须用
copytruncate:脚本以>>main.log方式持续写入,一次任务跑几十分钟、句柄一直开着。 默认的"改名+新建"会让运行中的进程继续写旧文件,当晚日志被切进归档、新文件却空着。 - 指令后面不能写注释:logrotate 会把注释当成参数值(实测报
bad rotation count '14 # 保留 14 份'),注释只能单独成行。tests/test_logrotate.py专门锁住了这条。
轮转由系统自带的 logrotate.timer(每天 00:46)驱动,无需额外定时任务。
sudo logrotate -d /etc/logrotate.d/xwlb # 干跑校验配置
sudo logrotate -f /etc/logrotate.d/xwlb # 立即轮转一次
zcat main.log-20260925.gz | less # 看归档
ls -lh main.log* # 看当前归档与占用
3.10 故障排查
| 现象 | 原因 / 处理 |
|---|---|
MySQL 连接失败 ... |
隧道不通。现在会自动恢复:连接失败时探测端口,不通则执行 autossh.sh 并等待(见 3.9 隧道自愈)。仍失败再手动 bash autossh.sh,并确认 config.yml 的 mysql.port 是隧道端口 13306 |
缺少敏感配置 ... |
.env 未填 MYSQL_PASSWORD / DASHSCOPE_API_KEY / DEEPSEEK_API_KEY |
externally-managed-environment |
不要用系统 pip,用 .venv/bin/pip |
No module named 'audioop' |
Python 3.13 需装 audioop-lts(已在 requirements 中) |
| 某天识别成功但精编为 0 条 | 切分返回非 JSON;现会自动用强化提示词重试一次,仍失败则保留原文不写库,可 newsProcess.py <日期> --force 重试 |
| 校对结果与原文数字不一致 | 事实守卫已拦截并回退原文,日志会打印 ⚠️ 校对改动了数值事实,已回退 ASR 原文 |
| 想完全关掉 LLM 校对 | config.yml 设 llm_correct.enabled: 0 |
接入点配置错误: ... |
routes.* 指向了不存在的接入点、kind 拼错、或缺 base_url;按日志提示补 endpoints 字段 |
ASR 接入点 ... kind=openai:实时语音识别仅支持 kind=dashscope |
ASR 换厂商需新增适配器;换区域 / 网关请保留 kind: dashscope 只改 url |
| 换供应商后报 401 / 403 | .env 里没有该接入点 api_key_env 指定的那个变量(不是把新 Key 塞进旧变量名) |
| 换供应商后报模型不存在 | models.correct_model / models.split_model 还是旧供应商的模型名,需同步修改 |
getVideo5 报某天 404 |
该天页面尚未上线(当天节目过期或未发布),日志会明确跳过 |
| 磁盘被中间产物占满 | python cleanup.py --dry-run 查看,再 python cleanup.py |
main.log 越来越大 |
已配 logrotate 保留 14 天(见 3.9);未装则 bash scripts/install_logrotate.sh |
日志轮转报 bad rotation count |
指令行写了行尾注释;logrotate 不支持,注释要单独成行 |
| 某天识别到一半卡死(无新日志、无 CPU) | ASR 的 Recognition.call 没有超时参数,长连接可能悬挂;定时任务有 40 分钟上限并在 21:30/22:00 重试。手动跑请自行 timeout |
| 定时任务没跑 | systemctl list-timers xwlb-daily.timer;systemctl status xwlb-daily.service;日志 journalctl -u xwlb-daily -n 100 |
| 定时任务报数据库连接失败 | 隧道服务:systemctl status xwlb-tunnel;手动兜底 bash autossh.sh |
四、相关文档
| 文档 | 内容 |
|---|---|
docs/ARCHITECTURE.md |
完整技术说明:数据流、模块与函数索引、数据库结构、配置项全表、外部调用实测参数、时序图 |
docs/REPORT_raw_vs_improve.md |
校对效果评估:7973 条真实数据统计、实测 token 开销、合并/关闭校对的对比与结论 |
docs/BUGS.md |
15 项缺陷清单(含严重度、位置、影响、证据、修复),以及本次修复的实测验证记录与后续待办 |
已知待办(详见 docs/BUGS.md):
main_videos.py的日期区间仍写死在文件内,尚未改为命令行参数;- 历史数据治理(截至 2026-09-25):
xwlb_daily有 100 行空白分片、202 天缺xwlb_daily_ext,可用上述分步入口补齐; - 项目尚未
git init(.gitignore已就绪);main.log无轮转(历史 34MB 作为证据保留)。