《新闻联播》每日抓取入库:全链路 + 可移植化 + 定时任务
从 CCTV 主页抓取《新闻联播》,下载 → 转 MP3/WAV → 静音切分 → ASR 识别 → LLM 校对 → 切分为单条新闻 → 入库 MySQL。 主要内容: - 全链路:getVideo5 抓取下载、audioRead 转写、deepseek 校对与切分、newsProcess 入库 - 可移植化:配置分层,.env 只放密钥、config.yml 放模型/接入点/路由/参数 - 可换供应商:endpoints(kind/base_url/api_key_env/extra_body)+ routes 按环节选路 - 数据保真:数值事实守卫,校对改动数字/年份/届次则整片回退 ASR 原文; 识别不完整不发布该日精编,避免半天内容被当成完整一天 - 定时任务:systemd 每天 21:00,失败 21:30 / 22:00 重试; 只缺切分时只重跑切分(省掉全部 ASR),用 state/.asr_complete_* 标记判定阶段 - 隧道自愈:13306 不通时自动执行 autossh.sh(所有入口共用,systemd 托管时只等待) - 中间产物每日清理;97 项离线自检(配置/清理/事实守卫/解析/隧道)
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
# 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 等都有明确日志与修复指引。
|
||||
- **97 项离线自检**:配置、清理、事实守卫、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`)
|
||||
|
||||
```sql
|
||||
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 供应商](#34-更换-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](#24-数据库结构))
|
||||
- 阿里云 DashScope API Key(语音识别 + 校对)、DeepSeek API Key(切分)
|
||||
|
||||
### 3.2 安装
|
||||
|
||||
```bash
|
||||
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` 排除)
|
||||
|
||||
```ini
|
||||
MYSQL_PASSWORD=...
|
||||
DASHSCOPE_API_KEY=sk-...
|
||||
DEEPSEEK_API_KEY=sk-...
|
||||
```
|
||||
|
||||
**(2)`config.yml` —— 其余全部配置**
|
||||
|
||||
```yaml
|
||||
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 > 代码内置默认值`。
|
||||
所以临时试验可以不改文件:
|
||||
|
||||
```bash
|
||||
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 即可) |
|
||||
|
||||
**示例:把校对从通义千问换成月之暗面**
|
||||
|
||||
```yaml
|
||||
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
|
||||
```
|
||||
|
||||
```ini
|
||||
# .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`](./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 条) |
|
||||
|
||||
关思考的参数名**因供应商而异**,写错会被**静默忽略**(不报错但也不生效),所以配在接入点上:
|
||||
|
||||
```yaml
|
||||
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 自检与测试
|
||||
|
||||
五个测试套件共 **97 项断言**,全部离线运行(不消耗 API 额度、不触碰真实文件):
|
||||
|
||||
```bash
|
||||
.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 项:隧道自愈各分支(临时端口,不碰真实隧道)
|
||||
```
|
||||
|
||||
其中隧道测试**不会碰真实的 13306 与 autossh.sh**:它用临时 config.yml + 临时端口的假脚本,
|
||||
验证"端口已通时绝不执行脚本""systemd 托管时只等待不抢端口"等关键约定。
|
||||
|
||||
连通性自检:
|
||||
|
||||
```bash
|
||||
# 打印生效配置并校验 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
|
||||
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 秒 |
|
||||
| 仍不通 | 如实报错,不再假装成功 |
|
||||
|
||||
```bash
|
||||
python tunnel.py # 手动探测 + 按需修复,退出码 0=通 / 1=仍不通
|
||||
python tunnel.py --dry-run # 只看会做什么,不执行
|
||||
```
|
||||
|
||||
配置:
|
||||
|
||||
```yaml
|
||||
mysql:
|
||||
tunnel:
|
||||
enabled: 1
|
||||
script: autossh.sh # 相对项目根
|
||||
systemd_unit: xwlb-tunnel.service # 该单元 active 时只等待,不抢端口
|
||||
wait_seconds: 30
|
||||
connect_timeout: 2
|
||||
```
|
||||
|
||||
(直连远端数据库时,`host` 不是本机地址 → 判定为"不适用隧道",不会去跑 autossh.sh。)
|
||||
|
||||
### 3.9 故障排查
|
||||
|
||||
| 现象 | 原因 / 处理 |
|
||||
|---|---|
|
||||
| `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` |
|
||||
| 某天识别到一半卡死(无新日志、无 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/ARCHITECTURE.md) | 完整技术说明:数据流、模块与函数索引、数据库结构、配置项全表、外部调用实测参数、时序图 |
|
||||
| [`docs/REPORT_raw_vs_improve.md`](./docs/REPORT_raw_vs_improve.md) | 校对效果评估:7973 条真实数据统计、实测 token 开销、合并/关闭校对的对比与结论 |
|
||||
| [`docs/BUGS.md`](./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 作为证据保留)。
|
||||
Reference in New Issue
Block a user