跳转到主要内容

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_idJSON body否非空→ShotForUser(shots JOIN tasks 属主)查无 404 shot not found
promptJSON body否缺省取 shot.VideoPrompt
standaloneJSON body否① 形态字段
ratioJSON body否①:firstNonEmpty(p.Ratio,"16:9") 忽略 shot.ratio;⑩:firstNonEmpty(p.Ratio, shotRatio(shot), "16:9")(冻结 null 传 null——PRD 差异②已登记)
durationJSON body仅⑩int64;0→shot.Duration→仍 0→5→clamp [1,15]
resolutionJSON 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 不存在/非本人404shot 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_idJSON 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 非本人404shot 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 边界与异常
条件系统行为提示/码
不存在/非本人404not 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 输入与校验
字段来源必填校验
textJSON body(POST)是TrimSpace 空 → 400 请输入配音旁白文本
voiceJSON 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请输入配音旁白文本
视频非本人404video 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-presets503数据库未配置(DIF-29-20)
降级模式调 faq200 正常—
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);非数字回退默认(冻结 +getSetting NaN 继续传播——billing.go:49-50 登记差异)。
  • 模型三链 modelByPurpose:settings active_model_<p> → model_endpoints 行(purpose+enabled=1)→ KV <p>_model → 中文缺省「星芒文本/图像/视频引擎」;image_fallback 无 active 链仅 KV image_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 /uploadmultipart file+form ratio/promptdata==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 超时「图片生成超时」;非 200 HTTP <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_at ISO 带 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 /audiosquery task_id无 task_id→全量 LIMIT 100
POST /audiosmultipart file+form name/task_iddata==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 落 default image/svg+xml(功能面 URL 直读不受影响,wire 头形差异 DIF-29-18)。

M3 端点覆盖对账

#端点功能节
1POST /api/videos/generateF3.01
2GET /api/videosF3.02
3POST /api/videos/tagF3.03
4POST /api/videos/generate-v2F3.01
5POST /api/videos/:id/cancelF3.04
6POST /api/videos/:id/voiceoverF3.05
7GET /api/videos/:id/voiceoverF3.05
8GET /api/standalone-videosF3.02
9GET /api/models/activeF3.07
10GET /api/imagesF3.08
11POST /api/images/uploadF3.08
12POST /api/images/generateF3.08
13GET /api/images/:idF3.08
14DELETE /api/images/:idF3.08
15GET /api/audiosF3.09
16POST /api/audiosF3.09
17DELETE /api/audios/:idF3.09
18GET /api/style-presetsF3.06
19GET /api/support/faqF3.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 裁决范围收窄注记引用)。