feat: frame-based positioning and mark system foundation

Core Changes:
- Fix SearchView to use start_frame/end_frame directly (no time*fps conversion)
- Add hard_delete support to delete_trace API
- VideoPlayer: Main timeline + Mark system foundation
- Proxy: Add local routes for auth, media, identity-matches, cluster-results
- Add .gitignore to exclude build artifacts and dependencies

Design Documents:
- Multi-track Mark system design (.opencode/plans/)
- Video editing positioning standards research

Files Modified:
- src/views/SearchView.vue: Frame positioning, ensureMinDuration (240 frames)
- src/views/PeopleView.vue: batchDeleteGroups with hard_delete
- src/api/index.ts: delete_trace with hard_delete body
- src/components/VideoPlayer.vue: Timeline + Mark UI
- src-tauri/src/proxy.rs: New local routes
- AGENTS.md: Update documentation
This commit is contained in:
2026-07-24 20:19:47 +08:00
parent 299f58e883
commit 5951aca086
38 changed files with 12107 additions and 1666 deletions

View File

@@ -0,0 +1,978 @@
# 多轨 Mark 系统设计文档
## 一、概述
### 1.1 目标
构建一个多轨道视频/音频编辑展示系统,支持:
- 多轨 Video/Audio 同步播放
- 每个轨道独立的 Mark 系统
- 基于 Frame 的精确定位和对齐
### 1.2 核心原则
| 原则 | 说明 |
|------|------|
| **Frame 是唯一标准** | 所有定位、长度、对齐都用 Frame整数 |
| **Time 仅用于显示** | Frame / FPS 转换,不存储 |
| **整数运算** | 所有 Frame 操作都是整数,避免浮点精度问题 |
| **原点对齐** | Frame 0 = Time 0所有轨道共享同一原点 |
| **轨道独立** | 每个轨道有独立的 Mark 系统 |
| **同步播放** | 全局播放头对齐所有轨道 |
### 1.3 概念统一
**核心洞察**Trace 和 Segment 只是不同 tag 的 Marks
```
传统概念 → 统一概念
────────────────────────────────
Trace → Marks with tag='face'
Segment → Marks with tag='segment'
ASR 分析 → Marks with tag='asr'
Noise 检测 → Marks with tag='noise'
用户标记 → Marks with tag='user'
```
**好处**
- 统一数据模型
- 简化代码实现
- 一致的交互行为
- 灵活扩展新类型
---
## 二、架构设计
### 2.1 层级架构
```
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (Application) │
│ - 剪接工具 │
│ - 审核工具 │
│ - 播放器 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 项目层 (Project) │
│ - 多轨道管理 │
│ - 全局播放状态 │
│ - FPS / TotalFrames 元数据 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 轨道层 (Track) │
│ - Video Track (视频轨道 + Marks) │
│ - Audio Track (音频轨道 + Marks) │
│ - Mark Track (纯 Mark 轨道) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Mark 层 (Mark) │
│ - startFrame / endFrame (整数) │
│ - tag / note / metadata │
│ - 属于特定 Track │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 数据流
```
┌──────────────┐
│ 数据源 │
│ - Face API │
│ - ASR API │
│ - Noise API │
└──────┬───────┘
│ 获取数据
┌──────────────┐
│ 转换层 │
│ Time→Frame │ (用 FPS)
│ 统一格式 │
└──────┬───────┘
│ Mark 数据
┌──────────────┐
│ Project │
│ 多轨道管理 │
└──────┬───────┘
│ Track + Marks
┌──────────────┐
│ 展示层 │
│ - Timeline │
│ - 播放器 │
└──────────────┘
```
---
## 三、数据结构
### 3.1 核心类型定义
```typescript
// ========== 基础类型 ==========
type Frame = number // 整数,绝对定位标准
// ========== Mark ==========
interface Mark {
id: string
trackId: string // 所属轨道
// 定位Frame整数
startFrame: Frame
endFrame?: Frame
// 分类与内容
tag: string // 'face' | 'asr' | 'noise' | 'user' | ...
note?: string
metadata?: any // 原始数据(如 ASR text, noise dB
// 审核状态
status?: 'pending' | 'confirmed' | 'resolved' | 'ignored'
severity?: 'low' | 'medium' | 'high'
}
// ========== Clip ==========
interface Clip {
id: string
// 源文件范围
sourceStartFrame: Frame
sourceEndFrame: Frame
// 时间轴位置(全局 Frame
timelineStartFrame: Frame
timelineEndFrame: Frame
}
// ========== Track ==========
interface Track {
id: string
type: 'video' | 'audio' | 'mark-only'
name: string
enabled: boolean // 是否启用
// 轨道内容
file_uuid?: string
clips?: Clip[]
// 独立的 Mark 系统
marks: Mark[]
}
// ========== Project ==========
interface Project {
id: string
name: string
// 全局元数据(所有轨道共享)
fps: number // 帧率
totalFrames: Frame // 总帧数(最长轨道)
duration: number // 总时长(秒),仅用于显示
// 多轨道
tracks: Track[]
// 播放状态
playhead: Frame // 当前播放位置(全局)
playing: boolean
}
```
### 3.2 Frame 操作规范
```typescript
// 所有 Frame 操作都是整数
// Frame → Time仅显示用
function frameToTime(frame: Frame, fps: number): number {
return frame / fps
}
// Time → Frame数据转换用
function timeToFrame(time: number, fps: number): Frame {
return Math.round(time * fps)
}
// 位置百分比Timeline 显示)
function frameToPercent(frame: Frame, totalFrames: Frame): number {
return (frame / totalFrames) * 100
}
// 格式化显示
function formatFrame(frame: Frame, fps: number): string {
const sec = frame / fps
const m = Math.floor(sec / 60)
const s = Math.floor(sec % 60)
return `${m}:${s.toString().padStart(2, '0')}`
}
```
---
## 四、轨道系统
### 4.1 轨道类型
| 类型 | 说明 | 内容 | Marks |
|------|------|------|-------|
| **Video Track** | 视频轨道 | video clips + file_uuid | face, noise, blurry, user... |
| **Audio Track** | 音频轨道 | audio clips + file_uuid | asr, silence, beat, noise... |
| **Mark Track** | 纯标记轨道 | 无媒体内容 | user, comment, todo, review... |
### 4.2 轨道 UI 布局
```
┌─────────────────────────────────────────────────────────────┐
│ Global Playhead: Frame 12345 (0:24.5) │
│ ▼ │
│ ──────────────────────────────────────────────────────── │
│ │
│ ☑ V1 (主视频) │
│ ├─ ████████████████████████████████ │
│ └─ marks: │
│ ├─ [face] ████ F100-120 │
│ ├─ [face] ███████████ F500-650 │
│ └─ [noise] ████ F800-900 │
│ │
│ ☑ A1 (原声) │
│ ├─ ████████████████████████████████ │
│ └─ marks: │
│ ├─ [asr] ████ F100-120 "你好" │
│ └─ [noise] ████ F800-900 │
│ │
│ ☑ M1 (审核标记) │
│ └─ marks: │
│ ├─ [user] ▼ F500 "需要检查" │
│ └─ [user] ▼ F1000 "待确认" │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 4.3 轨道操作
```typescript
interface TrackOperations {
// 播放控制
play(): void
pause(): void
seek(frame: Frame): void
// Mark 操作
addMark(mark: Omit<Mark, 'id' | 'trackId'>): Mark
updateMark(id: string, updates: Partial<Mark>): void
deleteMark(id: string): void
// 批量操作
batchUpdateMarks(filter: (m: Mark) => boolean, updates: Partial<Mark>): void
batchDeleteMarks(filter: (m: Mark) => boolean): void
// 导航
nextMark(tag?: string): Mark | null
prevMark(tag?: string): Mark | null
}
```
---
## 五、Mark 系统
### 5.1 Mark 类型与来源
| Tag | 来源 | 数据格式 | 转换 |
|-----|------|----------|------|
| **face** | Face Trace API | first_frame, last_frame | 无需转换 |
| **asr** | ASR API | start_time, end_time | Time → Frame |
| **noise** | 噪声检测 | start_time, end_time | Time → Frame |
| **silent** | 静音检测 | start_time, end_time | Time → Frame |
| **user** | 用户创建 | frame | 无需转换 |
| **comment** | 用户评论 | frame | 无需转换 |
### 5.2 Mark 数据源转换
```typescript
// Face Trace → Mark
function traceToMark(trace: FaceTrace, trackId: string): Mark {
return {
id: generateId(),
trackId,
startFrame: trace.first_frame,
endFrame: trace.last_frame,
tag: 'face',
note: trace.identity_name,
metadata: trace
}
}
// ASR → Mark
function asrToMark(asr: AsrChunk, trackId: string, fps: number): Mark {
return {
id: generateId(),
trackId,
startFrame: timeToFrame(asr.start_time, fps),
endFrame: timeToFrame(asr.end_time, fps),
tag: 'asr',
note: asr.text,
metadata: asr
}
}
// Noise → Mark
function noiseToMark(noise: NoiseSegment, trackId: string, fps: number): Mark {
return {
id: generateId(),
trackId,
startFrame: timeToFrame(noise.start_time, fps),
endFrame: timeToFrame(noise.end_time, fps),
tag: 'noise',
note: `${noise.db}dB`,
metadata: noise,
severity: noise.db > -30 ? 'high' : noise.db > -40 ? 'medium' : 'low'
}
}
```
### 5.3 Mark 操作表
| Tag | 操作 | 参数 |
|-----|------|------|
| **noise** | 删除片段 | padding: 5 frames |
| **silent** | 静音处理 | fade: 10ms |
| **blurry** | 标记忽略 | - |
| **face** | 绑定身份 | identity_uuid |
| **asr** | 编辑文本 | new_text |
| **user** | 自定义 | - |
---
## 六、Mark 点击交互行为
### 6.1 核心概念
点击 Mark 时,根据**当前播放状态**和**全局 PlayMode** 决定行为。
### 6.2 Sync Lock同步锁
```typescript
// 全局同步锁
const syncLock = ref<boolean>(true) // 默认锁定
// 锁定 🔒:点击 Mark → 所有轨道跳转到同一 frame
// 解锁 🔓:点击 Mark → 仅当前轨道跳转
```
**UI 控件**
```
[🔒 Sync ON] / [🔓 Sync OFF]
```
### 6.3 PlayMode全局播放模式
```typescript
type PlayMode = 'normal' | 'continue' | 'loop'
const globalPlayMode = ref<PlayMode>('normal')
```
| 模式 | 行为 |
|------|------|
| **Normal** | 播放到 Mark.endFrame 后停止 |
| **Continue** | 播放到 Mark.endFrame 后继续播放 |
| **Loop** | 循环播放 Mark.startFrame ~ endFrame |
**UI 控件**
```
[Normal] [Continue] [Loop]
```
### 6.4 点击逻辑流程
```
┌─────────────────────────────────────────────────────┐
│ 点击 Mark 触发 │
├─────────────────────────────────────────────────────┤
│ │
│ 1. Seek 到 Mark.startFrame │
│ ├─ SyncLock ON → 所有轨道同步跳转 │
│ └─ SyncLock OFF → 仅当前轨道跳转 │
│ │
│ 2. 检查当前播放状态 │
│ ├─ Pause → 只跳转,不播放 │
│ └─ Play → 跳转后根据 PlayMode 播放 │
│ │
│ 3. 根据 PlayMode 设置播放行为 │
│ ├─ Normal → 设置 stopAtFrame │
│ ├─ Continue → 清除 stopAtFrame │
│ └─ Loop → 设置 loopRange │
│ │
└─────────────────────────────────────────────────────┘
```
### 6.5 代码实现
```typescript
// 播放状态
const playhead = ref<Frame>(0)
const isPlaying = ref<boolean>(false)
const stopAtFrame = ref<Frame | null>(null)
const loopRange = ref<{ start: Frame; end: Frame } | null>(null)
// 点击 Mark
function onMarkClick(mark: Mark) {
// 1. Seek
if (syncLock.value) {
seekAllTracks(mark.startFrame) // 所有轨道同步
} else {
seekTrack(mark.trackId, mark.startFrame) // 仅当前轨道
}
// 2. Pause 状态:只跳转,不播放
if (!isPlaying.value) return
// 3. Play 状态:设置播放行为
switch (globalPlayMode.value) {
case 'normal':
stopAtFrame.value = mark.endFrame || mark.startFrame
loopRange.value = null
break
case 'continue':
stopAtFrame.value = null
loopRange.value = null
break
case 'loop':
// 不退出 loop点击其他 mark 不退出)
loopRange.value = {
start: mark.startFrame,
end: mark.endFrame || mark.startFrame
}
break
}
play()
}
// PlayMode 切换
function setPlayMode(mode: PlayMode) {
globalPlayMode.value = mode
// 切换模式时退出 loop
if (mode !== 'loop') {
loopRange.value = null
}
}
// 播放控制器
function tick() {
if (!isPlaying.value) return
const currentFrame = playhead.value
// 检查 Loop 模式
if (loopRange.value && currentFrame >= loopRange.value.end) {
seekToFrame(loopRange.value.start)
requestAnimationFrame(() => tick())
return
}
// 检查 Normal 模式的停止点
if (stopAtFrame.value !== null && currentFrame >= stopAtFrame.value) {
pause()
return
}
// Continue 模式:正常播放
playhead.value++
requestAnimationFrame(() => tick())
}
```
### 6.6 Loop 退出条件
| 操作 | 是否退出 Loop |
|------|--------------|
| 点击其他 Mark | ❌ 不退出 |
| 切换 PlayMode | ✅ 退出 |
| 手动暂停 | ✅ 退出 |
| 关闭播放器 | ✅ 退出 |
### 6.7 UI 控件设计
```
┌─────────────────────────────────────────────────────┐
│ 控制栏 │
├─────────────────────────────────────────────────────┤
│ [🔒 Sync] | [Normal] [Continue] [Loop] │
│ │
│ 状态指示: │
│ - Sync: ON/OFF │
│ - Mode: Normal/Continue/Loop │
│ - Loop Range: F100-200 (如果 looping) │
│ - Stop At: F300 (如果 normal mode) │
└─────────────────────────────────────────────────────┘
```
---
## 七、同步播放
### 6.1 播放机制
```typescript
class PlaybackController {
private project: Project
private playhead: Frame = 0
private playing: boolean = false
private startTime: number = 0
private startFrame: Frame = 0
play() {
this.playing = true
this.startTime = performance.now()
this.startFrame = this.playhead
this.tick()
}
private tick() {
if (!this.playing) return
const elapsed = (performance.now() - this.startTime) / 1000
this.playhead = this.startFrame + Math.round(elapsed * this.project.fps)
// 同步所有轨道
this.syncAllTracks(this.playhead)
requestAnimationFrame(() => this.tick())
}
private syncAllTracks(frame: Frame) {
this.project.tracks.forEach(track => {
if (!track.enabled) return
// 更新媒体内容
if (track.type === 'video') {
this.syncVideo(track, frame)
} else if (track.type === 'audio') {
this.syncAudio(track, frame)
}
// 高亮 Marks
this.highlightMarks(track.marks, frame)
})
}
seek(frame: Frame) {
this.playhead = frame
this.startTime = performance.now()
this.startFrame = frame
this.syncAllTracks(frame)
}
}
```
### 6.2 对齐保证
```typescript
// 验证原点对齐
function validateAlignment(project: Project): boolean {
// 所有轨道共享原点
const globalPlayhead = project.playhead
project.tracks.forEach(track => {
// 所有轨道的当前帧 = 全局播放头
const trackFrame = globalPlayhead
// Mark 高亮基于同一 frame
track.marks.forEach(mark => {
const isActive = mark.startFrame <= trackFrame &&
(mark.endFrame || mark.startFrame) >= trackFrame
})
})
return true
}
```
---
## 八、Timeline 设计
### 8.1 Timeline 对齐原则
```
所有 Timeline 共享同一基准:
┌─────────────────────────────────────────────────────────────┐
│ 基准Frame 0 ~ TotalFrames (65000) │
├─────────────────────────────────────────────────────────────┤
│ │
│ Main Timeline │
│ ██████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ ▲ Playhead: F12345 │
│ │
│ V1 Timeline │
│ ████████████████████████████████ │
│ [face] ████ [noise] ████ │
│ │
│ A1 Timeline │
│ ████████████████████████████████ │
│ [asr] ████ [asr] ████ │
│ │
│ M1 Timeline │
│ [user] ▼ [user] ▼ │
│ │
│ 所有 Timeline 长度一致,位置百分比计算相同 │
└─────────────────────────────────────────────────────────────┘
```
### 8.2 Timeline 计算
```typescript
// 所有 Timeline 用相同公式
const totalFrames = project.totalFrames
// 位置(百分比)
function getPosition(frame: Frame): number {
return (frame / totalFrames) * 100
}
// 宽度(百分比)
function getWidth(startFrame: Frame, endFrame: Frame): number {
return ((endFrame - startFrame) / totalFrames) * 100
}
// 像素位置(用于精确渲染)
function getPixelPosition(frame: Frame, width: number): number {
return Math.round((frame / totalFrames) * width)
}
```
---
## 九、API 设计
### 9.1 Project API
```typescript
// 创建项目
POST /api/v1/project
{
name: string,
fps: number,
totalFrames: number,
tracks: TrackConfig[]
}
// 获取项目
GET /api/v1/project/:id
// 更新项目
PUT /api/v1/project/:id
// 添加轨道
POST /api/v1/project/:id/track
{
type: 'video' | 'audio' | 'mark-only',
name: string,
file_uuid?: string
}
```
### 9.2 Track API
```typescript
// 获取轨道
GET /api/v1/track/:id
// 更新轨道
PUT /api/v1/track/:id
{
enabled?: boolean,
name?: string
}
// 删除轨道
DELETE /api/v1/track/:id
```
### 9.3 Mark API
```typescript
// 获取轨道的所有 Marks
GET /api/v1/track/:trackId/marks
// 添加 Mark
POST /api/v1/track/:trackId/mark
{
startFrame: Frame,
endFrame?: Frame,
tag: string,
note?: string
}
// 更新 Mark
PUT /api/v1/mark/:id
{
startFrame?: Frame,
endFrame?: Frame,
tag?: string,
note?: string,
status?: string
}
// 删除 Mark
DELETE /api/v1/mark/:id
// 批量操作
POST /api/v1/track/:trackId/marks/batch
{
action: 'update' | 'delete',
filter: { tag?: string, status?: string },
updates?: Partial<Mark>
}
```
---
## 十、数据持久化
### 10.1 SQLite 表结构
```sql
-- 项目表
CREATE TABLE projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
fps INTEGER NOT NULL,
total_frames INTEGER NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- 轨道表
CREATE TABLE tracks (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
type TEXT NOT NULL, -- 'video' | 'audio' | 'mark-only'
name TEXT NOT NULL,
file_uuid TEXT,
enabled INTEGER DEFAULT 1,
position INTEGER, -- 轨道顺序
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects(id)
);
-- Mark 表
CREATE TABLE marks (
id TEXT PRIMARY KEY,
track_id TEXT NOT NULL,
start_frame INTEGER NOT NULL,
end_frame INTEGER,
tag TEXT NOT NULL,
note TEXT,
status TEXT DEFAULT 'pending',
severity TEXT,
metadata TEXT, -- JSON
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (track_id) REFERENCES tracks(id)
);
-- 索引
CREATE INDEX idx_marks_track ON marks(track_id);
CREATE INDEX idx_marks_frame ON marks(start_frame, end_frame);
CREATE INDEX idx_marks_tag ON marks(tag);
```
---
## 十一、UI 组件
### 11.1 组件层级
```
App
└─ ProjectEditor
├─ Toolbar
│ ├─ PlayButton
│ ├─ FrameInput
│ └─ ZoomControl
├─ TimelinePanel
│ ├─ PlayheadIndicator
│ ├─ TrackList
│ │ ├─ TrackItem (Video)
│ │ ├─ TrackItem (Audio)
│ │ └─ TrackItem (Mark-only)
│ └─ TimeScale
└─ VideoPlayer (可选)
└─ MarkOverlay
```
### 11.2 核心组件
#### TimelinePanel
```vue
<template>
<div class="timeline-panel">
<div class="playhead" :style="{ left: playheadPct + '%' }"></div>
<div v-for="track in tracks" :key="track.id" class="track-row">
<div class="track-header">
<input type="checkbox" v-model="track.enabled" />
<span>{{ track.name }}</span>
</div>
<div class="track-timeline" @click="onTimelineClick">
<div
v-for="mark in track.marks"
:key="mark.id"
class="mark"
:class="{ active: isMarkActive(mark) }"
:style="getMarkStyle(mark)"
@click.stop="onMarkClick(mark)"
>
{{ mark.tag }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const playheadPct = computed(() =>
(project.playhead / project.totalFrames) * 100
)
function getMarkStyle(mark: Mark) {
return {
left: `${(mark.startFrame / project.totalFrames) * 100}%`,
width: `${((mark.endFrame || mark.startFrame) - mark.startFrame) / project.totalFrames * 100}%`
}
}
</script>
```
---
## 十二、实现步骤
### 12.1 Phase 1: 基础架构P0
1. **数据结构定义**
- 定义 TypeScript 接口
- 实现 Frame 操作函数
2. **SQLite 存储**
- 创建表结构
- 实现 CRUD API
3. **单轨道 Mark 系统**
- Track + Mark 基础功能
- Timeline 展示
### 12.2 Phase 2: 多轨播放P1
1. **多轨道管理**
- Project 层实现
- Track 增删改
2. **同步播放**
- PlaybackController
- 全局 Playhead
3. **Timeline 对齐**
- 统一计算公式
- 视觉对齐
### 12.3 Phase 3: 数据集成P1
1. **数据源转换**
- Face Trace → Mark
- ASR → Mark
- Noise → Mark
2. **FPS 管理**
- 视频元数据获取
- Time ↔ Frame 转换
### 12.4 Phase 4: 操作功能P2
1. **Mark 操作**
- 批量更新
- 批量删除
2. **剪辑功能**
- 基于 Mark 的剪辑
- 片段操作
---
## 十三、测试计划
### 13.1 单元测试
- Frame 操作函数
- Time ↔ Frame 转换
- Mark 过滤/排序
- Timeline 位置计算
### 13.2 集成测试
- 多轨同步播放
- Playhead 对齐
- Mark 高亮状态
- 数据持久化
### 13.3 E2E 测试
- 创建项目
- 添加轨道
- 创建/编辑/删除 Mark
- 播放和导航
---
## 十四、附录
### 14.1 命名约定
| 概念 | 变量名 | 类型 |
|------|--------|------|
| 帧 | `frame` | `Frame` (number) |
| 起始帧 | `startFrame` | `Frame` |
| 结束帧 | `endFrame` | `Frame` |
| 总帧数 | `totalFrames` | `Frame` |
| 帧率 | `fps` | `number` |
| 播放头 | `playhead` | `Frame` |
| 时间(秒) | `time` / `sec` | `number` |
### 14.2 配置参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| 默认 FPS | 30 | 无视频时使用 |
| 最小 Mark 宽度 | 1px | 渲染时保证可见 |
| Timeline 高度 | 24px | 每个轨道行高 |
| Playhead 更新频率 | 60fps | requestAnimationFrame |
---
## 十五、版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| 1.0 | 2026-07-24 | 初始设计文档 |
| 1.1 | 2026-07-24 | 添加概念统一说明、Mark 点击交互行为章节 |

