工程剪辑与合成(Synthesis)
本页聚焦于 VideoSynthesis.vue 的 6 步合成流程,提供完整时序图与接口字段字典,覆盖素材上传/注册、时间线构建与工程创建、提交剪辑、任务轮询、预签名下载、保存分镜与下载。
6步流程与时序图
mermaid
sequenceDiagram
participant SB as StoryBoard.vue
participant VS as VideoSynthesis.vue
participant ICE as ICE API
participant OSS as Aliyun OSS
participant API as Backend API
SB->>VS: startSynthesis(type)
VS->>VS: sanitizeUrl & 过滤空thumb
alt 采用OSS Key
VS->>API: fetchRegisterMediaInfo(mediaType=video,inputURL=oss://bucket/key)
API-->>VS: MediaId(v)
VS->>API: fetchRegisterMediaInfo(mediaType=audio,inputURL=oss://bucket/key)
API-->>VS: MediaId(a)
else URL/Blob音频
VS->>API: fetchGetDirectUploadUrl(bucket,key,expires)
API-->>VS: uploadUrl
VS->>OSS: PUT audio Blob
VS->>API: fetchGetPresignUrl(bucket,key)
API-->>VS: presign_url
end
VS->>API: fetchUploadMediaByUrl(data[])
API-->>VS: videoMediaId / audioMediaId
alt 单条合成(type=0)
VS->>ICE: fetchCreateEditingProject(title,coverURL,timeline)
ICE-->>VS: ProjectId
else 合并合成(type=1)
VS->>VS: convertStoryBoardDataToTimeline
VS->>ICE: fetchCreateEditingProject(...)
ICE-->>VS: ProjectId
end
VS->>ICE: fetchSubmitMediaProducing(projectId, outputMediaConfig)
ICE-->>VS: JobId
loop 3-5秒轮询
VS->>ICE: fetchGetMediaProducingJob(jobId)
ICE-->>VS: Status/MediaURL
end
VS->>API: fetchGetPresignUrl(bucket,key)
API-->>VS: presign_url
VS->>API: fetchIceSave(item)
API-->>VS: returnCode=0
VS->>VS: downloadFile(iceVideoUrl)接口字段字典
fetchRegisterMediaInfo
- 描述:注册媒体信息到 ICE,优先使用
oss://bucket/key格式 - 请求字段:
mediaType:video|audioinputURL:oss://bucket/key或http(s)(前端统一规范化)title:可选,媒体标题
- 响应字段:
MediaId:字符串,ICE媒体ID- 兼容返回(已存在):
existed: 1
fetchUploadMediaByUrl
- 描述:将 URL 指向的素材注册/上传到 ICE
- 请求字段(包裹在
data数组内的每条素材):description、line、name、effect:文案相关videoUrl:视频URL(可能追加&extension=extension.mp4)audioUrl:音频URLthumbUrl:缩略图URL
- 响应字段(数组):
videoJobId,videoMediaIdaudioJobId,audioMediaId
fetchCreateEditingProject
- 描述:创建剪辑工程
- 请求字段:
title:工程标题coverURL:封面图URLtimeline:包含VideoTracks、AudioTracks、SubtitleTracks的时间线对象
- 响应字段:
ProjectId:工程IDStatus:状态码(整数)
fetchSubmitMediaProducing
- 描述:提交工程剪辑并输出到特定OSS文件
- 请求字段:
projectId:工程IDoutputMediaTarget:oss-objectoutputMediaConfig.MediaURL:https://bucket.oss-region.aliyuncs.com/path/file.mp4
- 响应字段:
JobId,MediaId,ProjectId,RequestId
fetchGetMediaProducingJob
- 描述:查询剪辑任务状态
- 请求字段:
jobId:任务ID
- 响应字段:
MediaProducingJob对象Status:Success|Failed|RunningMediaURL:最终OSS地址- 其他:
Duration、Timeline、CreateTime、CompleteTime等
fetchGetPresignUrl
- 描述:获取OSS文件的临时下载地址(预签名)
- 请求字段:
bucket:OSS 桶key:对象键名
- 响应字段:
presign_url:可下载的临时URL
fetchIceSave
- 描述:保存单帧分镜结果(工程与输出绑定)
- 请求字段(示例):
title,projectId,jobId,iceVideoUrl,bucket,key
- 响应字段:
responseHeader.returnCode:0 表示成功
fetchGetDirectUploadUrl
- 描述:获取直传地址,用于音频 Blob 上传
- 请求字段:
bucket,key,expires(秒),region(可缺省) - 响应字段:
uploadUrl,key
fetchListOSSFiles
- 描述:列出OSS文件/目录
- 请求字段:
bucket,prefix,delimiter,maxKeys - 响应字段:对象列表
示例
上传/注册示例(请求)
json
{
"data": [{
"title": "未来城市空中交通",
"description": "清晨的未来科技城市...",
"line": "欢迎来到2150年的新都市...",
"videoUrl": "https://img.stooland.com/...14908.mp4",
"audioUrl": "https://gsv.ai-lab.top/outputs/61dc5b...e.wav",
"thumbUrl": "https://img.stooland.com/...thumb.webp"
}]
}提交剪辑(请求)
json
{
"projectId": "75573520d5294d87b6310f76bb32cd4d",
"outputMediaTarget": "oss-object",
"outputMediaConfig": {
"MediaURL": "https://aigc-sz-linkt.oss-cn-shenzhen.aliyuncs.com/ice-output/ICE自动剪辑2025-05-11-12-00.mp4"
}
}查询任务(响应)
json
{
"MediaProducingJob": {
"Status": "Success",
"MediaURL": "https://aigc-sz-linkt.../ICE自动剪辑2025-05-11-12-00.mp4"
}
}注意事项
fetchGetPresignUrl后端默认region为cn-shenzhen,前端可不传。- ICE 媒体已存在时后端兼容返回
MediaId与existed: 1,前端直接使用即可。 - 轮询间隔建议 3–5 秒,避免过度请求;失败时应停止并提示。
FFmpeg 合成(MCP + PyBridge)
方案与选型
- 方案概述
- 前端
FFmpegComposeDialog.vue构建timeline_json与options,调用后端 MCP 工具compose_timeline_ffmpeg。 - MCP 控制器将请求转发到 PyBridge(
PYBRIDGE_URL),由 ffmpeg-python 适配器执行合成。 - 合成进度通过
/mcp/jobs/:id/log尾部日志解析;完成后/mcp/jobs/:id返回status与media_url。 - 若
media_url为 OSS 域名,前端自动走fetchGetPresignUrl生成预签名,保证私有桶可播放。
- 前端
- 技术选型
- 合成引擎:ffmpeg-python(便于计划生成与日志结构化输出)。
- 字幕模式:统一使用整片 SRT(
overlay_mode='subtitles'),禁用逐行 drawtext,提高稳定性与一致性。 - 视频输出:强制 CFR 与
yuv420p像素格式,提升兼容性并减少闪烁。 - URL 签名:后端统一提供 OSS 预签名接口;前端检测后自动替换。
时间线与参数定义
- Timeline 对象(前端)
json
{
"storyboard_id": 123,
"videos": ["https://.../v1.mp4", "https://.../v2.mp4"],
"segments": [
{ "videoUrl": "https://.../v1.mp4", "audioUrl": "https://.../a1.mp3", "subtitleText": "第一段字幕" },
{ "videoUrl": "https://.../v2.mp4", "audioUrl": "https://.../a2.mp3", "subtitleText": "第二段字幕" }
],
"srtText": "1\\n00:00:00,000 --> 00:00:02,000\\n第一段字幕\\n...",
"subtitleSegments": [
{ "text": "第一段字幕", "start": 0.00, "duration": 2.00 },
{ "text": "第二段字幕", "start": 2.00, "duration": 2.00 }
]
}- Options(前端)
json
{
"impl": "ffmpeg-python",
"resolution": "auto",
"fps": 30,
"keep_ar": true,
"video_match_audio_speed": true,
"frame_interpolate": true,
"overlay_mode": "subtitles",
"use_audio_subtitles": true,
"subtitle_chunk_seconds": 2,
"vsync_mode": "cfr",
"pix_fmt": "yuv420p",
"only_subtitles_plan": false
}- 优先级与兜底
- 若存在
subtitleSegments,直接使用; - 若仅有
srtText,前端解析为subtitleSegments; only_subtitles_plan或存在字幕时,强制overlay_mode='subtitles'。
- 若存在
后端接口定义
- 触发合成(MCP 工具)
- 路径:
POST /mcp/tools/call - 请求:
- 路径:
json
{ "tool": "compose_timeline_ffmpeg", "args": { "agent_id": 1, "agent_app_id": 1, "member_id": 1, "storyboard_id": 123, "timeline_json": { }, "options": { } } }- 响应:
json
{ "accepted": true, "tool": "compose_timeline_ffmpeg", "job_id": "mcp_20251215121855_6343" }- 任务状态
- 路径:
GET /mcp/jobs/:id - 响应(示例):
- 路径:
json
{ "job_id": "mcp_...", "status": "success", "media_url": "https://aigc-sz-linkt.oss-cn-shenzhen.aliyuncs.com/ai/workflows/videos/20251215/mcp_....mp4" }- 任务日志
- 路径:
GET /mcp/jobs/:id/log - 响应:
- 路径:
json
{ "id": "mcp_...", "log": "stage:drawtext:start\\n...\\nsubprocess ffmpeg finished successfully" }- 预签名地址
- 路径:
POST /aigc/admin/ali/ice/getPresignUrl - 请求:
{ "bucket": "aigc-sz-linkt", "key": "ai/workflows/videos/20251215/mcp_....mp4" } - 响应:
{ "presign_url": "https://...&x-oss-signature=..." }
- 路径:
前端方法定义(FFmpegComposeDialog.vue)
- 关键方法
start():构建 payload 并触发合成;必要时将srtText解析为subtitleSegments。poll():每 2 秒轮询一次状态与日志;解析进度;完成后停止。tryPresignMediaUrl():当media_url为 OSS 域名时,自动调用预签名接口替换为可播放地址。generateSubtitlesVolc():通过 MCPgenerate_subtitle_volc生成subtitle_segments与srt_text,统一切换到 SRT 模式。
- 轮询停止条件
- 后端返回
status === 'success' || 'failed' - 日志尾部包含完成标志:
adapter finished successfullysubprocess ffmpeg finished successfullyffmpeg finished successfully- 或阶段行包含
drawtext:plan_written
- 已获得可播放的
mediaUrl(视为成功)
- 后端返回
目前进展
- 已统一字幕模式为 SRT,禁用逐行 drawtext;合成更稳定。
- 已增加轮询兜底:根据日志完成标志或
mediaUrl出现停止轮询。 - 已自动预签名 OSS 域名的
media_url,播放器可直接播放。 - 为避免闪烁:
- 默认开启插帧与变速对齐;
- 强制输出 CFR 与
yuv420p像素格式。
后期插帧(FILM)
方案与约束
- 模型:Google Research FILM(TF Hub
https://tfhub.dev/google/film/1) - 环境:GPU 仅启用;CPU 环境禁用并返回错误提示
- 输入:合成完成的视频
media_url(OSS 预签名或公网 URL) - 输出:提升帧率的视频(保持原时长),保留原音轨
接口与调用
- 工具(MCP)
- 名称:
film_interpolate - 请求:json
{ "tool": "film_interpolate", "args": { "input_url": "https://...mp4", "target_fps": 60 } } - 响应:json
{ "accepted": true, "tool": "film_interpolate", "job_id": "interp_1734250000_1234" }
- 名称:
- 后端(PyBridge)
- 端点:
POST /postprocess/interpolate - 请求:json
{ "input_url": "https://...mp4", "target_fps": 60, "model_handle": "https://tfhub.dev/google/film/1" } - 响应:json
{ "job_id": "interp_1734250000_1234", "accepted": true } - 查询:
GET /jobs/{job_id}与GET /jobs/{job_id}/log(与合成一致)
- 端点:
前端使用(FFmpegComposeDialog.vue)
- 在合成成功且出现
mediaUrl后,点击按钮“插帧(FILM)” - 触发
film_interpolate工具,后续按相同jobId流程轮询直到完成 - 完成后自动上传到 OSS,
/mcp/jobs/:id返回新的media_url(前端仍会预签名)
细节实现
- 帧抽取:
ffmpeg -vsync 0输出为 PNG 序列,避免损失 - 插帧策略:按
target_fps / orig_fps计算times_to_interpolate = ceil(log2(ratio)),递归插入中间帧 - 组装输出:以提升后的实际帧率写回视频;映射原音轨
-map 1:a?,libx264 + yuv420p保持兼容 - 日志标记:
stage:film:start/extract_frames/pair_done:X/assemble_video/success
环境依赖
- Python 包:
tensorflow>=2.9、tensorflow_hub、ffmpeg-python - 系统组件:
ffmpeg(命令行),NVIDIA CUDA/cuDNN 或对应 GPU 加速环境 - macOS(Metal)亦可使用
tensorflow-macos+tensorflow-metal插件(性能依设备而定)
常见问题与建议
- 若视频仍有轻微闪烁:
- 将
fps设置为与源视频一致(如 25/30); - 保持
resolution='auto',减少重采样。
- 将
- 若后端延迟返回
status:- 前端已按日志与
mediaUrl增加兜底;仍异常时请检查 PyBridge 日志。
- 前端已按日志与