已有视频、字幕或操作日志,却卡在“按秒、按片段还是按动作切”的问题上,通常不是数据量不够,而是配对单位没有定下来。决定粒度的不是模型名称,而是三个事实:视频侧最小可抽帧单位、文本侧是否带可靠时间戳、动作标签能不能给出起止时间。这三者里最短又最稳的区间,一般就是配对的天然单位。建议先在本地把同一批素材各切一组小样本,用同一套加载脚本读出来,比完长度和缺失,再写进数据规范。
适用场景:手里有视频,同时有字幕、转写文本或操作日志,但还没确定按什么粒度配对。操作动作:同一批素材分别按 1 秒窗口、语义片段、动作标签切三组,用同一个加载脚本跑通,输出对齐状态和缺失原因。验证方式:逐条检查文本时间区间是否完全落在视频片段区间内,跨段与贴边样本单独存放。风险边界:抽帧策略、字幕时间戳精度、动作标签完备性都会改变结论,换数据源后需要重跑比较,不宜把某一次结果当成长期规范。
先按秒、按片段、按动作三种粒度各切一组样本
切粒度不是调一个参数,而是造一组能互相比较的样本。三组样本最好来自同一批原始视频,保持视频来源、抽帧策略、文本来源一致,唯一变化的只有切分粒度,否则后面的长度和缺失比较就没有意义。
- 按秒:以视频起点为时间零点,固定 1 秒窗口,窗口用 [start_ms, end_ms) 半开区间,避免相邻窗口共用边界帧。抽帧数量先固定,例如每窗抽 1 到 4 帧,三组样本都要用同一套抽帧参数。
- 按片段:以字幕句末、镜头切换或人工标注的语义边界作为切点,片段长度不做硬性统一,但要记录长度分布。片段粒度更容易承载一句完整描述。
- 按动作:以操作日志或动作标签的 start/end 作为切点,动作区间允许重叠;重叠时按时间先后各出一条样本,不合并。
样本数量先控制在每组几十条,例如 30 到 50 条,覆盖相近的视频段落即可看出差别,不必一上来跑全量。命名建议用 {granularity}-{source_id}-{start_ms}-{end_ms} 的格式,sample_id 全局唯一,且在 manifest、帧缓存、文本记录里保持同名,方便脚本回查某一条到底切在哪里。
| 粒度 | 切分依据 | 视频侧内容 | 文本侧内容 | 典型长度 | 适合判断什么 |
|---|---|---|---|---|---|
| 按秒 | 固定时间窗口 | 每窗抽取固定帧数 | 完全落在窗内的字幕或转写片段 | 短,语义碎 | 时间戳是否可信、帧解码是否稳定 |
| 按片段 | 字幕句界或镜头边界 | 整段的多帧 | 完整句子或短段落 | 中等,长度浮动 | 视频与文本语义能否对上 |
| 按动作 | 操作日志或标签起止 | 动作区间内的帧 | 该动作对应的说明文本 | 不定,可能很短 | 控制类标签是否够用、缺失多不多 |
检查视频帧和文本片段的对齐点是否落在同一时间区间
配对的判定条件是时间区间包含关系,不是“看起来差不多就该配在一起”。先把单位统一成毫秒,再把时间基准统一成“相对视频起点的偏移”,动作日志如果带的是墙钟时间,需要先做一次偏移换算并记录换算依据。
- 帧时间戳:用帧序号除以帧率,或直接读取容器给出的时间戳,同一批样本只能选一种算法,并把算法名写进记录。
- 文本时间戳:如果字幕只有秒级精度,先按秒扩展成起止毫秒,同时写入 precision 字段,避免在毫秒级比较里被当成精确值。
- 包含判定:建议用 text.start_ms >= clip.start_ms 且 text.end_ms <= clip.end_ms。若允许重叠匹配,先算出 overlap_ms,超过设定阈值才归入该片段,并把 overlap_ms 一起写出来。
边界样本要专门留几条出来看。比如一条字幕从 990ms 到 2010ms,1 秒窗口 [1000, 2000) 装不下它,这就是跨段;再比如动作标签起点早于视频首帧时间戳,说明日志与视频起点没有对齐;还有文本 end_ms 恰好等于片段 end_ms 的贴边情况,这类样本可以通过包含判定,但要单独标记,后面回归时重点看。跨段的处理方式通常有三种:直接丢弃、裁剪到片段区间并标记 truncated、按重叠比例归到主要片段。选哪一种要写死在规范里,加载脚本不要静默保留跨段样本。
用通用加载脚本读取三组样本并输出配对结果
这一节的目的很单纯:确认目录里的三组样本能被同一段代码读出来,并且每条记录都能给出配对状态。下面的目录结构和伪代码只是骨架,字段名可以按现有数据替换,但粒度标识、起止时间、对齐状态这三类字段建议保留。
orca_data/
sec/
manifest.jsonl # 每条 1 秒窗口一行
texts.jsonl # 文本片段,带 start_ms / end_ms / precision
frames/ # 可选,缓存已抽好的帧
segment/
manifest.jsonl
texts.jsonl
action/
manifest.jsonl
texts.jsonl
boundary/
manifest.jsonl # 跨段、贴边、零长文本等边界样本
README.md # 数据规范、版本号、粒度定义
# manifest.jsonl 每行字段建议:
# sample_id, source_id, start_ms, end_ms, fps, frame_count, frame_ts_ms, usable
# texts.jsonl 每行字段建议:
# source_id, text, start_ms, end_ms, precision, action_label
import json
def load_manifest(path):
rows = []
with open(path, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line:
rows.append(json.loads(line))
return rows
def pick_text(clip, texts):
inside = [t for t in texts
if t['start_ms'] >= clip['start_ms']
and t['end_ms'] <= clip['end_ms']]
if not inside:
return None, 'cross_boundary_or_missing'
return ' '.join(t['text'] for t in inside), 'ok'
for g in ['sec', 'segment', 'action']:
clips = load_manifest('data/' + g + '/manifest.jsonl')
texts = load_manifest('data/' + g + '/texts.jsonl')
for c in clips:
text, status = pick_text(c, texts)
print({
'sample_id': c['sample_id'],
'granularity': g,
'clip_start_ms': c['start_ms'],
'clip_end_ms': c['end_ms'],
'frame_count': c.get('frame_count', 0),
'text': text,
'align_status': status,
'usable': status == 'ok',
})
输出字段至少覆盖这些:sample_id、granularity、clip_start_ms、clip_end_ms、frame_count 与 frame_ts_ms、text 与 text_start_ms/text_end_ms、text_precision、align_status、usable、drop_reason。align_status 建议用受控取值,例如 ok、cross_boundary、partial_overlap、missing、decode_failed,不要写自由文本,否则后面的统计没法聚合。
比较三种粒度下的输入长度和缺失比例
三组样本跑完之后,比较两件事:单条样本的输入长度,以及不可用样本的分布。长度不要只看平均值,按最小值、中位数、最大值三个点看更能暴露长尾;秒级窗口的文本通常很短,片段粒度的文本长度浮动大,动作粒度的样本长度可能极短也可能很长,这些都需要先看清楚再决定。
- 长度统计:分别记录每组的帧数、文本字符数或词数、文本条数,以及超长样本的 sample_id,方便回看是不是切分规则导致的。
- 缺失统计:统计无对应文本、文本跨段、无动作标签、帧解码失败、零长度文本这几类的条数,并给出它们在各自组内的占比字段,而不是凭印象判断。
- 不可用样本标记:在 manifest 里保留 usable 布尔值和 drop_reason 枚举值,例如 text_missing、text_cross_boundary、no_action_tag、decode_failed。文件不要删,删掉之后就再也无法判断换数据后是同一批样本被漏掉了,还是新增了别的缺失。
判断哪种粒度更可维护,可以看两点:一是边界样本占比是否稳定,二是同一条原始素材在换切分参数后能否复现同样的配对结果。如果某个粒度下大量样本需要人工裁剪才能对齐,维护成本通常偏高;如果缺失主要由标签本身没有覆盖造成,那问题在数据采集侧,不在切分规则。
选定一种粒度后写进数据规范,并留一组边界样本做回归
粒度一旦选定,就要写进数据规范并带上版本号,否则换一批视频、换一个标注人之后配对关系很容易悄悄失效。规范条目建议至少包含:粒度定义与时间窗口单位、时间基准和换算方式、帧时间戳算法、抽帧策略与每窗帧数、文本包含判定规则、文本截断与跨段处理方式、缺失枚举值、sample_id 命名规则、数据版本号与变更记录。
边界样本单独存一份清单,每次数据更新后跑一遍回归。清单里通常放这几类:字幕横跨两个窗口的样本、动作标签横跨窗口或在窗口外起止的样本、零长度文本、完全没有字幕但画面有动作的片段、恰好贴边的文本、帧率异常或抽帧失败的片段。
- 回归检查点一:加载脚本能读完全部样本,输出字段齐全,没有因为缺失字段直接抛异常。
- 回归检查点二:对齐判定与规范一致,跨段样本被明确标记,而不是混进 ok 里。
- 回归检查点三:不可用样本仍被登记在 manifest 中,drop_reason 无新增未定义取值。
- 回归检查点四:sample_id 能按命名规则还原出粒度、来源和起止时间,且与目录中实际文件对得上。
这四步跑通,才算把“喂什么数据、按什么粒度配对”落到可验证的状态;任何一步只靠人工确认而没有脚本输出,换数据后都很容易重新变成一笔糊涂账。