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 行与轮转前完全一致
402 lines
30 KiB
Markdown
402 lines
30 KiB
Markdown
# xwlb 项目技术说明
|
||
|
||
> 一句话定位:把央视《新闻联播》完整版视频自动转成**结构化单条新闻**并入库的离线流水线。
|
||
> 本文基于仓库当前源码 + `main.log`(2026-06-01 ~ 2026-09-23)实测行为整理。缺陷清单见 [`BUGS.md`](./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 节](#4-配置项分层敏感在-env其余在-configyml))。
|
||
|
||
---
|
||
|
||
## 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` 接口约定(调用方依赖,重写时必须保持一致)
|
||
```python
|
||
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`](./REPORT_raw_vs_improve.md)):
|
||
校对是机械任务,关掉思维链省约 13 倍 token 且输出几乎不变;
|
||
切分关掉会丢 4.7% 正文、少切 7 条新闻,**必须保留思考**。
|
||
|
||
### 4.3 环境准备
|
||
|
||
```bash
|
||
# 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,约 20s~1min |
|
||
| 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 30~45s + 校对 20~32s)估算,11 片约 10~14 分钟,加上下载、切分与片段处理,单日约 **25~35 分钟**。
|
||
|
||
---
|
||
|
||
## 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|*.mp4` 与 `audio_processing/*.wav`(含识别完成标记) |
|
||
| **定时任务(生产入口)** | `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 的验证记录](./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 且不写空行),其余分片照常入库。 |