# FSD M3 · 生成与素材

LLMS 索引： [llms.txt](/llms.txt)

---

> [← 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/) ｜ [附录 →](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/) ｜ 上游：[PRD §4 M3](/qmtv/product/prd-qmtv-v1/)
> 实现落点：`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 处理流程

```mermaid
flowchart TD
  A["POST /videos/generate(-v2)"] --> B["ShotForUser 404 先行"]
  B --> C["计费：① CostVideo=50 恒定<br/>⑩ video_pull + per_second×secs"]
  C -->|不足| D["402 积分不足"]
  C --> E["TryDeduct type=video<br/>⑩「视频生成 %ds %s（拉取%d+每秒%d，%s，-%d）」"]
  E --> F["submitVideo"]
  F --> G{"Mock?（api_key 空）"}
  G -->|是| H["MakeVideo SVG 落盘 → InsertVideo done<br/>LinkShotVideo → 同步返回 url"]
  G -->|否| I["InsertVideo processing + LinkShotVideoKeep<br/>SubmitVideo 真实供应商"]
  I -->|SyncURL 同步给片| J["go finishVideo（无 processing 帧）"]
  I -->|异步| K["go pollLoop + SSE processing"]
```

- 轮询节奏：首查 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）；**非数字回退默认**（冻结 `+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 /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 处理流程

```mermaid
flowchart TD
  A["POST /images/generate"] --> B["prompt 非空校验"]
  B --> C["TryDeduct type=image task_id=''<br/>「独立图片生成（-10）」"]
  C -->|不足| D["402"]
  C --> E["Service.Generate：真实通道 GenerateImage<br/>或 Mock MakeImage SVG"]
  E -->|失败| F["Refund claim=nil + 502<br/>「图片生成失败：<cause>（10 积分已退回）」"]
  E --> G["InsertImageStandalone kind=standalone<br/>expires_at=now+72h"]
  G --> H["ref_image_ids 非空→prompt 尾追 ' [参考图N张]'"]
```

- 真实通道 `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 /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 落 default `image/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](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/)）**：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_&lt;uid&gt; 行 id 即文件名）／DIF-29-20 style-presets 降级 503（冻结零 DB 依赖）／DIF-29-21 ref 追加错误吞没（冻结进中央 500）／DIF-29-22 真实通道 Link 失败 failFast 退款（冻结 try 外不退——integration_test.go:320-323 裁决范围收窄注记引用）。

---

反链：

- [QMTV 功能规格说明书（FSD）v1.0 · 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/)
- [FSD 附录 · PRD 对照 / 代码域索引 / 差异台账](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/)
