# 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()→ 正则/文本匹配「完整版《新闻联播》」 │ │ 的 ,得到 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.` → `endpoints.`,配置不全时抛可读错误 | | | `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` | 安装单元、启用定时器与隧道服务 | | 校对效果评估 | `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` 已有实例在运行。 **为什么必须有 `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 且不写空行),其余分片照常入库。