FSD M3 · 生成与素材
← 总则 | 附录 → | 上游:PRD §4 M3 实现落点:
backend/internal/domain/generation/(model/service/service_video/service_media/provider/repo)+cmd/server/generation_handlers.go+cmd/server/generation_assembly.go+cmd/server/routes.go:53-77(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:369-451/providers.js/assets.js/routes2.js:40-67,408-429/routes3.js:33-55,379-428仅差异对拍参照。
M3 生成与素材
19 端点全量。本域是计费唯一链路核心域(总则 §2.3):credits.Service.TryDeduct → generation.Service.SmartVideo/Service.Generate(provider 真实通道或 Mock)→ 失败由 generation 内部统一退款(RefundGenError 文案 + ClaimVideoRefund 认领防重),调用方不得重复退款。SSE video 一类事件四状态帧(processing/progress/done/failed,hub.go:21);Mock 资产链(SVG/WAV)为字节级移植、Mock 行为是契约的一部分。公开面 2 条(style-presets/faq),其余 amw。
F3.01 视频生成(generate / generate-v2)
对应 PRD:§4 M3 · POST /api/videos/generate、POST /api/videos/generate-v2 | 状态:实现 | 实现落点:generation_handlers.go:25-61、service_video.go:122-189/263-350 | UI:
/video(VideoView 生成表单)| 场景挂钩:无专门 e2e(E2E-GEN-02 间接前置依赖 standalone 面)
1 功能定义
分镜视频生成双形态:① 固定扁价 50(无 settings 键);⑩ 拉取+每秒动态价(clamp 1-15s)+resolution 白名单+响应多 model 键。Mock 同步返片,真实通道异步+轮询+SSE。
2 触发条件与前置状态
- 生成表单提交 → 两端点(Bearer);关联 shot 时前置:shot 存在且属主(先 404 后 402,service_video.go:123 注释冻结 index.js:374→376 同序)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| shot_id | JSON body | 否 | 非空→ShotForUser(shots JOIN tasks 属主)查无 404 shot not found |
| prompt | JSON body | 否 | 缺省取 shot.VideoPrompt |
| standalone | JSON body | 否 | ① 形态字段 |
| ratio | JSON body | 否 | ①:firstNonEmpty(p.Ratio,"16:9") 忽略 shot.ratio;⑩:firstNonEmpty(p.Ratio, shotRatio(shot), "16:9")(冻结 null 传 null——PRD 差异②已登记) |
| duration | JSON body | 仅⑩ | int64;0→shot.Duration→仍 0→5→clamp [1,15] |
| resolution | JSON body | 仅⑩ | 白名单 720p/1080p,白名单外回 shot.resolution,仍不在回 “1080p”(冻结原样透传 480p——PRD 差异②已登记) |
_ = c.Bind(&req) 忽略解析错误(handlers 层惯例);无显式 400 面。
4 处理流程
- 轮询节奏:首查 8s/步进 10s/上限 90 次/每 3 次 progress 帧(providers.js:164-187 镜像);完成谓词 status∈{succeeded,completed,success,done} 且 url 非空(或 url 非空 status 空,宽容);超时 90 次→「供应商任务超时(15分钟)(任务 X)」。
- 完成链:SetVideoDone+LinkShotVideo(video_done)+通知「视频生成完成」+SSE done;失败链:SetVideoFailed+SetShotStatus(failed)+Refund(ClaimVideoRefund 认领)+通知+SSE failed。
- ⑩ prompt 尾追
[resolution](service_video.go:176);standalone 恒 false(routes2.js:420)。
5 输出与结果状态
- Mock:
{id,status:"done",url,seconds,cost,resolution};⑩ 另多model键且置尾(videoV2MockResult 键序)。 - 真实:
{id,status:"processing",seconds,cost,resolution,note}(url 与 note 互斥:有 note 无 url / 有 url 无 note——service_video.go:89/:99 注释);⑩ 同序多model。 model值=billing 三链(settings active_model_video→model_endpoints→KV video_model→缺省「星芒视频引擎」)。
6 状态流转
videos.status:processing(真实分支插行)→ done(finishVideo)/failed(failVideo)/cancelled(F3.04);DDL 默认 'queued' 代码从不写。shots.status → video_done(LinkShotVideo)/failed。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| shot 不存在/非本人 | 404 | shot not found |
| 余额不足 | 402 | 积分不足 |
| Mock 落盘失败 | videoFailFast 退款 + 500 | 视频生成失败:<cause>(50 积分已退回) |
| 真实提交失败 | SetVideoFailed+SetShotStatus+退款(认领制) | 502 视频生成提交失败:<cause>(<cost> 积分已退回) |
| 轮询耗尽/供应商报失败 | 退款(认领制)+SSE failed+通知「视频生成失败」 | reason 截 80 |
8 权限与数据规则
- 权限:登录态;shot 属主经
JOIN tasks … WHERE t.user_id=?(repo.go:252-254);standalone 无属主对象。 - 数据规则:写
videos/shots/credit_records/notifications;videos.task_id空→wire “"(Go 空;冻结 null——测试断言 TaskID=="")。
F3.02 视频列表(videos / standalone-videos)
对应 PRD:§4 M3 · GET /api/videos、GET /api/standalone-videos | 状态:实现 | 实现落点:generation_handlers.go:64-85、repo.go:178-196 | UI:VideoView 历史列表/独立工具页| 场景挂钩:E2E-NAV-01(读面)
1 功能定义
两个视频列表读:全量(LIMIT 100)与独立生成子集(standalone=1)。
2 触发条件与前置状态
- 列表页/工具页加载 → 两端点(Bearer)。
3 输入与校验
无参数。
4 处理流程
- videos:
SELECT * FROM videos WHERE user_id=? ORDER BY created_at DESC LIMIT 100。 - standalone-videos:同式 +
AND standalone=1(E2E-GEN-02 前置造数时standalone:true必传——e2e 清单 :11)。
5 输出与结果状态
Video 行数组(键形见 F3.01 §6 前数据模型):id/user_id/task_id/shot_id/standalone/prompt/status/file_path/error/attempts/seconds/resolution/refunded/created_at;prompt/error 指针 nil→null;created_at LocalTime "2006-01-02 15:04:05"。
6 状态流转
无(纯读)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无视频 | 200 空数组/null(gorp nil slice) | — |
| 降级模式 | 503 | 数据库未配置 |
quirk:standalone/refunded wire 出 bool true/false(冻结 SQLite SELECT * 透传 1/0 数字——前端类型未声明零消费,DIF-29-17);ORDER BY created_at DESC 无次级键同秒抖动(冻结同构保真)。
8 权限与数据规则
- 权限:登录态;
user_id=?谓词。
F3.03 单场无损打标(videos/tag)
对应 PRD:§4 M3 · POST /api/videos/tag | 状态:实现 | 实现落点:generation_handlers.go:100-113、service_video.go:468-489 | UI:分镜卡「AI 打标」| 场景挂钩:无
1 功能定义
对单镜视频提示词追加一致性标签补丁,零写表(提示词 patch 由前端拿回自行保存);扣动态图片价。PRD :153「10×镜数」系计法误述——冻结/Go 均单镜单次扣(引用勿照抄)。
2 触发条件与前置状态
- 打标按钮 →
POST /api/videos/tag(Bearer);前置 shot 存在且属主。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| shot_id | JSON body | 是 | ShotForUser 查无 404 shot not found |
4 处理流程
TryDeduct(cost=billing.View().ImageCost settings image_generation 默认 10;type=tag;desc 单场无损打标(-10);taskId 传 shot id——冻结 index.js:391 照抄)→ 组装标签:transition_mode=="anchor_prev"→「锚定上一镜」否则「独立转场」。
5 输出与结果状态
{"tags":["主体一致","光影连续","运镜连贯",<tail>],"prompt_patch":"\n[一致性标签] 主体一致 / 光影连续 / 运镜连贯"}——prompt_patch 首字符为换行符(冻结模板字面量逐字)。
6 状态流转
无(零写表,service_video.go:468 注释)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| shot 非本人 | 404 | shot not found |
| 余额不足 | 402 | 积分不足 |
8 权限与数据规则
- 权限:登录态+shot 属主。计费价随 F6 settings(冻结恒 COSTS.image=10——PRD 差异④已登记)。
F3.04 生成取消(videos/:id/cancel)
对应 PRD:§4 M3 · POST /api/videos/:id/cancel | 状态:实现 | 实现落点:generation_handlers.go:88-97、service_video.go:453-465 | UI:VideoView cancelGen(confirm「供应商已产生费用,取消不退积分」→toast「已取消生成(积分不退)」)| 场景挂钩:无
1 功能定义
取消生成中的视频:置 cancelled+固定 error 文案,不退款、不中止轮询(其后 finishVideo/failVideo 仍可覆写终态——service_video.go:453-454 注释登记)。
2 触发条件与前置状态
- 生成卡取消按钮(confirm 前置)→
POST /api/videos/:id/cancel(Bearer)。
3 输入与校验
路径 :id;VideoByID(WHERE id=? AND user_id=?)查无 404 not found。
4 处理流程
SetVideoCancelled:UPDATE videos SET status='cancelled', error='用户取消(供应商已产生费用,积分不退)' WHERE id=?——不动 refunded(CancelErrorText model.go:93 冻结 routes2.js:428 逐字)。
5 输出与结果状态
{"cancelled":true,"refund":0}。
6 状态流转
processing → cancelled(但非终态锁——轮询 goroutine 仍可覆写 done/failed,登记于代码注释)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 不存在/非本人 | 404 | not found |
| 已终态再取消 | 仍 200 覆写 cancelled | — |
8 权限与数据规则
- 权限:登录态+属主;只写自己一行。
F3.05 视频配音(voiceover)
对应 PRD:§4 M3 · POST /api/videos/:id/voiceover、GET /api/videos/:id/voiceover | 状态:实现 | 实现落点:generation_handlers.go:252-278、service_media.go:155-184、repo.go:409-431 | UI:VideoView 配音面板| 场景挂钩:无
1 功能定义
Mock TTS:文本合成演示 WAV(零扣费),写 voiceovers 表;列表读最近 5 条。
2 触发条件与前置状态
- 配音面板提交/加载 → 两端点(Bearer);前置视频存在且属主。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| text | JSON body(POST) | 是 | TrimSpace 空 → 400 请输入配音旁白文本 |
| voice | JSON body(POST) | 否 | 白名单 female/male/child/narrator,白名单外静默回退 narrator |
4 处理流程
VideoByID 404 video not found → MakeVoiceoverWav(sr=16000,dur=min(4, 0.8+runeCount×0.02);base 频率 female 523/male 330/child 659/narrator 392)→ 落盘 tts_<uuid>.wav → InsertVoiceover 六列(id vo_ 前缀,status 走 DDL 默认 done)。列表:SELECT * FROM voiceovers WHERE video_id=? AND user_id=? ORDER BY created_at DESC LIMIT 5(查无返空数组无 404 面,冻结同)。
5 输出与结果状态
POST → {"id","url","voice","note":"演示 TTS 音(正式版接入真实配音模型)"}(冻结 routes3.js:412 逐字);GET → Voiceover 数组。
6 状态流转
voiceovers.status 恒 done(INSERT 不写该列)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 文本空 | 400 | 请输入配音旁白文本 |
| 视频非本人 | 404 | video not found |
| voice 非法值 | 静默 narrator | — |
8 权限与数据规则
- 权限:登录态+视频属主。
- 数据规则:WAV 时长用 rune 计数(JS
.length是 UTF-16 单元——astral 字符差异登记不设防,mockassets.go:93-94)。
F3.06 风格预设与 FAQ(style-presets / support/faq)
对应 PRD:§4 M3 · GET /api/style-presets、GET /api/support/faq(公开面,无 amw)| 状态:实现 | 实现落点:generation_handlers.go:193-202/281-283、service_media.go:194-207 | UI:风格选择器/FAQ 页| 场景挂钩:E2E-SNAP-01(间接)
1 功能定义
两个公开只读面:19 项风格目录(thumb SVG 幂等落盘)与 6 条静态 FAQ。FAQ 无 requireDB(唯一例外,冻结同);style-presets 有(降级 503——冻结零 DB 依赖恒 200,DIF-29-20)。
2 触发条件与前置状态
- 未登录可访问(pub 组,routes.go:58-59)。
3 输入与校验
无参数。
4 处理流程
- style-presets:遍历 StyleCatalog 19 项(cyberpunk→custom,顺序即响应序,与冻结 routes3.js:33-51 逐字一致)→ MakeStyleThumb 每次覆盖写
style_<id>.svg(冻结 existsSync 存在复用——确定性内容结果等价,mockassets.go:172 登记差异)。 - faq:静态 6 条
{q,a}(含「图片 10 积分/张;视频 = 拉取 5 + 每秒 5 积分」计费说明——冻结原文照抄,与 F6 覆盖价可能漂移系冻结固有)。
5 输出与结果状态
style-presets → [{id,name,group,thumb}]×19;faq → [{q,a}]×6。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 降级模式调 style-presets | 503 | 数据库未配置(DIF-29-20) |
| 降级模式调 faq | 200 正常 | — |
| thumb 落盘失败 | 500 | — |
8 权限与数据规则
- 权限:公开(无 amw 无 requireDB——faq);写入面仅文件系统(style SVG)。
F3.07 计价投影(models/active)
对应 PRD:§4 M3 · GET /api/models/active(v1.3 W10 补登归属 M3——「⑲ getBilling 投影」)| 状态:实现 | 实现落点:generation_handlers.go:286-291、generation_assembly.go:31-62、service_video.go:62-76、credits/billing.go:64-103 | UI:VideoView/DashboardView 计价展示(消费方表述两份文档不一致:PRD :155 称 DashboardView、API-增量.md:65 称 VideoView——B 类漂移登记,留勘误)| 场景挂钩:无
1 功能定义
当前计费配置投影:价格四键(settings 可调)+ 模型三链解析结果——前端计价展示与费用估算的唯一数据源。
2 触发条件与前置状态
- 前端挂载拉取 →
GET /api/models/active(Bearer)。
3 输入与校验
无参数。
4 处理流程
- 价格四键:settings
image_generation/video_pull/video_per_second/text_tokens_per_credit(默认 10/5/5/1000);非数字回退默认(冻结+getSettingNaN 继续传播——billing.go:49-50 登记差异)。 - 模型三链
modelByPurpose:settingsactive_model_<p>→ model_endpoints 行(purpose+enabled=1)→ KV<p>_model→ 中文缺省「星芒文本/图像/视频引擎」;image_fallback无 active 链仅 KVimage_model_fallback→「星芒图像引擎 · 备选」。 - settings 表
key为 MySQL 保留字须反引号(task_assembly.go:33-35 注释载十五续修复的存量 bug)。
5 输出与结果状态
{"image_cost":N,"video_pull":N,"video_per_second":N,"text_tokens_per_credit":N,"models":{"text","image_primary","image_fallback","video"}}(BillingView 键序)。
6 状态流转
无(读投影)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 降级模式 | 503 | 数据库未配置 |
| settings 值非数字 | 回退默认(冻结 NaN 传播——登记差异) | — |
8 权限与数据规则
- 权限:登录态(冻结 authRequired routes2.js:67 同)。
F3.08 图片素材(images 五端点)
对应 PRD:§4 M3 · GET /api/images、POST /api/images/upload、POST /api/images/generate、GET /api/images/:id、DELETE /api/images/:id | 状态:实现 | 实现落点:generation_handlers.go:116-190、service.go:83-141、service_media.go:23-92、repo.go:19-46/306-369 | UI:
/tools(IndependentToolsView)| 场景挂钩:E2E-GEN-01(generate Mock 同步)、E2E-CRD-01(台账「独立图片生成(-10)」)
1 功能定义
独立图片素材五动作:standalone 列表、上传(72h 过期)、生图(动态价+可选参考图)、单张重定向读(query token 兜底两处之一)、删除(恒 ok)。
2 触发条件与前置状态
- 工具页操作 → 五端点(Bearer);
GET /images/:id另支持 query token(C3:FromRequest Bearer 优先+query 兜底;冻结 index.js:426-427 query 优先且路由级无 authRequired——差异已登记 routes.go:72)。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| GET /images | — | 无 |
| POST /upload | multipart file+form ratio/prompt | data==nil → 400 缺少文件(冻结 index.js:405) |
| POST /generate | {"prompt","ratio","ref_image_ids":[]} | TrimSpace(prompt) 空 → 400 请输入图片提示词(先校验后扣费,冻结同序) |
| GET /:id | — | 查无 404 not found |
| DELETE /:id | — | 0 行也 200(恒 ok,repo.go:102 注释) |
4 处理流程
- 真实通道
provider.GenerateImage(provider.go:129-180):POST/images/generations(model/prompt 截 4000/n:1/size 按 ratio 9:16→1024x1536、1:1→1024x1024、默认 1536x1024);180s 超时「图片生成超时」;非 200HTTP <code>:<msg截200>;data 空「接口未返回图片数据,请核对模型名」;b64 解码 ext=.png、url 下载 ext 由扩展捕获组不归一化;两者皆无「图片接口返回格式未知(无 url/b64_json)」——失败原样上抛。 - upload:ext 空补 .png;落盘
up_文件名/img_行 id 怪形态(冻结 saveUpload 保留);origin_name 过 FixFilename;prompt 缺省上传原图 <名>。 - GET /:id:
SELECT file_path WHERE id=? AND user_id=?→ 302 重定向(冻结 res.redirect 默认 302 同)。
5 输出与结果状态
- 列表 → StandaloneImage 数组(
expires_atISO 带 T/Z 或 null——C7 例外全达成,PRD 差异①)。 - upload →
{"id","url"}(无 expires_at 键);generate →{"id","url","expires_at"};delete → data true。
6 状态流转
无独立状态机;images 行插入即终态(expires_at 72h 后逻辑过期,无清理任务)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 缺文件 | 400 | 缺少文件 |
| prompt 空 | 400 | 请输入图片提示词 |
| 余额不足 | 402 | 积分不足 |
| 生成失败 | 退款(claim=nil 无防重面——images 同步路径照冻结) | 502 图片生成失败:<cause>(<cost> 积分已退回) |
| 删不存在 | 200 true | — |
| ref 追加失败 | _ = 吞错(冻结同语句抛错进中央 500——DIF-29-21) | — |
8 权限与数据规则
- 权限:登录态;读删谓词
user_id=?;upload/generate 无跨表属主。 - 数据规则:写
images/credit_records;up20 为请求体总量限额(冻结 multer 单文件 20MB——M9 节同族差异引用不重立)。
F3.09 音频素材(audios 三端点)
对应 PRD:§4 M3 · GET /api/audios、POST /api/audios、DELETE /api/audios/:id | 状态:实现 | 实现落点:generation_handlers.go:205-249、service_media.go:112-139、repo.go:371-407 | UI:VideoView 配音素材区/独立工具页| 场景挂钩:无
1 功能定义
音频素材三动作:列表(按 task 过滤双分支)、上传(白名单扩展名)、删除(恒 ok)。全部零扣费。
2 触发条件与前置状态
- 素材区操作 → 三端点(Bearer);上传限额 50MB(up50)。
3 输入与校验
| 端点 | 输入 | 校验 |
|---|---|---|
| GET /audios | query task_id | 无 task_id→全量 LIMIT 100 |
| POST /audios | multipart file+form name/task_id | data==nil → 400 缺少音频文件;ext(小写)不在 .mp3/.wav/.m4a/.aac/.ogg/.flac → 400 仅支持 mp3/wav/m4a/aac/ogg/flac(先白名单后落盘,冻结同序) |
| DELETE /audios/:id | — | 恒 ok |
4 处理流程
上传:Upload 落盘(up_ 文件名形态)→ InsertAudio(id au_ 前缀——routes3.js:391,P0 误写 aud_ 已勘误)→ name=truncate(firstNonEmpty(name,fileName),60);task_id 空串落 ''(冻结落 NULL——DDL NOT NULL 适配,repo.go:113 注记)。列表双分支:有 task_id→WHERE user_id=? AND (task_id=? OR task_id='') 无 LIMIT(冻结 task_id IS NULL 适配);无→LIMIT 100。
5 输出与结果状态
列表 → Audio 数组(id/user_id/task_id/name/file_path/size/created_at);上传 → {"id","url"};删除 → data true。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 缺音频文件 | 400 | 缺少音频文件 |
| 扩展名非法 | 400 | 仅支持 mp3/wav/m4a/aac/ogg/flac |
| 删不存在 | 200 true | — |
8 权限与数据规则
- 权限:登录态;谓词
user_id=?。 - 数据规则:
contentTypeOf缺.mp3/.m4a/.../.mp4分支——音频上传 Content-Type 落 defaultimage/svg+xml(功能面 URL 直读不受影响,wire 头形差异 DIF-29-18)。
M3 端点覆盖对账
| # | 端点 | 功能节 |
|---|---|---|
| 1 | POST /api/videos/generate | F3.01 |
| 2 | GET /api/videos | F3.02 |
| 3 | POST /api/videos/tag | F3.03 |
| 4 | POST /api/videos/generate-v2 | F3.01 |
| 5 | POST /api/videos/:id/cancel | F3.04 |
| 6 | POST /api/videos/:id/voiceover | F3.05 |
| 7 | GET /api/videos/:id/voiceover | F3.05 |
| 8 | GET /api/standalone-videos | F3.02 |
| 9 | GET /api/models/active | F3.07 |
| 10 | GET /api/images | F3.08 |
| 11 | POST /api/images/upload | F3.08 |
| 12 | POST /api/images/generate | F3.08 |
| 13 | GET /api/images/:id | F3.08 |
| 14 | DELETE /api/images/:id | F3.08 |
| 15 | GET /api/audios | F3.09 |
| 16 | POST /api/audios | F3.09 |
| 17 | DELETE /api/audios/:id | F3.09 |
| 18 | GET /api/style-presets | F3.06 |
| 19 | GET /api/support/faq | F3.06 |
19/19 对账 routes.go:58-77 与 PRD §4 M3。已登记差异引用:PRD 差异①(expires_at C7)②(resolution/ratio 收紧向)③(SSE progress 时序 fetch 后)④(tag 动态价);POST /images/storyboard/generate 归 M2(PRD :150)。
DIF 新发现(本期取证,入 99-附录 B):DIF-29-17 videos standalone/refunded wire bool vs 冻结 1/0(前端零消费)/DIF-29-18 contentTypeOf 缺音频/视频扩展分支(Content-Type 落 svg)/DIF-29-19 Mock 落盘文件名形态(label+ObjectKey vs 冻结 img_<uid> 行 id 即文件名)/DIF-29-20 style-presets 降级 503(冻结零 DB 依赖)/DIF-29-21 ref 追加错误吞没(冻结进中央 500)/DIF-29-22 真实通道 Link 失败 failFast 退款(冻结 try 外不退——integration_test.go:320-323 裁决范围收窄注记引用)。