View File

@@ -0,0 +1,281 @@
# 视频编辑定位标准研究报告
## 一、行业标准概览
### 1.1 SMPTE Timecode行业标准
**来源**: Society of Motion Picture and Television Engineers (SMPTE)
**标准**: SMPTE 12M (2008年修订为 SMPTE 12M-1, 12M-2, 12M-3)
**核心原则**:
- **Frame-based**: 以帧为单位,格式 `HH:MM:SS:FF`(小时:分钟:秒:帧)
- **整数运算**: 所有帧号都是整数
- **原点对齐**: Frame 0 = Time 0
- **FPS 依赖**: 帧率决定显示时间,但帧数不变
**支持的帧率**:
| FPS | 用途 |
|-----|------|
| 23.98 (24÷1.001) | 北美 HDTV |
| 24 | 电影、ATSC、2K/4K/6K |
| 25 | PAL/SECAM欧洲、澳大利亚|
| 29.97 (30÷1.001) | NTSC北美、日本|
| 30 | ATSC |
**Drop-Frame vs Non-Drop-Frame**:
- **Drop-Frame (DF)**: 每 minute 跳过 frame 0,1除第10分钟用于补偿 29.97fps
- **Non-Drop-Frame (NDF)**: 连续帧号
- 表示法: DF 用分号 `HH;MM;SS;FF`NDF 用冒号 `HH:MM:SS:FF`
---
### 1.2 EDL (Edit Decision List)
**用途**: 剪辑决策列表,用于记录剪辑点
**简单 EDL 格式**:
**Time-based**:
```
[begin second] [end second] [action]
5.3 7.1 0 # cut from 5.3s to 7.1s
15 16.7 1 # mute from 15s to 16.7s
```
**Frame-based**:
```
#[begin frame] #[end frame] [action]
#127 #170 0 # cut from frame 127 to 170
#360 #400 1 # mute from frame 360 to 400
```
**关键点**:
- 支持两种定位方式time浮点和 frame整数
- Action 类型0=cut, 1=mute, 2=scene marker, 3=skip
- **推荐使用 Frame-based** 以保证精度
---
### 1.3 AAF (Advanced Authoring Format)
**来源**: Advanced Media Workflow Association (AMWA)
**标准化**: SMPTE
**特点**:
- 专业级跨平台数据交换格式
- 包含 Essence Data音频、视频等和 Metadata
- 支持复杂的对象关系描述
- 追踪从源文件到最终产出的完整历史
- 用于"进行中的作品"works in progress
**与 MXF 的关系**:
- AAF 用于编辑中的项目
- MXF 用于交换完成的媒体产品
- MXF 是 AAF 数据模型的子集
---
### 1.4 OpenTimelineIO
**来源**: Pixar 开发,现由 Academy Software Foundation 维护
**状态**: 成熟框架,广泛应用于影视行业
**核心设计**:
- **OpenTime**: 无依赖的时间处理库
- **数据模型**: Timeline → Track → Clip → Media Reference
- **帧与时间分离**:
- `RationalTime`: value + rate精确的时间表示
- `TimeRange`: start_time + duration
- 帧是整数,时间是 RationalTime
**示例**:
```python
import opentimelineio as otio
timeline = otio.adapters.read_from_file("project.aaf")
for clip in timeline.find_clips():
print(clip.name, clip.duration())
# duration() 返回 RationalTime(value, rate)
```
**支持的格式**:
- Final Cut Pro XML
- AAF
- CMX 3600 EDL
- 原生 `.otio`, `.otioz`, `.otiod`
---
## 二、主流软件对比
### 2.1 Adobe Premiere Pro
**定位方式**:
- 使用 SMPTE Timecode
- 支持 Drop-Frame 和 Non-Drop-Frame
- 内部用帧号定位,显示用时间码
- 导出格式EDL, XML, AAF
### 2.2 Final Cut Pro
**定位方式**:
- 使用 Apple 时间码格式
- Frame-based 内部处理
- 导出格式FCP XML专有格式
- 时间以 frame + fps 存储
### 2.3 DaVinci Resolve
**定位方式**:
- 支持多种时间码格式
- 项目设置选择 FPS全局
- Timeline 使用 frame 定位
- 支持 EDL, AAF, OTIO 导出
### 2.4 Avid Media Composer
**定位方式**:
- 专业级 SMPTE Timecode 实现
- 支持 Drop-Frame 计数
- 轨道级时间码管理
- AAF 原生支持
---
## 三、核心设计原则总结
### 3.1 行业共识
| 原则 | 说明 |
|------|------|
| **Frame 是唯一标准** | 所有定位、剪辑、同步都用帧号 |
| **整数运算** | 避免浮点精度问题 |
| **FPS 是元数据** | 不改变帧数,只改变显示时间 |
| **原点对齐** | Frame 0 = Time 0 |
| **Time 仅用于显示** | Frame ÷ FPS = Time |
### 3.2 数据流架构
```
┌─────────────────────────────────────────────┐
│ Source Data (各种格式) │
│ - Video files (frame-based metadata) │
│ - Audio files (time-based or sample-based) │
│ - Analysis results (various formats) │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 统一转换层 (标准化) │
│ - Time → Frame (用 FPS) │
│ - Sample → Frame (用 sample_rate) │
│ - 所有数据统一为 Frame │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 内部数据模型 (Frame-based) │
│ - Mark.startFrame (integer) │
│ - Mark.endFrame (integer) │
│ - Clip.timelineStartFrame (integer) │
│ - Project.fps (metadata) │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Timeline 展示层 │
│ - 位置frame / totalFrames * 100% │
│ - 显示frame / fps → "MM:SS" │
│ - 所有轨道对齐 │
└─────────────────────────────────────────────┘
```
---
## 四、对 Momentry Studio 的启示
### 4.1 遵循行业标准
**推荐做法**:
1. ✅ 采用 Frame 作为唯一定位标准
2. ✅ 所有 Frame 操作使用整数
3. ✅ FPS 作为项目元数据
4. ✅ Time 仅用于显示转换
5. ✅ 原点对齐Frame 0 = Time 0
### 4.2 数据格式建议
```typescript
// Mark 定义(符合行业标准)
interface Mark {
id: string
startFrame: number // integer, SMPTE-style
endFrame?: number // integer
tag: string
note?: string
}
// Clip 定义(类似 OTIO
interface Clip {
sourceStartFrame: number // integer
sourceEndFrame: number // integer
timelineStartFrame: number // integer
timelineEndFrame: number // integer
}
// Project 定义
interface Project {
fps: number // 元数据
totalFrames: number // integer
duration: number // 仅用于显示,计算得出
}
```
### 4.3 导出格式支持
**优先级**:
1. **P0 - 内部格式**: 自定义 JSON/SQLiteFrame-based
2. **P1 - EDL**: 简单 EDLFrame-based
3. **P2 - OTIO**: OpenTimelineIO 格式(行业标准)
4. **P3 - AAF/FCPXML**: 专业软件互操作
---
## 五、参考资料
### 5.1 标准文档
- [SMPTE ST 12-1:2008](https://ieeexplore.ieee.org/document/7289820/) - Time and Control Code
- [CMX 3600 EDL Specification](http://xmil.biz/EDL-X/CMX3600.pdf)
- [OpenTimelineIO Documentation](https://opentimelineio.readthedocs.io/)
### 5.2 相关 Wikipedia 文章
- [SMPTE timecode](https://en.wikipedia.org/wiki/SMPTE_timecode)
- [Edit decision list](https://en.wikipedia.org/wiki/Edit_decision_list)
- [Advanced Authoring Format](https://en.wikipedia.org/wiki/Advanced_Authoring_Format)
### 5.3 开源项目
- [OpenTimelineIO](https://github.com/AcademySoftwareFoundation/OpenTimelineIO) - Pixar 开源
- [OpenTimelineIO Plugins](https://github.com/OpenTimelineIO) - 各种适配器
---
## 六、结论
视频编辑行业的定位标准明确且统一:
1. **Frame 是唯一标准** - 所有专业软件和行业标准都基于帧号
2. **整数运算** - 避免浮点精度问题
3. **FPS 作为元数据** - 不改变帧数,只影响显示时间
4. **OpenTimelineIO 是最佳参考** - 现代、开源、行业标准
Momentry Studio 的设计完全符合行业标准:
- ✅ Frame-based 定位
- ✅ 整数运算
- ✅ FPS 作为元数据
- ✅ 原点对齐
- ✅ 多轨同步
可以继续按照现有设计文档推进实现。