直播录播情报(模板版 · 功能包)
> 定位:同类做法多是「等切片、看二手总结」;这套是「自己把整场音轨转成逐字稿」——
> 一句口播能被定位到秒、能点开原视频当面对质,而不是「我记得他好像说过」。
> 一份引擎 + 一份目标配置,换人只换配置、不动代码。
本技能是脱敏模板:不含任何具体主播、团队或对手的名称。目标用 config.json 描述,
开跑前由 scripts/preflight.py 检查并填充。配置不齐时拦住不开跑,而不是空跑一圈给你 0 条结果。
代码在 scripts/,配置模板在 config.example.json,方法论在 references/。
第 0 步:听懂一句话,把配置补齐(别跳)
用户不会说「请先跑 preflight」。他只会说:
> 「看看 <某某> 昨晚直播了什么内容」
> 「<某某> 在 2026 年 9 月 27 日发生了啥事儿」
他这么说,你就这么做
| 用户这句话 | 你直接提取 | 落到哪 |
|---|---|---|
| 「看看某某昨晚直播什么内容」 | 主体=某某,日期=昨天 |
--who 某某 + MMDD=昨天 |
| 「某某在 2026 年 9 月 27 日发生了啥事儿」 | 主体=某某,日期=2026-09-27 |
--who 某某 + 0927 --year 2026 |
| 「某某和某某那场 PK 谁赢了」 | 主体 + 对手,日期没给 | 先跑最近 3 天;仍无则问「哪天」 |
| 「某某最近怎么样」 | 主体,日期=最近一场 | 先 find_live.py 试昨天,空则往前提一天 |
能推的一律不问。 只有两种情况值得开口,一次问一句:
- 名字对不上(同名主播、昵称不确定是谁)→ 问「你说的是哪一个」;
- 真没日期、也没有「昨晚 / 最近 / 那场」这类能推的线索 → 问「哪一天」。
问完就开跑,不要在这之后又冒出一堆确认项。
agent 内部执行(这三条不用念给用户)
cd "<技能目录>" # 本机为 ~/.workbuddy/skills/live-replay-intel
PY="<装了 numpy/av/faster_whisper 的 python>"
S="scripts"
# ① 查缺项(不改任何文件)
"$PY" "$S/preflight.py"
# ② 把上表提取到的信息一次性补进去并落盘
"$PY" "$S/preflight.py" \
--set name=<主播正名> \
--set aliases=<别名1,别名2> \
--set seed_bvids=<BV号1,BV号2> --emit
# ③ 复检 0 退出后开跑
"$PY" "$S/find_live.py" <MMDD>
跑完直接给结论(看见了什么、比分多少、几点几分说的),不要把「我执行了 preflight、退出码 0」这类过程念给用户。
退出码(按码决定下一步,不要凭感觉跳过)
| 退出码 | 含义 | 该做什么 |
|---|---|---|
0 |
配置通过 | 直接开跑,进第 1 步 |
2 |
仍有缺项 | 看提示里每条的「怎么改」;或用 --set 补(见下) |
3 |
参数 / 文件本身坏了 | --set 必须是 k=v;或 config.json 的 JSON 语法错了 |
> 只想验一把、不真跑:find_live.py <MMDD> --check。
> 但补齐配置只有 preflight.py --set ... --emit 这一条路,--check 不会改任何文件。
缺信息时怎么补
-
名字已经给了、只是配置里没存 → 别去翻配置文件,直接带上跑:
bash "$PY" "$S/find_live.py" <MMDD> --who "<主播名>" --alias "<别名1,别名2>" -
有名字、没有 BV 号 → 先跑,不用等填满。纯搜索也能出源(实测某日只靠搜索找到 1 条),
够先看一眼;在结果里看到反复出现的转录号 UP,再回来补seed_bvids把覆盖面拉满。 -
确实什么都缺(连名字都没给)→ 不猜、不编,把缺项原样回给用户,一次说完:
还差 1 项就能开跑: 主播名 —— 告诉我是谁就行,剩下的我自己配。
为什么这道门禁必须有
「找到 0 条源」有三种完全不同的成因,输出却长得一模一样:
| 真实原因 | 怎么区分 | 谁来兜 |
|---|---|---|
| 主体名没填 / 填错 | 搜索有结果但一条都不匹配 | 门禁拦住 |
seed_bvids 是模板占位文字 |
合集扫描一条都进不去 | 门禁拦住 |
| 接口风控(连续检索后间歇性全空) | 所有查询一起返回 0 条 | 脚本自动识别并点名 |
前两种配好就不会发生,所以提前拦住;第三种不会被当成「他那天没播」交给你 ——
find_live.py 会直接说「判定为接口风控」,并给出两条出路(等 10–20 分钟 / 走 --mid 合集通道)。
配置字段(只有 name 是必填)
| 字段 | 必填 | 作用 | --set 写法 |
|---|---|---|---|
subject.name |
✅ | 正名,用于拼搜索词 | --set name=某某 |
subject.aliases |
建议 | 别名/昵称。切片标题常只写昵称,没有别名会漏 | --set aliases=甲,乙 |
subject.up_whitelist |
强烈建议 | 转录号 UP 名,用来接住标题不含主体名的切片 | --set up_whitelist=UP1,UP2 |
subject.seed_bvids |
关键 | 每个转录号填一条 BV,脚本据此解析 mid 并扫其全部合集 | --set seed_bvids=BV1xx,BV1yy |
extra_keywords |
可选 | 场次专有玩法名/常出现的对手名,转写完自动统计频次。只填核实过她会念出来的词,标题词/榜单名不算(会把同音口播词带偏,见 pitfalls 第 25 条) | --set extra_keywords=词1,词2 |
--set 是覆盖语义:同一个字段传多次,以最后一次为准。所以要一次给全,别分批。
另有一组 asr 参数(model / word_timestamps / prompt)只能用配置文件或环境变量改,
不支持 --set —— 它们不决定「能不能跑」,只决定「转得多准、多快」,不该挡住开跑。
为什么 seed_bvids 决定成败(同一日期实测)
| 种子数 | 候选总量 | 该日有效源 |
|---|---|---|
| 0 个(纯搜索) | 40 | 1 条 |
| 1 个 UP | 458 | 1 条 |
| 3 个不同 UP | 898 | 14 条(7 个全场 + 7 条切片) |
原因:搜索对历史日期几乎无效(0927 这种词返回的全是近期热门切片),
真正的源都躺在各转录号的按月合集里,而一个种子只能进它自己那个 UP 的合集。
想覆盖几个转录号,就得给几个种子。
依赖
转写要 numpy / av / faster_whisper / opencc。没装就一定会失败,
所以 preflight.py --stage pull 会先把这件事查出来(退出码 2),而不是跑到一半报 ImportError。
模型默认 faster-whisper small:CPU 上约 1 分钟转 5 分钟音频,中文直播够用。
换 medium 精度略升但慢 3 倍,CPU 机器上多数场景不值得。
适用与不适用
适用
- 主播在 B 站有转录号上传全场录播(中腰部以上主播绝大多数有)
- 要「某句话的准确时间点」「某场连麦的完整过程」「某天有没有说过某件事」
- 要量化:某场比分、某晚连了几个人、某个词出现多少次
不适用
- 完全没有录播留存的直播(无源可查,只能靠切片)
- 要「实时」的场景(本流程是事后回溯,单场转写 30–120 分钟)
主流程(五步,按顺序做)
第 1 步 · 找源 → scripts/find_live.py
python find_live.py 0927 # 默认当年
python find_live.py 0927 --year 2025
python find_live.py 0927 --pages # 展开每个 P 的 cid/时长/part
它做四件事:多组日期变体搜索(0927 / 9月27日 / 260927 / 2026年9月27日)→ 去重 → 逐个取详情 → 抽录制起点。
输出里最值钱的是「起录时间」:转录号的标题/part/desc 里常带录像软件的时间戳
(2026.09.27 20.30.49、SVID_20260927_203016、Record_2026-09-27-20-31-30),
这是最硬的绝对时间锚点,比用「官方预热说 21:20 开打」反推准得多。
一个源读到,整场挂钟就都定了。
同时它会标注每个候选的可用性:
- ★起录时间可读 —— 优先选它
- N 段需偏移合并 —— 多 P 的上下集,合并要加偏移
- ⚠疑似同场多版本 —— 同一场的宽频版/清屏版/弹幕版,别合并,选一个
- 预计转写 ~N 分钟 —— 按 5.5 倍实时保守估,用来判断这场值不值得跑
开头会先给一段「一句话结论」,把候选分成三档(整场 / 分段·单局 / 切片):
| 档 | 判据 | 能拿它干什么 |
|---|---|---|
| 整场 | ≥60 分钟 且 ≥本场最长那条的 80% | 可以跑全场转写,结论能代表整场 |
| 分段 / 单局 | 20 分钟 ~ 整场线 | 只覆盖其中一段,别当整场用 |
| 切片 | <20 分钟 | 仅片段证据,引用时必须写明「仅片段」 |
为什么按「本场最长的 80%」而不是死认 60 分钟:5 小时场里剪出来的 1.5 小时高光够过 60 分钟线,
按死线会被算成「整场」,正好是最容易误导人的那种误判。
如果整场是 0 条,报告会直接写「这场没有整场源,先别判定没录」——实测转录号普遍在
次日凌晨到次日下午才补整场,当晚搜不到属正常,建议等 3~6 小时重跑同一条命令。
第 2 步 · 拉音 + 转写 + 合并 → scripts/pull_live.py
python pull_live.py 0927 --src BV1xxxxxxxxx --pages all \
--note "09月27日 夜场" --start 20:31
链路:取 playurl(选最小带宽的 m4s 音轨)→ 解码 16k 单声道 wav → 300 秒一块转写 → 合并 → 生成挂钟对照版。
--start 给的是第 1 步读到的起录时间,会额外产出 <date>_wall.txt,每行同时给「转写内时刻 / 挂钟时刻」,读起来省脑子。
断点续跑:wav 存在就跳下载、chunks 有内容就跳转写。中断了原命令重跑即可,不会重来。
跑完会自动清中间件(m4s 和 wav)。Windows 上删除偶尔会被杀软/删除守卫占着句柄,
这时会打一条 ⚠ 有 N 个临时文件没删掉 并列出大小,不影响转写结果。
要收拾残留跑:
python clean.py # 只盘点:列出残留文件和总大小,不删
python clean.py --apply # 真删
python clean.py --date 0927 --chunks --apply # 连分块 jsonl 一起清
clean.py 只碰 week/ 下的 *.m4s / *.wav / chunks/*.jsonl,
*_segs.jsonl、*_merged.txt、*.srt、*.vtt 这些成品永远不动。
耗时参考(实测值,非估算):120 分钟场次用 small/int8/6 线程转完 15.6 分钟(约 7.7 倍实时);
纯语音片段能到 14 倍;有其它程序抢 CPU 时会掉到 3–4 倍。4 小时场约 25–40 分钟。
跑之前先算一下值不值 —— 只想确认一句话,先看切片够不够。
转写阶段会自动做三件事
| 动作 | 由谁控制 | 说明 |
|---|---|---|
| 热词注入 | 自动,取 subject.name + aliases + extra_keywords |
⚠ 只放核实过主体确实会念出来的词。2026-10-18 实测:把标题词「开场舞」放进表里,模型把 40:05–40:17 的 4~5 句「开麦」全塌成了 7 遍「开场舞」,字数还从 619 掉到 517。拿不准的词别放 —— 少一个热词只是几个同音错字,多一个假热词是整段幻觉。详见 references/pitfalls.md 第 25 条 |
| 置信度落盘 | 每段自动记 wp / lp / ns / cr |
wp 是段级(平均词概率),另外三项是窗口级;见下节 |
| 词级时间戳 | asr.word_timestamps,默认开 |
关掉体积小、快一到两成,但只能定位到句(±10 秒) |
| prompt 回声剥离 | 自动 | 模型有 0.05% 概率把 prompt 主体原样吐进稿里(含退化重复),会按「最长公共子串 ≥6 字」剜掉,text 和词级数组 w 一起清 |
模型不用自己下:本地没有就按 hf-mirror.com → huggingface.co 顺序自动拉,下不完整站换下一个。
尺寸用环境变量 LRI_MODEL=tiny|base|small|medium|large-v3 或 asr.model 指定,默认 small(约 462MB)。
主流的 HF 镜像里 SJTUG 那条实测 404,所以没放进轮询表。
第 3 步 · 检索 → scripts/probe.py
python probe.py --list # 哪些日期已转写
python probe.py 0927 # 概览 + 高频词
python probe.py 0927 --kw 连麦,开会 # 任一命中
python probe.py 0927 --from 45:00 --to 52:00 # 时间窗
python probe.py 0927 --kw 连麦 --ctx 2 --bv BV1xxx --start 20:31 # 上下文+跳转+挂钟
python probe.py 0927 --kw 连麦 --words # 多给一行词级时间(精确到词)
python probe.py 0927 --kw 连麦 --hide-unsure # 直接滤掉标 ? 的可疑段
这一步最常用。--ctx 给上下文行,--bv + --start 让每条命中都带 ?t=秒 跳转链接和挂钟时间,报告里的证据行就是从这里复制出去的。
怎么判断某一段可不可信
检索输出里时间戳后面可能跟一个 ?,那是转写自检给的「这段没把握」。判据是词概率:
| 字段 | 含义 | 粒度 | 标记阈值 |
|---|---|---|---|
wp |
段内平均词概率 | 段级(真正的单段指标) | < 0.55 且字数 ≥5 才标 ? |
lp |
avg_logprob | 窗口级(同一块几十段共用一个值) | 只作退路 |
cr |
compression_ratio | 窗口级 | > 2.4 只作退路 |
ns |
no_speech_prob | 窗口级 | 只记录 |
短句(<5 字)不参与 wp 判定——这条是 2026-10-18 实测加的:
408 段说话样本里,1~4 字的段有 25% 低于 0.55,但文本本身是对的
(「好啊!」wp=0.07、「对啊」wp=0.00、「开场舞。」wp=0.01)。
原因是短句的概率被前后音乐/静音稀释了。而 ≥5 字的段吻合得很好:
5~8 字中位 0.78、9~15 字中位 0.82。修掉这条以后标 ? 率从 13.5% 降到 6.6%,
标记才重新变得可信。
这里有个实测教训,别被文档骗了:不少人(包括本技能的早期版本)拿 avg_logprob 当段级置信度用,
但 faster-whisper 是 cum_logprob / (seq_len+1) 按解码窗口算一次再分给窗口内所有段。
300 秒一块时,几十段的 lp 几乎完全相同(实测全落在 -0.41~-0.58),
拿它判单段等于没判。真正能区分好坏的是词概率的段均值:
同一段音频里,好段 wp 能到 0.75~0.90,烂段只有 0.17~0.52。
标了 ? 的段落引用前必须回听原片,否则很容易把 ASR 的同音错字当成本人原话写进报告。
第 4 步 · 出报告
from report_base import Report
r = Report("某某 09-27 直播内容梳理", "2026-09-27 夜场 · 20:31–00:17 · 226 分钟")
r.kpi([("全场时长", "226 分钟"), ("转写段数", "3,621"), ("证据条数", "63")])
r.h2("一、一句话结论")
r.quote("「原话」", "转写 0:55:47 · 21:25", "https://www.bilibili.com/video/BV1xxx/?t=3347")
r.timeline([(282, "20:36", "开场预告", "L1")], "BV1xxx")
r.save(r"...\报告.html")
骨架已经把 CSS、KPI 卡、时间轴、表格、可信度徽章、div 配平都包好了,正文不做 % 格式化,所以出现 50% 不需要转义。
第 5 步 · 要发切片时才用 · 导出字幕 → scripts/export_sub.py
python export_sub.py 0927 # 出 0927.srt / 0927.vtt / 0927_字幕校验.txt
python export_sub.py 0927 --max-chars 14 # 竖屏短视频,行宽更紧
python export_sub.py 0927 --cps 7 --min-dur 1.2 # 老人向内容,念得更慢
python export_sub.py 0927 --no-merge # 不合并碎句(逐句对照原音时用)
whisper 的 segment 是按「语音停顿」切的,常常一停十几秒、一行四五十字,直接当字幕会一屏塞满。
这个脚本按「句末标点 → 逗号 → 硬切」三级重断。
有词级时间就按词切(准),没有才退回「把时间按字数比例摊回原区间」的估法。
两者差别很大:实测有一段的 start–end 横跨 30.5 秒,里面只有「你的旋律」4 个字
(中间全是音乐),按比例摊会变成 30 秒的超慢字幕,按词切就是正常的 0.6 秒。
碎句会合并。实况里满屏是「谢谢」「好的」这种 0.2 秒的句子,单看每条都超语速,
并到邻句就正常了。实测一场 2 小时连麦场:合并让字幕从 1448 行降到 946 行,
超语速条数从 183 降到 21。
语速按等效字数算:中文 1 字算 1,英文 1 词算 2。
不这么算,中英混排时英文行会全部爆表(实测最离谱的「34 字/秒」全是英文歌词)。
导出时会统一做简繁转换,避免「老的聲音」和简体段文本混排。
同时输出一份校验报告:总行数、平均行长、全片语速、语速超标条数(念不完)、超行宽条数、过短条数。
语速超标只报告不改时间轴 —— 自动拉长会和下一句撞车,宁可让剪辑的人看到问题再定。
三条铁判据(用它们防止把推测写成事实)
① 三源互证,至少占两源
- A 源:UP 主置顶的 AI 摘要(常自带时间戳,最省力但可能有误)
- B 源:UP 整理的时间表 / 切片标题(能定位环节,粒度为分钟)
- C 源:本地转写(最硬,能到秒,但 ASR 对数字和专名不可靠)
三者冲突时以 C 为准,但数字类结论必须回到 A/B 至少一条。
② 每个结论标可信度
- L1 本人当场说出 + 独立来源 ≥2 条
- L2 单一来源但可自洽验证(如「随即进入奖励环节」反证赢了)
- L3 只有转述/推测,或 ASR 单独给出且无法复核
③ 取不到就写取不到
没报分、没有奖惩环节、所有源都不提胜负 —— 就写「结果未取到,不作结论」。
一句「我真的很努力了」听起来像输了,但那是语气推断,不是证据。
同理,标了 ? 的转写段落也不能当原话引用,除非回听原片确认过。
关系网怎么建(如果任务涉及)
转写全文不适合用来统计人名:实测同一人会被 ASR 写成 3–5 种变体,单个名字全片只出现个位数次。
可靠做法是从切片标题建——尤其是像
【下半场】《9人三局》09.27#A#B#C#D 这种用 # 把局内所有人列出来的标题,那才是实体证据。
转写只用来核对单场细节。详见 references/relationship.md。
隐私与合规(发布/分享前必读)
这套能力作用于真人,产出物一旦外发就有边界问题:
- 只在个人研究范围内使用。不要把他人关系网、私交、打赏情况、行程作息整理成档案分发。
- 不要采集或比对声纹。声纹属于生物识别信息,未经本人同意建立他人声纹库并做身份识别,在多数司法辖区都是高风险行为。
- 报告里引用原话要限定在公开直播内容,不要延伸到直播间外的私人信息,不要据此去定位真人身份/住址/联系方式。
- 涉及未成年人、医疗、财务等敏感内容,一律不做扩散。
本技能踩过的坑
完整清单见 references/pitfalls.md,最要紧的四条:
- B 站搜索返回
code=0但result为空 = 风控,不是没内容。补buvid3后同一个词立刻命中。 - 合集接口
items_lists已从数组变字典,按旧结构遍历会静默返回空,不报错。 - 标题里的
YYMMDD会跨年误判:250927会被当成次年的 09-27。 - 多 P ≠ 上下集:两个 P 时长几乎相同多半是同一场的不同版本,按上下集合并会让时间轴翻倍。
红线
- 不编造时间戳。没读到起录时间就写「未读到」,不要用「大概 20:30」糊过去。
- 不把 ASR 数字当准。ASR 会把「一万两千九」写成「1万2900」或「1290」,数字结论必须找第二个源。
- 不跳过「取不到」。缺口要显式写在报告里,这是报告可信度的一部分。
- 不上传任何含真实人物档案的实例到公开技能库。
文件索引
SKILL.md 本文件:门禁 + 流程 + 判据
config.example.json 目标配置模板(preflight --set --emit 会自动生成 config.json)
scripts/
preflight.py 开跑前配置确认(独立入口,退出码 0/2/3,支持 --set/--emit/--json)
klib.py B站接口 + 下载解码 + 分块转写 + 合并 + 模型自动下载(被下面几个复用)
find_live.py 找某日全场录播源,自动抽起录时间
pull_live.py 下载 → 解码 → 转写 → 合并(断点续跑)
probe.py 转写检索:关键词 / 时间窗 / 上下文 / 跳转 / 挂钟 / 词级定位 / 可疑标记
export_sub.py 导出 SRT / VTT 字幕,带断句重排与语速校验
clean.py 盘点/清理残留的 m4s、wav、分块 jsonl(默认只盘点,--apply 才删)
report_base.py HTML 报告骨架(KPI/时间轴/表格/徽章,自带配平自检)
references/
sources.md 源类型识别:哪类源给什么、可靠性怎么排
schedule.md 「日期指纹」法:怎么把切片反查到具体直播日
relationship.md 关系网怎么建、怎么验证、什么时候不该建
pitfalls.md 完整坑清单(含接口变更与环境问题)
数据默认落在 ~/.live_replay_intel(可用环境变量 LRI_BASE 改),
转写模型落在 <LRI_BASE>/models/faster-whisper-<尺寸>。