@starreel/mcp 0.1.106 → 0.1.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -144,6 +144,16 @@ Don't ask the user at every step. Sort work into three tiers:
144
144
  glowing energy, floating landforms and non-human forms as "wrong era",
145
145
  dragging the look toward literal historical drama. A single scene can break
146
146
  away via `update_scene`'s `era_contract` (scene level outranks episode level).
147
+ **When the era check conflicts with a customer-uploaded portrait** (the upload
148
+ itself, or the body / hair / sheet images derived from it), frame generation
149
+ stops with `400 ERA_USER_REF_CONFLICT` — nothing generated, nothing billed. Do
150
+ not retry blindly and do not pass `allow_era_ref_conflict: true` on your own:
151
+ tell the customer which character and image was flagged, and let them choose —
152
+ keep the look (then pass `allow_era_ref_conflict: true` on `generate_shot_frame`
153
+ / `generate_frames`; the images are kept, not dropped), fix the portrait, or fix
154
+ the era contract. `generate_frames` dispatches in the background, so a shot
155
+ blocked this way does not error the batch call — after it, read `get_jobs`:
156
+ `frame_dispatch_rejections` lists every refused shot with its `code` and message.
147
157
  **Props are a separate chain and the era contract does not reach them.** The
148
158
  white-background prop sheet deliberately injects no visual lock and no era
149
159
  contract (long lock prose drowns the style words), so a prop's period rests
@@ -677,6 +687,7 @@ Map the reason to an action:
677
687
  | `transient` | true | BestOfN / quality-gate / rate-limit / network, or the audit itself failed to run | Back off, retry |
678
688
  | `contract_rejected` | true | A **content/contract** rejection (era, composition, shot size, head-count, readable text…) — **not** a network blip. Measured: a blind re-roll clears it about a third of the time | Retry once or twice; if the same reason keeps coming back, read `fail_hint` and change the shot (see *What you can actually change* below) instead of re-rolling |
679
689
  | `needs_content_fix` | false | A content/contract conflict that a re-roll almost never clears (measured ≤10%) — most often the character's wardrobe/accessory record conflicting with the approved portrait | Change the shot description / character record **first**, then regenerate. A blind retry is a full-price repeat |
690
+ | `diagnosis_required` | false | Repeated same-input same-reason failures, or an existing candidate awaits a verdict | Call `get_frame_retry_diagnosis` with the image task ID. Reaudit the existing candidate for audit errors; review evidence for unknown reasons. Do not submit an unchanged paid retry or rewrite the script without authorization. |
680
691
  | `repeat_rejected` | false | Same shot rejected for the same reason until the circuit breaker tripped (auto-clears after 24 h) | Change the prompt / references / shot design **first** (see *What you can actually change* below); a blind retry is a full-price repeat of the same rejection |
681
692
  | `pair_collateral` | true | The **other** frame of this shot failed its audit; this frame was never judged bad — it was only closed out with the batch | Do **not** edit this frame. If the shot carries `reopen_pair_id`, pass it to `generate_shot_frame` to redo only the faulty side; otherwise regenerate the shot |
682
693
  | `unknown` | false | Unclassified | Read `fail_hint`; don't auto-retry |
@@ -1073,10 +1084,17 @@ to close") tells the vendor to fit that entire sequence into each 3-second shot.
1073
1084
  the customer — **every time**, including revisions. Otherwise the platform's
1074
1085
  records describe a cut the customer never received, and nobody can later tell
1075
1086
  which version shipped or which generation each shot came from. The platform
1076
- re-hashes the file itself and snapshots every shot's source clip; put what you
1077
- changed outside in `manifest`. `get_final_cut` then returns it as `delivery`
1078
- next to the platform's own `download_url`; `list_deliveries` /
1079
- `set_current_delivery` manage versions.
1087
+ re-hashes the file itself. When delivery provenance is enabled, pass the original
1088
+ `handoff_pack_id` and `handoff_manifest_sha256`: registration uses that frozen
1089
+ export, never today's shot pointers. Without an export, provenance stays unknown.
1090
+ Keep `source-manifest.json` and `download-receipt.json`; downloaded hashes are
1091
+ client-side evidence, not proof of customer acceptance. Put finishing edits in
1092
+ `manifest`. Registration does not select or accept a version in the new flow.
1093
+ Use `set_current_delivery` for selection and `confirm_delivery_acceptance` only
1094
+ after the customer's explicit statement. For verbal completion without a file,
1095
+ use `delivery_id=null`: the platform reports completion with file unverified.
1096
+ Read `delivery_completion` independently of historical production warnings.
1097
+ `get_final_cut.download_url` remains the platform render URL.
1080
1098
  - **Local color grading / third-party AI finishing**: call `export_handoff_pack`
1081
1099
  with `purpose="local_color"`; for existing still frames use `media_type="images"`
1082
1100
  (no video generation required). Save the result as `manifest.json`, obtain the
@@ -1137,3 +1155,18 @@ Full reference: https://api.shortreelai.com/docs/mcp
1137
1155
  - `ready` 和生成数量只表示产物存在;当前有效数量使用 `get_pipeline_status.asset_versions_summary.*.current_effective`。换帧后旧视频仍可查看,但失效版本不能混入当前有效汇总。
1138
1156
  - `get_asset_versions(storyboard_id)` 查看历史版本链接、依赖 ID/版本哈希及各自状态。历史没有快照时不从当前绑定伪造依赖。
1139
1157
  - `get_asset_binding_repair_plan(episode_id)` 只返回缺失场景绑定的候选和依据,`applied=false`。必须逐项审核,不按名字自动回填,也不自动重新生成客户已完成的片子。
1158
+
1159
+ ### Original product lettering
1160
+
1161
+ Use `update_shot(product_text_contract=...)` only for explicitly chosen assets,
1162
+ source images, normalized source regions, and exact original text. Never infer
1163
+ these bindings from names or backfill old shots. `quote_shot_frame` and
1164
+ `quote_frames` expose the rendering limitation and original-media/postproduction
1165
+ alternatives before generation. Generated text matching the requested text still
1166
+ needs a visual check. `record_product_text_review` may record a result only after
1167
+ the user explicitly confirms they inspected that exact file. Bind the current
1168
+ contract hash, generation ID and URL from `get_storyboards.product_text`;
1169
+ model output is not human acceptance. This does not accept the entire film.
1170
+
1171
+ ### 费用归因
1172
+ 核对任务、素材版本、采用情况和返工原因使用 `get_cost_attribution`。实际花费是账本净额,未知证据不猜测;未采用不等于浪费,图片修改也可能产生费用。明确原因后用 `classify_cost_reason` 登记说明,不会改变账单。旧 `rework.video` 是重复成功出片计数,不是平台失败归责。
@@ -204,7 +204,7 @@ def compile_timeline(pack_dir, plan_path=None, outdir=None):
204
204
  total = cursor
205
205
  by_n = {s["shot_number"]: s for s in shots}
206
206
  bgm = []
207
- for b in manifest.get("bgm", []):
207
+ for b in ([] if (manifest.get("audio_contract") or {}).get("music_output") == "forbidden" else manifest.get("bgm", [])):
208
208
  anchor = by_n.get(int(b.get("anchor_shot", 1)))
209
209
  if not anchor:
210
210
  warnings.append(f"BGM {b.get('file')}: anchor_shot 不存在,按 0 处理")
@@ -156,11 +156,14 @@ def main():
156
156
  if local_color and os.path.exists(out) and os.listdir(out):
157
157
  sys.exit("本地后期请使用新的空目录,避免覆盖源素材或误用旧版本缓存")
158
158
  os.makedirs(out, exist_ok=True)
159
- if local_color:
159
+ if local_color or m.get("pack_id"):
160
160
  with open(os.path.join(out, "source-manifest.json"), "w", encoding="utf-8") as f:
161
161
  json.dump(m, f, ensure_ascii=False, indent=2)
162
+ if local_color:
162
163
  # 再次兜底:本地后期不自动烘焙项目 LUT。
163
164
  m.setdefault("render_target", {})["color_lut"] = None
165
+ expected_hashes = {}
166
+ receipts = []
164
167
  jobs = [] # (url, 包内相对路径)
165
168
 
166
169
  for i, sh in enumerate(m.get("shots", [])):
@@ -178,6 +181,7 @@ def main():
178
181
  clip = sh.get("clip") or {}
179
182
  if clip.get("url"):
180
183
  rel = f"clips/shot_{tag}.mp4"
184
+ expected_hashes[rel] = (sh.get("provenance") or {}).get("generation_sha256")
181
185
  jobs.append((clip.pop("url"), rel))
182
186
  clip["file"] = rel
183
187
 
@@ -225,13 +229,27 @@ def main():
225
229
  def one(job):
226
230
  url, rel = job
227
231
  try:
228
- fetch(url, os.path.join(out, rel))
232
+ dest = os.path.join(out, rel)
233
+ fetch(url, dest)
234
+ digest = hashlib.sha256()
235
+ with open(dest, "rb") as downloaded:
236
+ for chunk in iter(lambda: downloaded.read(1024 * 1024), b""):
237
+ digest.update(chunk)
238
+ sha = digest.hexdigest()
239
+ expected = expected_hashes.get(rel)
240
+ if expected and expected != sha:
241
+ os.unlink(dest)
242
+ raise RuntimeError("downloaded file SHA-256 differs from exported version")
243
+ receipts.append({"file": rel, "source_url": url, "sha256": sha, "size_bytes": os.path.getsize(dest), "expected_sha256": expected})
229
244
  except Exception as e: # 单个失败不中断整批,最后一起报
230
245
  errors.append(f"{rel}: {e}")
231
246
 
232
247
  with ThreadPoolExecutor(max_workers=max(1, a.jobs)) as ex:
233
248
  list(ex.map(one, jobs))
234
249
 
250
+ with open(os.path.join(out, "download-receipt.json"), "w", encoding="utf-8") as receipt:
251
+ json.dump({"pack_id": m.get("pack_id"), "manifest_sha256": m.get("manifest_sha256"), "status": "failed" if errors else "downloaded", "files": sorted(receipts, key=lambda r: r["file"]), "errors": errors}, receipt, ensure_ascii=False, indent=2)
252
+
235
253
  if local_color and not errors:
236
254
  write_postproduction(out, m, jobs)
237
255
 
@@ -8,7 +8,7 @@ export function localColorHandoff(manifest, goal = '', missingShots = []) {
8
8
  workflow: 'local_color',
9
9
  status: 'not_processed',
10
10
  goal,
11
- platform_lut: manifest.render_target?.color_lut ?? null,
11
+ platform_lut: manifest.render_target?.source_color_lut ?? manifest.render_target?.color_lut ?? null,
12
12
  missing_shots: missingShots,
13
13
  steps: [
14
14
  '将本清单保存为 manifest.json,用 save_handoff_toolchain 保存工具链,再运行 python3 fetch_pack.py manifest.json -o ./pack;使用新的输出目录。',
@@ -11,7 +11,7 @@
11
11
  * 用 get_storyboards / get_episode_status 轮询到完成。下载链接只发我方 COS 链接。
12
12
  */
13
13
  import { z } from 'zod';
14
- import { imageHandoff, localColorHandoff } from './local-postproduction.js';
14
+ import { localColorHandoff } from './local-postproduction.js';
15
15
  import { readFileSync, writeFileSync, chmodSync, mkdirSync } from 'node:fs';
16
16
  import { tmpdir } from 'node:os';
17
17
  import { dirname, join } from 'node:path';
@@ -525,6 +525,7 @@ export function registerProduceTools(server, client) {
525
525
  }, async ({ episode_id, quote_id, confirm_replace, review_token, acknowledge_review }) => jsonResult(await client.producePost(`/episodes/${episode_id}/storyboards/generate`, {
526
526
  quote_id, review_token, acknowledge_review, ...(confirm_replace ? { confirm_replace } : {}),
527
527
  })));
528
+ server.tool('get_frame_retry_diagnosis', '免费只读查询图片任务的连败诊断、证据任务 ID 和下一步动作。diagnosis_required 时先调用本工具:审计故障重审已有候选,未知原因先复核;不得原样重抽或擅自改剧本。', { image_id: z.number().int().positive() }, async ({ image_id }) => jsonResult(await client.produceGet(`/images/${image_id}/diagnosis`)));
528
529
  server.tool('get_asset_versions', '只读查询一个镜头的全部图片/视频版本、历史查看链接、依赖 ID 与版本哈希,以及已生成→与当前输入一致→审核有效→已采用四项独立事实。历史缺少依赖记录时显示未知,不补造快照。免费。', { storyboard_id: z.number().int().positive() }, async ({ storyboard_id }) => jsonResult(await client.produceGet(`/storyboards/${storyboard_id}/asset-versions`)));
529
530
  server.tool('get_asset_binding_repair_plan', '只读生成一集历史缺失场景绑定的可审核修复提案。返回原始地点/时段、候选 ID、匹配依据、歧义和 proposed_scene_id;applied=false,绝不按名字自动回填。逐项核对后才由客户决定是否改绑。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/asset-binding-repair-plan`)));
530
531
  server.tool('get_storyboards', '读某一集的分镜列表(供审阅/查进度)。含每镜首帧(first_frame_image)与视频(video_url)是否就绪。' +
@@ -540,7 +541,8 @@ export function registerProduceTools(server, client) {
540
541
  '两者均暂停本次自动尾帧回写与续接,先复核视频;字段缺席不代表做过检测,也不要直接重试收费生成。' +
541
542
  '★not_required=旁白/片尾卡镜:帧与视频由成片层渲染,本镜不需要生成——数补齐进度时把它当已完成,别重试。' +
542
543
  'fail_reason(sensitive/text_sensitive/copyright/face_mismatch/account_overdue/quota_full/authorizing/' +
543
- 'insufficient_credits/transient/repeat_rejected/pair_collateral)、retryable(true=可重试;false=改内容换图,重试无效)、fail_hint(人读文案)。' +
544
+ 'insufficient_credits/transient/repeat_rejected/diagnosis_required/pair_collateral)、retryable(true=可重试;false=先按fail_hint处理,勿原样重试)、fail_hint(人读文案)。' +
545
+ '★diagnosis_required=同输入同细因连败或已有候选等待判决:先查看已有候选及审计依据。审计故障先重审同一候选,原因未知先复核,不得直接改剧本或重复生成。' +
544
546
  '照 retryable 判该重试还是该改内容,别解析中文。' +
545
547
  '★pair_collateral=同镜另一帧未通过、本帧随批结束——**本帧自身没被判不合格,别去改它**:有 reopen_pair_id 就用它只重掷有过错的一侧,否则直接重生本镜。' +
546
548
  '★若某镜带 degraded_frames:[{frame_type,reason,reason_label,since,hint}],表示该帧是系统在同因连拒熔断后**放行**的(判据照记未拒),' +
@@ -560,6 +562,7 @@ export function registerProduceTools(server, client) {
560
562
  '出图注入谁的定妆图/哪张场景图/哪些道具图由这几项决定;角色名对照 get_characters、道具名对照 get_props。' +
561
563
  '改绑用 update_shot 的 character_ids(★全量覆盖,先从这里读现值再改,漏传即解绑)与 character_presence;' +
562
564
  '改完再读一次本工具核对。character_bindings 缺席 = 这次没读到(不是没绑),空数组才是没绑。' +
565
+ '★启用双边版本审核时,seam_review.status 为 passed/failed/not_checked/stale;stale 是证据过期,不是画面失败,不据此付费重生。continuity_intent 可用 update_shot 明确。' +
563
566
  '★每镜带 **seam_state**(本镜与上一镜的镜间接缝,与官网帧时间线同一判据):' +
564
567
  'ok=已续接(首帧就是上镜尾帧,或平台做过首尾帧衔接、已落地且之后两侧都没再重生) / ' +
565
568
  'unchained=该连续却没接上(标了连续从没接过,或平台发起的衔接重生没落地:被拒/失败/还在生成——本镜 frame_status 会是 failed/processing;' +
@@ -720,7 +723,8 @@ export function registerProduceTools(server, client) {
720
723
  '真实进度只看 get_storyboards 的 first_frame_image / get_jobs 的逐条生成记录;' +
721
724
  '余额不足(402)会中止整批,此时轮询再久也不会有结果,应去查余额而不是继续等。' +
722
725
  '★★**个别镜反复出不来、其余镜都好了**:那不是等得不够久,是这几镜被内容闸连拒。' +
723
- '看 get_storyboards 的 fail_reason(repeat_rejected / needs_content_fix / contract_rejected),' +
726
+ '看 get_storyboards 的 fail_reason(repeat_rejected / diagnosis_required / needs_content_fix / contract_rejected),' +
727
+ '若 diagnosis_required,先按 fail_hint 诊断已有候选;审计错误先重审,证据不足先复核,不要原样重抽或擅自改剧本。' +
724
728
  '然后**跑 run_precheck**(事后也能跑,会告诉你是哪种矛盾),按它的提示用 update_shot 改景别/描述/绑定角色再重生。' +
725
729
  '别继续 generate_frames 空转,也别把客户支去外部工具做图——外部图绕开身份锚与帧审计,人脸服装画风必漂。' +
726
730
  REVIEW_GATE_HINT('review_storyboards', '分镜') + CONFIRM_HINT, {
@@ -732,8 +736,9 @@ export function registerProduceTools(server, client) {
732
736
  'gpt-image-2.5-flare(默认·基础11点+每张参考图18点)/gpt-image-2.5-sunburst(同价·中文字形与细节更准)/' +
733
737
  'gemini-3.1-flash-image(Nano Banana 2·71点)/gemini-3-pro-image(Nano Banana Pro·更精细·175点)/' +
734
738
  'gemini-3.1-flash-lite-image(Nano Banana 2 Lite·便宜·31点)/doubao-seedream-5-0-260128(Seedream 5.0)'),
735
- }, async ({ episode_id, quote_id, frame_type, image_model, review_token, acknowledge_review }) => jsonResult(await client.producePost(`/episodes/${episode_id}/frames/generate`, {
736
- quote_id, frame_type, image_model, review_token, acknowledge_review,
739
+ allow_era_ref_conflict: z.boolean().optional().describe('★默认不传。时代审核判定客户上传的定妆图(或由它派生的身体/发型/设定图)与时代设定冲突时,出图会被拦下(400 ERA_USER_REF_CONFLICT,未扣费)。**先把冲突原因告诉客户、由客户确认造型无误**后才带 true:这些图保留不剔、照常出图。造型真不对就改定妆图,时代设定错了就 set_era_contract'),
740
+ }, async ({ episode_id, quote_id, frame_type, image_model, review_token, acknowledge_review, allow_era_ref_conflict }) => jsonResult(await client.producePost(`/episodes/${episode_id}/frames/generate`, {
741
+ quote_id, frame_type, image_model, review_token, acknowledge_review, allow_era_ref_conflict,
737
742
  })));
738
743
  // ---------- 单镜出帧/重生(改某一镜的画面走这里,别去外部平台出图再传回来) ----------
739
744
  server.tool('quote_shot_frame', '报价:重画/补出**某一镜的某一帧**要多少点(一帧=一张图)。返回 estimated_points、quote_id。零扣费。' +
@@ -765,7 +770,8 @@ export function registerProduceTools(server, client) {
765
770
  reopen_pair_id: z.string().optional().describe('来自 get_storyboards 该镜的同名字段;只重掷有问题的那一侧,不必再传 frame_type'),
766
771
  image_model: z.string().optional().describe('临时覆盖本次重画的图片模型(★必须与 quote_shot_frame 传的那个一致,否则 400 IMAGE_MODEL_MISMATCH;不传=用 drama 级设定,默认 ChatGPT Image 2.5 Flare);取值同 generate_frames'),
767
772
  allow_missing_terminal: z.boolean().optional().describe('★逃生门,默认不传。本镜标为「状态改变」却没写终态时,出尾帧会被前置闸拦下;带 true 表示「我知道,照现状出」。正解是先 update_shot 补 last_frame_prompt——那类镜照现状出的成功率 9.4%,这个参数只用于确认本镜就该「几乎不变」的场合'),
768
- }, async ({ storyboard_id, quote_id, frame_type, replace_user_frame, reopen_pair_id, image_model, allow_missing_terminal }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/frame/generate`, { quote_id, frame_type, replace_user_frame, reopen_pair_id, image_model, allow_missing_terminal })));
773
+ allow_era_ref_conflict: z.boolean().optional().describe('★默认不传。时代审核判定客户上传的定妆图(或由它派生的身体/发型/设定图)与时代设定冲突时,出图会被拦下(400 ERA_USER_REF_CONFLICT,未扣费)。**先把冲突原因告诉客户、由客户确认造型无误**后才带 true:这些图保留不剔、照常出图。造型真不对就改定妆图,时代设定错了就 set_era_contract'),
774
+ }, async ({ storyboard_id, quote_id, frame_type, replace_user_frame, reopen_pair_id, image_model, allow_missing_terminal, allow_era_ref_conflict }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/frame/generate`, { quote_id, frame_type, replace_user_frame, reopen_pair_id, image_model, allow_missing_terminal, allow_era_ref_conflict })));
769
775
  // ---------- 出视频(videos,大额) ----------
770
776
  server.tool('quote_videos', '报价:给某一集所有分镜批量出视频要多少点(与实际扣费同函数,较准)。返回 estimated_points、quote_id。零扣费。' +
771
777
  '⚠️ 视频是大额花费,务必把点数清楚告诉用户并等其确认。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/videos/quote`)));
@@ -1086,22 +1092,19 @@ export function registerProduceTools(server, client) {
1086
1092
  const image_url = await client.uploadLocalFile(file_path, 'image');
1087
1093
  return jsonResult(await client.producePost(`/storyboards/${storyboard_id}/frame`, { image_url, frame_type: frame_type ?? 'first_frame' }));
1088
1094
  });
1089
- server.tool('register_external_delivery', '★后期在平台外完成(本地换旁白/裁剪/加片尾/配乐)后,把**最终交给客户的那一版成片**登记回平台。免费。' +
1090
- '不登记的话,平台里的成片记录并不代表客户实际收到的版本,事后无法追溯「交付的是哪一版、每镜用的哪次生成」。' +
1091
- '平台会自己下载复核 sha256/大小/时长(不采信你报的值),并自动快照每镜此刻的来源素材(当前视频、采用的生成 id 及其哈希、裁剪窗口)' +
1092
- '和最近一次平台成片;你可在 manifest 里附上外部后期做了什么(例如 {audio:{narration:"本地替换",music:"..."}, trims:[...], ending:"..."})。' +
1093
- '新登记即为当前交付版,版本号每集递增;不改平台成片(get_final_cut 的 download_url 不变,另返回 delivery)。' +
1094
- '传 file_path(本地视频,≤300MB,自动上传)或已用 upload-url(kind=footage)上传得到的 file_url,二选一。', {
1095
+ server.tool('register_external_delivery', '登记外部后期成片,服务端核验 sha256、大小和时长。免费。有原始导出时传导出时的 handoff_pack_id 与 handoff_manifest_sha256;平台使用原始冻结清单,不按登记时最新素材猜来源。没有原始导出时来源标未知。登记不代表客户接受;启用追溯后还需 set_current_delivery 选择当前版本,confirm_delivery_acceptance 记录客户明确确认。file_path 或 file_url 二选一。', {
1095
1096
  episode_id: z.number().int().positive(),
1096
1097
  file_path: z.string().optional().describe('本地成片路径(mp4/mov/webm/m4v,≤300MB)'),
1097
1098
  file_url: z.string().optional().describe('已上传到本平台的 public_url(与 file_path 二选一)'),
1099
+ handoff_pack_id: z.string().optional(),
1100
+ handoff_manifest_sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
1098
1101
  note: z.string().max(500).optional(),
1099
1102
  manifest: z.record(z.unknown()).optional().describe('外部后期做了什么(≤64KB),原样存进交付清单的 client 段'),
1100
- }, async ({ episode_id, file_path, file_url, note, manifest }) => {
1103
+ }, async ({ episode_id, file_path, file_url, note, manifest, handoff_pack_id, handoff_manifest_sha256 }) => {
1101
1104
  const url = file_path ? await client.uploadLocalFile(file_path, 'footage') : file_url;
1102
1105
  if (!url)
1103
1106
  throw new Error('file_path 与 file_url 必须给一个');
1104
- return jsonResult(await client.producePost(`/episodes/${episode_id}/deliveries`, { file_url: url, note, manifest }));
1107
+ return jsonResult(await client.producePost(`/episodes/${episode_id}/deliveries`, { file_url: url, note, manifest, handoff_pack_id, handoff_manifest_sha256 }));
1105
1108
  });
1106
1109
  server.tool('list_deliveries', '列本集**所有成片版本**(新→旧,平台成片与外部交付共用一条版本号),每条含 sha256、时长、is_current 与完整清单。免费。' +
1107
1110
  '\n★source=platform:平台每出一版成片(compose_episode / rerender_episode / 多画幅)都会**自动登记**,清单记的是**渲染用的那份时间线**——' +
@@ -1109,6 +1112,7 @@ export function registerProduceTools(server, client) {
1109
1112
  '以及登记时刻的台词审计分族计数。写交付说明、核对「交出去的是哪一版」以它为准,别凭记忆。' +
1110
1113
  '平台版本**不会自动成为当前交付版**;客户选定哪一版,用 set_current_delivery 指过去。' +
1111
1114
  '\n★source=external:register_external_delivery 登记的外部后期成片。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/deliveries`)));
1115
+ server.tool('confirm_delivery_acceptance', '只有客户明确确认后才记录接受或撤销接受;不得把下载成功、模型判断或生成完成当客户验收。delivery_id=null 表示只有客户口头确认、文件未核验;否则绑定指定交付版本。选择当前与客户接受是独立事实,使用 set_current_delivery 选定后平台才显示该文件已交付完成。免费,不生成、不改素材。', { episode_id: z.number().int().positive(), delivery_id: z.number().int().positive().nullable(), client_accepted: z.boolean(), confirm_client_statement: z.literal(true), note: z.string().min(1).max(1000) }, async ({ episode_id, ...body }) => jsonResult(await client.producePost(`/episodes/${episode_id}/delivery-confirmation`, body)));
1112
1116
  server.tool('set_current_delivery', '把本集的某个成片版本设为当前交付版(例如客户最终选了较早的一版)。免费。平台版本(source=platform)与外部版本都可以设。' +
1113
1117
  '只影响 get_final_cut 的 delivery,不改平台成片。', { episode_id: z.number().int().positive(), delivery_id: z.number().int().positive() }, async ({ episode_id, delivery_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/deliveries/${delivery_id}/current`)));
1114
1118
  server.tool('upload_shot_footage', '用**客户自有的整段视频**(录屏 / 产品实拍 / 第三方成片)直接当某镜的视频——实拍素材镜。' +
@@ -1278,6 +1282,8 @@ export function registerProduceTools(server, client) {
1278
1282
  '有终态描述的是 40.2%;不补描述就重生,送厂商的提示词里根本没有终态段,画出来仍和首帧一样、' +
1279
1283
  '还是会被拒,而每次重生都照常扣费。get_shot_prompts 的 terminal_desc_missing=true 就是这个信号。', {
1280
1284
  storyboard_id: z.number().int().positive(),
1285
+ product_text_contract: z.object({ version: z.literal(1), strategy: z.enum(['generate_with_review', 'original_media', 'postproduction']), targets: z.array(z.object({ asset_kind: z.enum(['prop', 'product']), asset_id: z.number().int().positive(), reference_url: z.string().url(), region: z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() }), exact_text: z.string().min(1).max(256) })).min(1).max(4) }).nullable().optional().describe('明确保留的实物原字:资产 ID、原图、归一化区域、原文及处理方式;生成不保证逐字正确,必须人工核对。null 清除声明,不回填历史。'),
1286
+ continuity_intent: z.enum(['continuous_action', 'shot_change', 'insert_detail', 'jump_cut']).optional().describe('本镜相对上镜的剪辑意图:连续动作、换景别、插入细节、有意跳切。改变后旧双边审核失效;不自动生成或补审。'),
1281
1287
  character_ids: z.array(z.number().int().positive()).optional()
1282
1288
  .describe('本镜出场角色 ID 列表(★全量覆盖式,非增量,漏传的角色会被解绑)。决定出图时注入哪些角色的定妆图/设定图——非人角色(动物/生物)也必须绑定,否则形象会漂移。id 必须来自当前集已关联角色'),
1283
1289
  character_presence: z.array(z.object({
@@ -1286,7 +1292,7 @@ export function registerProduceTools(server, client) {
1286
1292
  })).optional()
1287
1293
  .describe('画外音标记:[{character_id, presence}]。voice_only = 这一镜他只有声音、人不在画面(画外音/门外说话/电话那头)——' +
1288
1294
  '出图时他的定妆图、人脸锁、站位、主体脸锚全部跳过,台词与配音不受影响;on_screen = 恢复在画面。' +
1289
- '角色必须在本镜绑定里(可与 character_ids 同一请求,先改绑再标)。只列要改的角色,没列的不动。' +
1295
+ '角色必须在本镜绑定里(可与 character_ids 同一请求,先改绑再标)。只列要改的角色,没列的不动(空数组 = 不改任何标记)。' +
1290
1296
  '★完全没出现在画面、台词也不是他说的 → 直接从 character_ids 去掉,不用标 voice_only'),
1291
1297
  title: z.string().optional().describe('镜头标题'),
1292
1298
  description: z.string().optional().describe('画面描述'),
@@ -1438,14 +1444,19 @@ export function registerProduceTools(server, client) {
1438
1444
  server.tool('get_assets', '读一部剧的全部资产聚合(角色/场景/道具/图片)。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/assets`)));
1439
1445
  // ========== P1 · 任务进度 + 成本/预算 ==========
1440
1446
  server.tool('get_jobs', '查一部剧的任务队列进度(出图/出视频/合成各阶段的 pending/processing/done/failed)。' +
1441
- '异步生成后用它看进度。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/jobs`)));
1447
+ '异步生成后用它看进度。免费。' +
1448
+ '★整集出帧(generate_frames)是后台逐镜派发,被拒的镜不会出现在任务列表里——看响应的 frame_dispatch_rejections' +
1449
+ '(每镜 storyboard_id/frame_type/code/message)。code=ERA_USER_REF_CONFLICT 表示客户上传的定妆图被时代审核判冲突:' +
1450
+ '把 message 转告客户、由客户确认后,带 allow_era_ref_conflict=true 对那几镜 generate_shot_frame。重启后该列表清空。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/jobs`)));
1442
1451
  server.tool('get_drama_bill', '本剧净费用账单(免费)。核账、回答「到底花了多少、钱花在哪、退了多少」时用它,别自己拼流水。' +
1443
1452
  '\n· totals / by_kind:每类 charged(扣费)、refunded(已退回)、net(净额)、charges/refunds(笔数)。' +
1444
1453
  '**net 才是花费**,与网页「本剧已花费」、get_budget_status 同源;charged 含失败后自动退回的空转,只用于核账。' +
1445
1454
  '\n· in_flight:已扣费、结果还没出的任务(平台是提交即扣、失败再退,没有单独的预扣)。' +
1446
- '\n· rework.video:同一镜第 2 次起的成功出片(返工);出图不计(首帧/尾帧/best-of-N 候选本来就是同镜多张)。' +
1455
+ '\n· rework.video:同一镜第 2 次起的成功出片计数(不证明浪费或失败);原因与图片费用用 get_cost_attribution 核验。旧统计出图不计(首帧/尾帧/best-of-N 候选本来就是同镜多张)。' +
1447
1456
  '\n· account_level:音色库克隆/设计/试听等**不属于任何一部剧**的语音扣费,单列、counted_in_drama=false,不计入本剧。' +
1448
1457
  '\n· available=false:账本暂时不可达,不给数字(不拿估算冒充),稍后重试。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/bill`)));
1458
+ server.tool('get_cost_attribution', '读取实际扣费与退款逐行归因。免费;净额仍以账本为准。图片和视频均显示任务、冻结输入版本、是否采用和明确登记的返工原因。未知和歧义不猜测;未采用不等于浪费。available=false 时没有可靠金额;complete=false 时明细有截断但 totals 是全额。历史 rework.video 只是重复成功出片的计数,不证明失败或浪费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/cost-attribution`)));
1459
+ server.tool('classify_cost_reason', '按明确证据登记某一账本行原因,保留操作者和说明。免费,不改变任何扣费或退款;不能仅凭未采用、重试或报错就判为供应商失败。', { drama_id: z.number().int().positive(), ledger_id: z.number().int().positive(), category: z.enum(['normal_alternative', 'customer_change', 'platform_repair', 'provider_failure', 'unknown']), note: z.string().min(1).max(2000) }, async ({ drama_id, ledger_id, category, note }) => jsonResult(await client.producePost(`/dramas/${drama_id}/cost-attribution/${ledger_id}/reason`, { category, note })));
1449
1460
  server.tool('get_cost_estimate', '查一部剧的整体成本(点数):已花费 + 还没做的部分的待花费区间 + 分类明细。免费。' +
1450
1461
  'already_spent_points 与 get_budget_status 的 spent、网页「本剧已花费」同源(平台扣费账本净额,已减退款),是同一个数。' +
1451
1462
  'spent_source=usage_mirror 表示账本暂不可达、回退到本地镜像(偏少,不含文本/配乐),向客户报数时须注明,稍后重试即恢复;spent_note 是口径说明。' +
@@ -1592,6 +1603,17 @@ export function registerProduceTools(server, client) {
1592
1603
  server.tool('mark_signature_prop', '标记/取消「招牌道具」(会在多镜复现的关键道具,加强一致性追踪)。signature=false 取消。免费。', { prop_id: z.number().int().positive(), signature: z.boolean().optional().describe('默认 true 标记;false 取消') }, async ({ prop_id, signature }) => jsonResult(await client.producePost(`/props/${prop_id}/signature`, { signature: signature !== false })));
1593
1604
  server.tool('get_color_scripts', '读该剧已生成的色彩脚本(全片配色情绪)。generate_color_script 生成、此工具读取。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/color-scripts`)));
1594
1605
  server.tool('get_motion_templates', '读该剧已有的动作模板(运镜/动作预设)。generate_motion_templates 生成、此工具读取。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/motion-templates`)));
1606
+ server.tool('record_product_text_review', '免费记录用户对当前产品原字的人工核对结果。只有用户明确打开文件逐字核对并确认结果后才能调用;不得用模型判断冒充人工核对。' +
1607
+ '从 get_storyboards 的 product_text 取当前 contract_hash 和 reviews 中的帧位、文件、generation_id;上传素材 generation_id 为 null,必须传同一响应的 media_version。任一变化后旧审核不能沿用。不重生、不换采用版本、不代表整片客户验收。', { storyboard_id: z.number().int().positive(), frame_type: z.enum(['first_frame', 'last_frame', 'video_identity']), generation_id: z.number().int().positive().nullable(), media_version: z.string().optional(), image_url: z.string(), contract_hash: z.string(), passed: z.boolean(), confirm_visual_review: z.literal(true), note: z.string().min(1).max(1000) }, async ({ storyboard_id, ...body }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/product-text-review`, body)));
1608
+ server.tool('audit_seams', '审核当前双边帧的剪辑连续性,只检查、不重生、不替换资产。默认 dry_run=true 返回待审范围,不收费。' +
1609
+ '插入细节仅检查物体身份和状态,不要求复制构图。已有当前有效结论不重复审核。' +
1610
+ '执行会按实际文本 token 用量计费,estimated_points=null 表示金额未报价,并非免费。' +
1611
+ '先展示计划和计费方式,经客户明确接受后才传 dry_run=false 与 confirm_usage_billing=true。' +
1612
+ '执行后用 get_storyboards 的 seam_review 检查实际结果;attempted 不代表通过。', {
1613
+ episode_id: z.number().int().positive(),
1614
+ dry_run: z.boolean().optional().describe('默认 true,只读计划'),
1615
+ confirm_usage_billing: z.boolean().optional().describe('仅客户明确接受按实际文本 token 用量计费后传 true'),
1616
+ }, async ({ episode_id, dry_run, confirm_usage_billing }) => jsonResult(await client.producePost(`/episodes/${episode_id}/audit-seams`, { dry_run: dry_run !== false, confirm_usage_billing: confirm_usage_billing === true })));
1595
1617
  // ========== P2 · 连续性 / 场景组 / 多画幅 ==========
1596
1618
  server.tool('chain_frames', '整集首尾帧接力链:让每镜首帧承接上镜尾帧,镜间画面连续(比各镜独立出帧更顺)。' +
1597
1619
  '★何时调:首帧出完、tail_frame_plan 要的尾帧也补完之后,review_frames / generate_videos 之前。' +
@@ -1777,7 +1799,8 @@ export function registerProduceTools(server, client) {
1777
1799
  // ── 素材交接包:客户自己拼片的通道 ──────────────────────────────────────
1778
1800
  // 与 compose_episode 的分工:compose_episode = 「平台替你拼」;本组工具 = 「素材给你,
1779
1801
  // 你自己决定转场、自己拼」。两条路都不丢客户的对白/音效/配乐/字幕成果。
1780
- server.tool('export_handoff_pack', '导出本集「素材交接包」清单:逐镜裸片 + 对白音轨 + 音效 + 配乐 + 字幕的可下载 URL,' +
1802
+ server.tool('export_handoff_pack', '启用交付追溯后会保存不可变导出清单(不改素材)。保留 pack_id 和 manifest_sha256,回传外部成片时引用。' +
1803
+ '导出本集「素材交接包」清单:逐镜裸片 + 对白音轨 + 音效 + 配乐 + 字幕的可下载 URL,' +
1781
1804
  '交给你在**自己那边**完成转场决策、拼接、混音、烧字幕——平台不参与终拼。免费,零扣费。\n' +
1782
1805
  '★本地调色/第三方 AI 后期:purpose=local_color;只有图片时传 media_type=images,无需生成视频。' +
1783
1806
  '本模式保留源文件、关闭导出包的自动 LUT、附带后期说明和本地对比页流程;不上传第三方、不收费生成、不回填平台。' +
@@ -1806,10 +1829,10 @@ export function registerProduceTools(server, client) {
1806
1829
  color_goal: z.string().max(4000).optional().describe('客户的后期目标与保护项;作为说明保存,不发送给模型'),
1807
1830
  }, async ({ episode_id, media_type, purpose, color_goal }) => {
1808
1831
  if (media_type === 'images') {
1809
- const rows = await client.produceGet(`/episodes/${episode_id}/storyboards`);
1810
- return jsonResult(imageHandoff(episode_id, rows, color_goal));
1832
+ const manifest = await client.produceGet(`/episodes/${episode_id}/handoff-pack?media_type=images`);
1833
+ return jsonResult(localColorHandoff(manifest, color_goal, manifest.missing_shots ?? []));
1811
1834
  }
1812
- const manifest = await client.produceGet(`/episodes/${episode_id}/handoff-pack`);
1835
+ const manifest = await client.produceGet(`/episodes/${episode_id}/handoff-pack${purpose === 'local_color' ? '?purpose=local_color' : ''}`);
1813
1836
  if (purpose === 'local_color')
1814
1837
  return jsonResult(localColorHandoff(manifest, color_goal));
1815
1838
  return jsonResult({
package/openapi.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "StarReel Production API",
5
- "version": "0.1.106",
6
- "description": "Turn a script into a finished, downloadable short-drama episode over REST.\n\nPipeline: script → AI rewrite → cast/scenes/props extraction → storyboards → portraits & sheets + scene plates → keyframes → video shots → TTS → final cut (.mp4).\n\n**Billing is prepaid and agent-safe**: big-ticket stages are quote-then-generate (`quote_*` returns a `quote_id`). For video, quote == actual charge. For images the quote is a *range* — models billed per reference image are priced before those are assembled, so `estimated_points` is the upper bound (size the balance by it and a run never dies half-way) and `typical_points` is the usual cost; fixed-price models return the two equal. Insufficient balance returns 402 — nothing half-runs and the account never goes negative.\n\nAuth: exchange your API key at `POST /v1/agent/token` for a 15-minute bearer token.\n\nGenerated from the @starreel/mcp v0.1.106 tool surface (operationIds match MCP tool names 1:1)."
5
+ "version": "0.1.108",
6
+ "description": "Turn a script into a finished, downloadable short-drama episode over REST.\n\nPipeline: script → AI rewrite → cast/scenes/props extraction → storyboards → portraits & sheets + scene plates → keyframes → video shots → TTS → final cut (.mp4).\n\n**Billing is prepaid and agent-safe**: big-ticket stages are quote-then-generate (`quote_*` returns a `quote_id`). For video, quote == actual charge. For images the quote is a *range* — models billed per reference image are priced before those are assembled, so `estimated_points` is the upper bound (size the balance by it and a run never dies half-way) and `typical_points` is the usual cost; fixed-price models return the two equal. Insufficient balance returns 402 — nothing half-runs and the account never goes negative.\n\nAuth: exchange your API key at `POST /v1/agent/token` for a 15-minute bearer token.\n\nGenerated from the @starreel/mcp v0.1.108 tool surface (operationIds match MCP tool names 1:1)."
7
7
  },
8
8
  "servers": [
9
9
  {
@@ -47,6 +47,10 @@
47
47
  "name": "episodes",
48
48
  "description": "Operations under /episodes"
49
49
  },
50
+ {
51
+ "name": "images",
52
+ "description": "Operations under /images"
53
+ },
50
54
  {
51
55
  "name": "products",
52
56
  "description": "Operations under /products"
@@ -1375,7 +1379,7 @@
1375
1379
  "get": {
1376
1380
  "operationId": "get_drama_bill",
1377
1381
  "summary": "本剧净费用账单(免费)",
1378
- "description": "本剧净费用账单(免费)。核账、回答「到底花了多少、钱花在哪、退了多少」时用它,别自己拼流水。\n· totals / by_kind:每类 charged(扣费)、refunded(已退回)、net(净额)、charges/refunds(笔数)。**net 才是花费**,与网页「本剧已花费」、get_budget_status 同源;charged 含失败后自动退回的空转,只用于核账。\n· in_flight:已扣费、结果还没出的任务(平台是提交即扣、失败再退,没有单独的预扣)。\n· rework.video:同一镜第 2 次起的成功出片(返工);出图不计(首帧/尾帧/best-of-N 候选本来就是同镜多张)。\n· account_level:音色库克隆/设计/试听等**不属于任何一部剧**的语音扣费,单列、counted_in_drama=false,不计入本剧。\n· available=false:账本暂时不可达,不给数字(不拿估算冒充),稍后重试。",
1382
+ "description": "本剧净费用账单(免费)。核账、回答「到底花了多少、钱花在哪、退了多少」时用它,别自己拼流水。\n· totals / by_kind:每类 charged(扣费)、refunded(已退回)、net(净额)、charges/refunds(笔数)。**net 才是花费**,与网页「本剧已花费」、get_budget_status 同源;charged 含失败后自动退回的空转,只用于核账。\n· in_flight:已扣费、结果还没出的任务(平台是提交即扣、失败再退,没有单独的预扣)。\n· rework.video:同一镜第 2 次起的成功出片计数(不证明浪费或失败);原因与图片费用用 get_cost_attribution 核验。旧统计出图不计(首帧/尾帧/best-of-N 候选本来就是同镜多张)。\n· account_level:音色库克隆/设计/试听等**不属于任何一部剧**的语音扣费,单列、counted_in_drama=false,不计入本剧。\n· available=false:账本暂时不可达,不给数字(不拿估算冒充),稍后重试。",
1379
1383
  "tags": [
1380
1384
  "dramas"
1381
1385
  ],
@@ -1575,6 +1579,140 @@
1575
1579
  }
1576
1580
  }
1577
1581
  },
1582
+ "/dramas/{drama_id}/cost-attribution": {
1583
+ "get": {
1584
+ "operationId": "get_cost_attribution",
1585
+ "summary": "读取实际扣费与退款逐行归因",
1586
+ "description": "读取实际扣费与退款逐行归因。免费;净额仍以账本为准。图片和视频均显示任务、冻结输入版本、是否采用和明确登记的返工原因。未知和歧义不猜测;未采用不等于浪费。available=false 时没有可靠金额;complete=false 时明细有截断但 totals 是全额。历史 rework.video 只是重复成功出片的计数,不证明失败或浪费。",
1587
+ "tags": [
1588
+ "dramas"
1589
+ ],
1590
+ "parameters": [
1591
+ {
1592
+ "name": "drama_id",
1593
+ "in": "path",
1594
+ "required": true,
1595
+ "schema": {
1596
+ "type": "integer",
1597
+ "exclusiveMinimum": 0
1598
+ }
1599
+ }
1600
+ ],
1601
+ "responses": {
1602
+ "200": {
1603
+ "description": "StarReel envelope",
1604
+ "content": {
1605
+ "application/json": {
1606
+ "schema": {
1607
+ "type": "object",
1608
+ "properties": {
1609
+ "code": {
1610
+ "type": "integer"
1611
+ },
1612
+ "message": {
1613
+ "type": "string"
1614
+ },
1615
+ "data": {
1616
+ "description": "Operation result payload"
1617
+ }
1618
+ }
1619
+ }
1620
+ }
1621
+ }
1622
+ },
1623
+ "402": {
1624
+ "description": "Insufficient prepaid balance (never overdrafts); response carries `needed` points"
1625
+ }
1626
+ }
1627
+ }
1628
+ },
1629
+ "/dramas/{drama_id}/cost-attribution/{ledger_id}/reason": {
1630
+ "post": {
1631
+ "operationId": "classify_cost_reason",
1632
+ "summary": "按明确证据登记某一账本行原因,保留操作者和说明",
1633
+ "description": "按明确证据登记某一账本行原因,保留操作者和说明。免费,不改变任何扣费或退款;不能仅凭未采用、重试或报错就判为供应商失败。",
1634
+ "tags": [
1635
+ "dramas"
1636
+ ],
1637
+ "parameters": [
1638
+ {
1639
+ "name": "drama_id",
1640
+ "in": "path",
1641
+ "required": true,
1642
+ "schema": {
1643
+ "type": "integer",
1644
+ "exclusiveMinimum": 0
1645
+ }
1646
+ },
1647
+ {
1648
+ "name": "ledger_id",
1649
+ "in": "path",
1650
+ "required": true,
1651
+ "schema": {
1652
+ "type": "integer",
1653
+ "exclusiveMinimum": 0
1654
+ }
1655
+ }
1656
+ ],
1657
+ "requestBody": {
1658
+ "required": true,
1659
+ "content": {
1660
+ "application/json": {
1661
+ "schema": {
1662
+ "type": "object",
1663
+ "properties": {
1664
+ "category": {
1665
+ "type": "string",
1666
+ "enum": [
1667
+ "normal_alternative",
1668
+ "customer_change",
1669
+ "platform_repair",
1670
+ "provider_failure",
1671
+ "unknown"
1672
+ ]
1673
+ },
1674
+ "note": {
1675
+ "type": "string",
1676
+ "minLength": 1,
1677
+ "maxLength": 2000
1678
+ }
1679
+ },
1680
+ "required": [
1681
+ "category",
1682
+ "note"
1683
+ ]
1684
+ }
1685
+ }
1686
+ }
1687
+ },
1688
+ "responses": {
1689
+ "200": {
1690
+ "description": "StarReel envelope",
1691
+ "content": {
1692
+ "application/json": {
1693
+ "schema": {
1694
+ "type": "object",
1695
+ "properties": {
1696
+ "code": {
1697
+ "type": "integer"
1698
+ },
1699
+ "message": {
1700
+ "type": "string"
1701
+ },
1702
+ "data": {
1703
+ "description": "Operation result payload"
1704
+ }
1705
+ }
1706
+ }
1707
+ }
1708
+ }
1709
+ },
1710
+ "402": {
1711
+ "description": "Insufficient prepaid balance (never overdrafts); response carries `needed` points"
1712
+ }
1713
+ }
1714
+ }
1715
+ },
1578
1716
  "/dramas/{drama_id}/cost-estimate": {
1579
1717
  "get": {
1580
1718
  "operationId": "get_cost_estimate",
@@ -1832,7 +1970,7 @@
1832
1970
  "get": {
1833
1971
  "operationId": "get_jobs",
1834
1972
  "summary": "查一部剧的任务队列进度(出图/出视频/合成各阶段的 pending/processing/done/failed)",
1835
- "description": "查一部剧的任务队列进度(出图/出视频/合成各阶段的 pending/processing/done/failed)。异步生成后用它看进度。免费。",
1973
+ "description": "查一部剧的任务队列进度(出图/出视频/合成各阶段的 pending/processing/done/failed)。异步生成后用它看进度。免费。★整集出帧(generate_frames)是后台逐镜派发,被拒的镜不会出现在任务列表里——看响应的 frame_dispatch_rejections(每镜 storyboard_id/frame_type/code/message)。code=ERA_USER_REF_CONFLICT 表示客户上传的定妆图被时代审核判冲突:把 message 转告客户、由客户确认后,带 allow_era_ref_conflict=true 对那几镜 generate_shot_frame。重启后该列表清空。",
1836
1974
  "tags": [
1837
1975
  "dramas"
1838
1976
  ],
@@ -3121,6 +3259,71 @@
3121
3259
  }
3122
3260
  }
3123
3261
  },
3262
+ "/episodes/{episode_id}/audit-seams": {
3263
+ "post": {
3264
+ "operationId": "audit_seams",
3265
+ "summary": "审核当前双边帧的剪辑连续性,只检查、不重生、不替换资产",
3266
+ "description": "审核当前双边帧的剪辑连续性,只检查、不重生、不替换资产。默认 dry_run=true 返回待审范围,不收费。插入细节仅检查物体身份和状态,不要求复制构图。已有当前有效结论不重复审核。执行会按实际文本 token 用量计费,estimated_points=null 表示金额未报价,并非免费。先展示计划和计费方式,经客户明确接受后才传 dry_run=false 与 confirm_usage_billing=true。执行后用 get_storyboards 的 seam_review 检查实际结果;attempted 不代表通过。",
3267
+ "tags": [
3268
+ "episodes"
3269
+ ],
3270
+ "parameters": [
3271
+ {
3272
+ "name": "episode_id",
3273
+ "in": "path",
3274
+ "required": true,
3275
+ "schema": {
3276
+ "type": "integer",
3277
+ "exclusiveMinimum": 0
3278
+ }
3279
+ }
3280
+ ],
3281
+ "requestBody": {
3282
+ "required": false,
3283
+ "content": {
3284
+ "application/json": {
3285
+ "schema": {
3286
+ "type": "object",
3287
+ "properties": {
3288
+ "dry_run": {
3289
+ "type": "boolean"
3290
+ },
3291
+ "confirm_usage_billing": {
3292
+ "type": "boolean"
3293
+ }
3294
+ }
3295
+ }
3296
+ }
3297
+ }
3298
+ },
3299
+ "responses": {
3300
+ "200": {
3301
+ "description": "StarReel envelope",
3302
+ "content": {
3303
+ "application/json": {
3304
+ "schema": {
3305
+ "type": "object",
3306
+ "properties": {
3307
+ "code": {
3308
+ "type": "integer"
3309
+ },
3310
+ "message": {
3311
+ "type": "string"
3312
+ },
3313
+ "data": {
3314
+ "description": "Operation result payload"
3315
+ }
3316
+ }
3317
+ }
3318
+ }
3319
+ }
3320
+ },
3321
+ "402": {
3322
+ "description": "Insufficient prepaid balance (never overdrafts); response carries `needed` points"
3323
+ }
3324
+ }
3325
+ }
3326
+ },
3124
3327
  "/episodes/{episode_id}/autofill-status": {
3125
3328
  "get": {
3126
3329
  "operationId": "get_autofill_status",
@@ -3605,6 +3808,87 @@
3605
3808
  }
3606
3809
  }
3607
3810
  },
3811
+ "/episodes/{episode_id}/delivery-confirmation": {
3812
+ "post": {
3813
+ "operationId": "confirm_delivery_acceptance",
3814
+ "summary": "只有客户明确确认后才记录接受或撤销接受;不得把下载成功、模型判断或生成完成当客户验收",
3815
+ "description": "只有客户明确确认后才记录接受或撤销接受;不得把下载成功、模型判断或生成完成当客户验收。delivery_id=null 表示只有客户口头确认、文件未核验;否则绑定指定交付版本。选择当前与客户接受是独立事实,使用 set_current_delivery 选定后平台才显示该文件已交付完成。免费,不生成、不改素材。",
3816
+ "tags": [
3817
+ "episodes"
3818
+ ],
3819
+ "parameters": [
3820
+ {
3821
+ "name": "episode_id",
3822
+ "in": "path",
3823
+ "required": true,
3824
+ "schema": {
3825
+ "type": "integer",
3826
+ "exclusiveMinimum": 0
3827
+ }
3828
+ }
3829
+ ],
3830
+ "requestBody": {
3831
+ "required": true,
3832
+ "content": {
3833
+ "application/json": {
3834
+ "schema": {
3835
+ "type": "object",
3836
+ "properties": {
3837
+ "delivery_id": {
3838
+ "type": "integer",
3839
+ "exclusiveMinimum": 0
3840
+ },
3841
+ "client_accepted": {
3842
+ "type": "boolean"
3843
+ },
3844
+ "confirm_client_statement": {
3845
+ "type": "boolean",
3846
+ "const": true
3847
+ },
3848
+ "note": {
3849
+ "type": "string",
3850
+ "minLength": 1,
3851
+ "maxLength": 1000
3852
+ }
3853
+ },
3854
+ "required": [
3855
+ "delivery_id",
3856
+ "client_accepted",
3857
+ "confirm_client_statement",
3858
+ "note"
3859
+ ]
3860
+ }
3861
+ }
3862
+ }
3863
+ },
3864
+ "responses": {
3865
+ "200": {
3866
+ "description": "StarReel envelope",
3867
+ "content": {
3868
+ "application/json": {
3869
+ "schema": {
3870
+ "type": "object",
3871
+ "properties": {
3872
+ "code": {
3873
+ "type": "integer"
3874
+ },
3875
+ "message": {
3876
+ "type": "string"
3877
+ },
3878
+ "data": {
3879
+ "description": "Operation result payload"
3880
+ }
3881
+ }
3882
+ }
3883
+ }
3884
+ }
3885
+ },
3886
+ "402": {
3887
+ "description": "Insufficient prepaid balance (never overdrafts); response carries `needed` points"
3888
+ }
3889
+ }
3890
+ }
3891
+ },
3608
3892
  "/episodes/{episode_id}/derive-pose-chain": {
3609
3893
  "post": {
3610
3894
  "operationId": "derive_pose_chain",
@@ -4542,7 +4826,7 @@
4542
4826
  "post": {
4543
4827
  "operationId": "generate_frames",
4544
4828
  "summary": "确认后批量出帧:后台异步",
4545
- "description": "确认后批量出帧:后台异步。用 get_storyboards 轮询,first_frame_image/last_frame_image 逐镜填充即完成。★图片生成较慢——每张几十秒到数分钟(尤其高清模型),整集可能十几分钟。轮询看 frame_status:**pending=还在生成(继续耐心等,别当失败、别重复调 generate_frames,重复触发=白花钱)**、ready=完成、failed=才是真失败。别因为「等了一会儿还没出」就判定生成失败或重试。出视频前必须先出帧,否则视频会退化成无一致性锚点的画面。★**出帧前先把两个锚备齐**:定妆图(generate_portraits_and_sheets,锚人)与空景基板(generate_scene_images,锚景)。缺基板不会拦你,但每个场景的第一镜会没有背景锚(平台的兜底补图只惠及同场景后续镜),背景从首镜起就开始漂。用 get_pipeline_status 的 generate_scene_images 步核对 completed/total。frame_type 要与 quote_frames 用的一致(默认 first_frame)。★这是**批量补缺帧**:只给「缺该帧」的镜出图,已有首帧的镜会跳过——这是正常设计、不是\"系统拒绝重出\"。要**重出/重画某一镜已有的帧**(如换了定妆图要让新图生效),用 generate_shot_frame(单镜重生,平台带身份锚),不是这个工具、更不是自制图 upload_shot_frame。★**本工具默认只出首帧,出帧是两趟**:首帧出完必须调一次免费的 tail_frame_plan——它按平台自己的判据告出哪几镜需要独立尾帧(末态≠首态:大运镜/物体脱手/状态改变,占比约三成,你自己猜不出来),再用 frame_type=last_frame 批量补。生产实测 32 集里 30 集整集只出了首帧、其中 23 集一路出完视频:那些镜出视频时只有首帧一个锚,末态由模型自由发挥。本工具响应体里的 shots_needing_last_frame 就是这个数,不为 0 就别直接去 review_frames。★响应里的 frames_planned 是**计划数,不是已成功数**——本接口在后台派发循环开跑之前就返回了。真实进度只看 get_storyboards 的 first_frame_image / get_jobs 的逐条生成记录;余额不足(402)会中止整批,此时轮询再久也不会有结果,应去查余额而不是继续等。★★**个别镜反复出不来、其余镜都好了**:那不是等得不够久,是这几镜被内容闸连拒。看 get_storyboards 的 fail_reason(repeat_rejected / needs_content_fix / contract_rejected),然后**跑 run_precheck**(事后也能跑,会告诉你是哪种矛盾),按它的提示用 update_shot 改景别/描述/绑定角色再重生。别继续 generate_frames 空转,也别把客户支去外部工具做图——外部图绕开身份锚与帧审计,人脸服装画风必漂。\n★【分镜审查硬闸·免费】本步前必须先调 review_storyboards:把返回的 findings 逐条原样告诉客户(每条带 code=问题类型、shots=命中镜号、action=该调哪个工具修),再把 review_token 传进本工具。未审查会被 400 拒。审查后又改了内容 → token 自动失效,复审一次即可(仍免费)。有 error 时默认拦截;客户知情并坚持照现状继续,才带 acknowledge_review:true——带病推进大概率产出废片且照常扣费,不要替客户做这个决定。⚠️ 批量报价确认流程:先调对应的 quote_* 工具,把返回的 estimated_points 原样告诉用户,用户明确同意后,才用返回的 quote_id 调本工具。不要擅自确认。★**出图类报价(定妆图/场景图/出帧)给的是两个数**:estimated_points 是**上界**(按该模型参考图张数上限估,保证你不会按低了的预算开跑、出到一半 402 中断);同一响应里的 typical_points 是**通常花费**。两个都要告诉客户——只说上界会让人以为贵得多(满参考图的上界可达通常值的 2 倍以上),只说通常值又会让余额准备不足。固定价模型下两者相等,这时说一个数就行。出视频的报价与扣费同函数,不存在这个区间。",
4829
+ "description": "确认后批量出帧:后台异步。用 get_storyboards 轮询,first_frame_image/last_frame_image 逐镜填充即完成。★图片生成较慢——每张几十秒到数分钟(尤其高清模型),整集可能十几分钟。轮询看 frame_status:**pending=还在生成(继续耐心等,别当失败、别重复调 generate_frames,重复触发=白花钱)**、ready=完成、failed=才是真失败。别因为「等了一会儿还没出」就判定生成失败或重试。出视频前必须先出帧,否则视频会退化成无一致性锚点的画面。★**出帧前先把两个锚备齐**:定妆图(generate_portraits_and_sheets,锚人)与空景基板(generate_scene_images,锚景)。缺基板不会拦你,但每个场景的第一镜会没有背景锚(平台的兜底补图只惠及同场景后续镜),背景从首镜起就开始漂。用 get_pipeline_status 的 generate_scene_images 步核对 completed/total。frame_type 要与 quote_frames 用的一致(默认 first_frame)。★这是**批量补缺帧**:只给「缺该帧」的镜出图,已有首帧的镜会跳过——这是正常设计、不是\"系统拒绝重出\"。要**重出/重画某一镜已有的帧**(如换了定妆图要让新图生效),用 generate_shot_frame(单镜重生,平台带身份锚),不是这个工具、更不是自制图 upload_shot_frame。★**本工具默认只出首帧,出帧是两趟**:首帧出完必须调一次免费的 tail_frame_plan——它按平台自己的判据告出哪几镜需要独立尾帧(末态≠首态:大运镜/物体脱手/状态改变,占比约三成,你自己猜不出来),再用 frame_type=last_frame 批量补。生产实测 32 集里 30 集整集只出了首帧、其中 23 集一路出完视频:那些镜出视频时只有首帧一个锚,末态由模型自由发挥。本工具响应体里的 shots_needing_last_frame 就是这个数,不为 0 就别直接去 review_frames。★响应里的 frames_planned 是**计划数,不是已成功数**——本接口在后台派发循环开跑之前就返回了。真实进度只看 get_storyboards 的 first_frame_image / get_jobs 的逐条生成记录;余额不足(402)会中止整批,此时轮询再久也不会有结果,应去查余额而不是继续等。★★**个别镜反复出不来、其余镜都好了**:那不是等得不够久,是这几镜被内容闸连拒。看 get_storyboards 的 fail_reason(repeat_rejected / diagnosis_required / needs_content_fix / contract_rejected),若 diagnosis_required,先按 fail_hint 诊断已有候选;审计错误先重审,证据不足先复核,不要原样重抽或擅自改剧本。然后**跑 run_precheck**(事后也能跑,会告诉你是哪种矛盾),按它的提示用 update_shot 改景别/描述/绑定角色再重生。别继续 generate_frames 空转,也别把客户支去外部工具做图——外部图绕开身份锚与帧审计,人脸服装画风必漂。\n★【分镜审查硬闸·免费】本步前必须先调 review_storyboards:把返回的 findings 逐条原样告诉客户(每条带 code=问题类型、shots=命中镜号、action=该调哪个工具修),再把 review_token 传进本工具。未审查会被 400 拒。审查后又改了内容 → token 自动失效,复审一次即可(仍免费)。有 error 时默认拦截;客户知情并坚持照现状继续,才带 acknowledge_review:true——带病推进大概率产出废片且照常扣费,不要替客户做这个决定。⚠️ 批量报价确认流程:先调对应的 quote_* 工具,把返回的 estimated_points 原样告诉用户,用户明确同意后,才用返回的 quote_id 调本工具。不要擅自确认。★**出图类报价(定妆图/场景图/出帧)给的是两个数**:estimated_points 是**上界**(按该模型参考图张数上限估,保证你不会按低了的预算开跑、出到一半 402 中断);同一响应里的 typical_points 是**通常花费**。两个都要告诉客户——只说上界会让人以为贵得多(满参考图的上界可达通常值的 2 倍以上),只说通常值又会让余额准备不足。固定价模型下两者相等,这时说一个数就行。出视频的报价与扣费同函数,不存在这个区间。",
4546
4830
  "tags": [
4547
4831
  "episodes"
4548
4832
  ],
@@ -4584,6 +4868,9 @@
4584
4868
  },
4585
4869
  "acknowledge_review": {
4586
4870
  "type": "boolean"
4871
+ },
4872
+ "allow_era_ref_conflict": {
4873
+ "type": "boolean"
4587
4874
  }
4588
4875
  },
4589
4876
  "required": [
@@ -4741,8 +5028,8 @@
4741
5028
  "/episodes/{episode_id}/handoff-pack": {
4742
5029
  "get": {
4743
5030
  "operationId": "export_handoff_pack",
4744
- "summary": "导出本集「素材交接包」清单:逐镜裸片 + 对白音轨 + 音效 + 配乐 + 字幕的可下载 URL,交给你在**自己那边**完成转场决策、拼接、混音、烧字幕——平台不参与终拼",
4745
- "description": "导出本集「素材交接包」清单:逐镜裸片 + 对白音轨 + 音效 + 配乐 + 字幕的可下载 URL,交给你在**自己那边**完成转场决策、拼接、混音、烧字幕——平台不参与终拼。免费,零扣费。\n★本地调色/第三方 AI 后期:purpose=local_color;只有图片时传 media_type=images,无需生成视频。本模式保留源文件、关闭导出包的自动 LUT、附带后期说明和本地对比页流程;不上传第三方、不收费生成、不回填平台。图片来源可能已有处理,不能把所有导出图承诺为未调色原片。\n\n【推荐流程】① 调本工具拿 manifest;② 按 clips[].url 把裸片下载到本地;③ 你自己看片判断每个接缝该用什么转场(manifest 给了 scene_boundary 场景边界作判据),写一份 plan.json;④ 用 get_handoff_toolchain 拿到 compile_timeline.py 展开时间轴、assemble.sh 装配出成片。工具链已经把「加了重叠转场之后字幕/对白/音效怎么跟着位移」算好了。\n\n【三个不看就会翻车的事实】\n① **人声在哪要逐镜读 `shots[].voice_track.location`,不能读整集的 audio_contract.mode**——后者是整剧级声明,而全画外旁白镜的旁白是平台在完成侧混进裸片的,同一集里逐镜可以不同。四种取值各有各的处置:`separate_file`=对白在 dialogue_audio,不铺这镜就没台词;`baked_in_clip`=已在裸片音轨里,再叠一遍会双声;`missing`=**平台侧确认缺失**(旁白补偿失败),字幕还在但声音哪都没有——回平台重生成该镜,拉长转场掩盖不了;`unknown`=有台词却查不到来源,**必须试听裸片**再决定。★有字幕从来不等于有声音:字幕是逐句的,音频是逐镜的。`conflicts_with_contract=true` 的镜以实测为准,notes 里已按镜号点名。\n② 每镜必须按 trim_head_ms / duration_ms 裁剪再用;直接拼整条裸片会把平台已经 QC 掉的首尾形变帧一起拼进去。\n③ 字幕 cue、dialogue_audio.offset_ms、sfx[].offset_ms 的基准都是「该镜 trim 之后的第 0 毫秒」,不是成片绝对时间。你加多少重叠转场都不用改它们——交给 compile_timeline.py 展开,别手算累加。\n\n想让平台代拼、要平台级质量闸(终拼预检/音画等长/响度母带),改用 compose_episode。",
5031
+ "summary": "启用交付追溯后会保存不可变导出清单(不改素材)",
5032
+ "description": "启用交付追溯后会保存不可变导出清单(不改素材)。保留 pack_id 和 manifest_sha256,回传外部成片时引用。导出本集「素材交接包」清单:逐镜裸片 + 对白音轨 + 音效 + 配乐 + 字幕的可下载 URL,交给你在**自己那边**完成转场决策、拼接、混音、烧字幕——平台不参与终拼。免费,零扣费。\n★本地调色/第三方 AI 后期:purpose=local_color;只有图片时传 media_type=images,无需生成视频。本模式保留源文件、关闭导出包的自动 LUT、附带后期说明和本地对比页流程;不上传第三方、不收费生成、不回填平台。图片来源可能已有处理,不能把所有导出图承诺为未调色原片。\n\n【推荐流程】① 调本工具拿 manifest;② 按 clips[].url 把裸片下载到本地;③ 你自己看片判断每个接缝该用什么转场(manifest 给了 scene_boundary 场景边界作判据),写一份 plan.json;④ 用 get_handoff_toolchain 拿到 compile_timeline.py 展开时间轴、assemble.sh 装配出成片。工具链已经把「加了重叠转场之后字幕/对白/音效怎么跟着位移」算好了。\n\n【三个不看就会翻车的事实】\n① **人声在哪要逐镜读 `shots[].voice_track.location`,不能读整集的 audio_contract.mode**——后者是整剧级声明,而全画外旁白镜的旁白是平台在完成侧混进裸片的,同一集里逐镜可以不同。四种取值各有各的处置:`separate_file`=对白在 dialogue_audio,不铺这镜就没台词;`baked_in_clip`=已在裸片音轨里,再叠一遍会双声;`missing`=**平台侧确认缺失**(旁白补偿失败),字幕还在但声音哪都没有——回平台重生成该镜,拉长转场掩盖不了;`unknown`=有台词却查不到来源,**必须试听裸片**再决定。★有字幕从来不等于有声音:字幕是逐句的,音频是逐镜的。`conflicts_with_contract=true` 的镜以实测为准,notes 里已按镜号点名。\n② 每镜必须按 trim_head_ms / duration_ms 裁剪再用;直接拼整条裸片会把平台已经 QC 掉的首尾形变帧一起拼进去。\n③ 字幕 cue、dialogue_audio.offset_ms、sfx[].offset_ms 的基准都是「该镜 trim 之后的第 0 毫秒」,不是成片绝对时间。你加多少重叠转场都不用改它们——交给 compile_timeline.py 展开,别手算累加。\n\n想让平台代拼、要平台级质量闸(终拼预检/音画等长/响度母带),改用 compose_episode。",
4746
5033
  "tags": [
4747
5034
  "episodes"
4748
5035
  ],
@@ -6380,7 +6667,7 @@
6380
6667
  "get": {
6381
6668
  "operationId": "get_storyboards",
6382
6669
  "summary": "读某一集的分镜列表(供审阅/查进度)",
6383
- "description": "读某一集的分镜列表(供审阅/查进度)。含每镜首帧(first_frame_image)与视频(video_url)是否就绪。asset_versions 按首帧/尾帧/视频分别给出 generated(已生成)、input_consistent(与当前输入一致)、review_valid(审核有效)、adopted(已采用)、current_effective(当前有效)。null=缺少证据;ready 只表示存在产物,不代表当前有效。旧素材仍可查看。★每镜带**台词与说话人**:dialogue(原文,无台词为 null)、dialogue_lines([{speaker,text}] 逐行,与字幕/配音同一个解析器拆的——这里的 speaker 就是成片里被配音的那个;舞台提示已剥),以及 is_key_moment(1=关键镜,走高端图模)与 emotion_intensity(1-10)。核对「台词有没有丢/谁说的/关键镜是哪几镜/情绪曲线」用这一次调用即可,不用逐镜拉;update_shot 改完台词也在这里核对。画面/视频/首尾帧提示词仍只在 get_shot_prompts 逐镜读。★每镜还带**结构化状态**:frame_status/video_status(ready/pending/authorizing/rejected/failed/none/not_required)、★video_status=ready 只表示视频已生成,不等于画面验收通过。video_quality_issues 若含 terminal_cut:status=suspected 表示片尾疑似短促切镜,cut_at_s/tail_duration_s 给出位置;status=unavailable 表示探测未完成,不能当检测通过。两者均暂停本次自动尾帧回写与续接,先复核视频;字段缺席不代表做过检测,也不要直接重试收费生成。★not_required=旁白/片尾卡镜:帧与视频由成片层渲染,本镜不需要生成——数补齐进度时把它当已完成,别重试。fail_reason(sensitive/text_sensitive/copyright/face_mismatch/account_overdue/quota_full/authorizing/insufficient_credits/transient/repeat_rejected/pair_collateral)、retryable(true=可重试;false=改内容换图,重试无效)、fail_hint(人读文案)。照 retryable 判该重试还是该改内容,别解析中文。★pair_collateral=同镜另一帧未通过、本帧随批结束——**本帧自身没被判不合格,别去改它**:有 reopen_pair_id 就用它只重掷有过错的一侧,否则直接重生本镜。★若某镜带 degraded_frames:[{frame_type,reason,reason_label,since,hint}],表示该帧是系统在同因连拒熔断后**放行**的(判据照记未拒),URL 上与干净帧无区别但需人工复核;不满意按 hint 修正输入后 generate_shot_frame 重生该帧。★terminal_desc_missing=true:该镜标为「状态改变」却三处都没说结束时什么样——**出尾帧前**就能看出来,现在整集一次拿到(此前只有 get_shot_prompts 能逐镜问,一集几十次没人会去问)。先 update_shot 把结束状态写进 last_frame_prompt 再出尾帧:照现状硬出的历史成功率 9.4%,写了终态的 40.2%,而每次被拒都照常扣费。确实该「几乎不变」的镜才用 generate_shot_frame 的 allow_missing_terminal 放行。★first_frame_source/last_frame_source='upload' 表示该帧是**外部上传图**(绕开了身份锚/画风锚/best-of-N/帧审计整条质量链路)——人物·服装·画风漂移排查先看这些镜;外部图导致的漂移不是平台生成质量问题,修复正路是删掉外部图改走 generate_shot_frame 平台重生。★若某镜带 reopen_pair_id:该镜首尾帧同时生成时只有一侧真的有问题、另一侧是无辜陪拒,原样传给 generate_shot_frame 的 reopen_pair_id 参数可以只重掷有问题的那一侧(省一半算力/费用,不会拿去生成一张这次根本没打算重做的图)。没有这个字段就按 fail_reason/retryable 走常规重试。免费。★每镜还带**绑定**(只有 ID):character_ids(本镜绑定的角色)、character_bindings[{character_id,presence}](presence=on_screen 在画面 / voice_only 只有声音、不进画面)、scene_id、prop_ids、active_wardrobe_id。出图注入谁的定妆图/哪张场景图/哪些道具图由这几项决定;角色名对照 get_characters、道具名对照 get_props。改绑用 update_shot 的 character_ids(★全量覆盖,先从这里读现值再改,漏传即解绑)与 character_presence;改完再读一次本工具核对。character_bindings 缺席 = 这次没读到(不是没绑),空数组才是没绑。★每镜带 **seam_state**(本镜与上一镜的镜间接缝,与官网帧时间线同一判据):ok=已续接(首帧就是上镜尾帧,或平台做过首尾帧衔接、已落地且之后两侧都没再重生) / unchained=该连续却没接上(标了连续从没接过,或平台发起的衔接重生没落地:被拒/失败/还在生成——本镜 frame_status 会是 failed/processing;或衔接审过之后本镜首帧又被重生过) / broken=接过但没接上(seam_failed_dims 给出哪几维不连续:characters/wardrobe/props/lighting/scene;seam_audit_status=stale 且没有维度 = 上一镜尾帧在衔接之后又重生过,本镜首帧接的还是旧尾帧) / intended=有意切换(硬切/换场/时间跳跃/景别大跳/插入镜/外部上传帧),不用管 / missing=两侧帧没齐 / unknown=没标意图也没接过(系统没判,不是有问题) / na=首镜或旁白卡。seam_audit_status=pending 表示衔接已落地、平台的衔接审计还没出结论。出视频前只需处理 unchained 与 broken:整集用 chain_frames(先 dry_run),单道用 chain_shot_from_prev / chain_shot_from。",
6670
+ "description": "读某一集的分镜列表(供审阅/查进度)。含每镜首帧(first_frame_image)与视频(video_url)是否就绪。asset_versions 按首帧/尾帧/视频分别给出 generated(已生成)、input_consistent(与当前输入一致)、review_valid(审核有效)、adopted(已采用)、current_effective(当前有效)。null=缺少证据;ready 只表示存在产物,不代表当前有效。旧素材仍可查看。★每镜带**台词与说话人**:dialogue(原文,无台词为 null)、dialogue_lines([{speaker,text}] 逐行,与字幕/配音同一个解析器拆的——这里的 speaker 就是成片里被配音的那个;舞台提示已剥),以及 is_key_moment(1=关键镜,走高端图模)与 emotion_intensity(1-10)。核对「台词有没有丢/谁说的/关键镜是哪几镜/情绪曲线」用这一次调用即可,不用逐镜拉;update_shot 改完台词也在这里核对。画面/视频/首尾帧提示词仍只在 get_shot_prompts 逐镜读。★每镜还带**结构化状态**:frame_status/video_status(ready/pending/authorizing/rejected/failed/none/not_required)、★video_status=ready 只表示视频已生成,不等于画面验收通过。video_quality_issues 若含 terminal_cut:status=suspected 表示片尾疑似短促切镜,cut_at_s/tail_duration_s 给出位置;status=unavailable 表示探测未完成,不能当检测通过。两者均暂停本次自动尾帧回写与续接,先复核视频;字段缺席不代表做过检测,也不要直接重试收费生成。★not_required=旁白/片尾卡镜:帧与视频由成片层渲染,本镜不需要生成——数补齐进度时把它当已完成,别重试。fail_reason(sensitive/text_sensitive/copyright/face_mismatch/account_overdue/quota_full/authorizing/insufficient_credits/transient/repeat_rejected/diagnosis_required/pair_collateral)、retryable(true=可重试;false=先按fail_hint处理,勿原样重试)、fail_hint(人读文案)。★diagnosis_required=同输入同细因连败或已有候选等待判决:先查看已有候选及审计依据。审计故障先重审同一候选,原因未知先复核,不得直接改剧本或重复生成。照 retryable 判该重试还是该改内容,别解析中文。★pair_collateral=同镜另一帧未通过、本帧随批结束——**本帧自身没被判不合格,别去改它**:有 reopen_pair_id 就用它只重掷有过错的一侧,否则直接重生本镜。★若某镜带 degraded_frames:[{frame_type,reason,reason_label,since,hint}],表示该帧是系统在同因连拒熔断后**放行**的(判据照记未拒),URL 上与干净帧无区别但需人工复核;不满意按 hint 修正输入后 generate_shot_frame 重生该帧。★terminal_desc_missing=true:该镜标为「状态改变」却三处都没说结束时什么样——**出尾帧前**就能看出来,现在整集一次拿到(此前只有 get_shot_prompts 能逐镜问,一集几十次没人会去问)。先 update_shot 把结束状态写进 last_frame_prompt 再出尾帧:照现状硬出的历史成功率 9.4%,写了终态的 40.2%,而每次被拒都照常扣费。确实该「几乎不变」的镜才用 generate_shot_frame 的 allow_missing_terminal 放行。★first_frame_source/last_frame_source='upload' 表示该帧是**外部上传图**(绕开了身份锚/画风锚/best-of-N/帧审计整条质量链路)——人物·服装·画风漂移排查先看这些镜;外部图导致的漂移不是平台生成质量问题,修复正路是删掉外部图改走 generate_shot_frame 平台重生。★若某镜带 reopen_pair_id:该镜首尾帧同时生成时只有一侧真的有问题、另一侧是无辜陪拒,原样传给 generate_shot_frame 的 reopen_pair_id 参数可以只重掷有问题的那一侧(省一半算力/费用,不会拿去生成一张这次根本没打算重做的图)。没有这个字段就按 fail_reason/retryable 走常规重试。免费。★每镜还带**绑定**(只有 ID):character_ids(本镜绑定的角色)、character_bindings[{character_id,presence}](presence=on_screen 在画面 / voice_only 只有声音、不进画面)、scene_id、prop_ids、active_wardrobe_id。出图注入谁的定妆图/哪张场景图/哪些道具图由这几项决定;角色名对照 get_characters、道具名对照 get_props。改绑用 update_shot 的 character_ids(★全量覆盖,先从这里读现值再改,漏传即解绑)与 character_presence;改完再读一次本工具核对。character_bindings 缺席 = 这次没读到(不是没绑),空数组才是没绑。★启用双边版本审核时,seam_review.status 为 passed/failed/not_checked/stale;stale 是证据过期,不是画面失败,不据此付费重生。continuity_intent 可用 update_shot 明确。★每镜带 **seam_state**(本镜与上一镜的镜间接缝,与官网帧时间线同一判据):ok=已续接(首帧就是上镜尾帧,或平台做过首尾帧衔接、已落地且之后两侧都没再重生) / unchained=该连续却没接上(标了连续从没接过,或平台发起的衔接重生没落地:被拒/失败/还在生成——本镜 frame_status 会是 failed/processing;或衔接审过之后本镜首帧又被重生过) / broken=接过但没接上(seam_failed_dims 给出哪几维不连续:characters/wardrobe/props/lighting/scene;seam_audit_status=stale 且没有维度 = 上一镜尾帧在衔接之后又重生过,本镜首帧接的还是旧尾帧) / intended=有意切换(硬切/换场/时间跳跃/景别大跳/插入镜/外部上传帧),不用管 / missing=两侧帧没齐 / unknown=没标意图也没接过(系统没判,不是有问题) / na=首镜或旁白卡。seam_audit_status=pending 表示衔接已落地、平台的衔接审计还没出结论。出视频前只需处理 unchained 与 broken:整集用 chain_frames(先 dry_run),单道用 chain_shot_from_prev / chain_shot_from。",
6384
6671
  "tags": [
6385
6672
  "episodes"
6386
6673
  ],
@@ -7848,6 +8135,53 @@
7848
8135
  }
7849
8136
  }
7850
8137
  },
8138
+ "/images/{image_id}/diagnosis": {
8139
+ "get": {
8140
+ "operationId": "get_frame_retry_diagnosis",
8141
+ "summary": "免费只读查询图片任务的连败诊断、证据任务 ID 和下一步动作",
8142
+ "description": "免费只读查询图片任务的连败诊断、证据任务 ID 和下一步动作。diagnosis_required 时先调用本工具:审计故障重审已有候选,未知原因先复核;不得原样重抽或擅自改剧本。",
8143
+ "tags": [
8144
+ "images"
8145
+ ],
8146
+ "parameters": [
8147
+ {
8148
+ "name": "image_id",
8149
+ "in": "path",
8150
+ "required": true,
8151
+ "schema": {
8152
+ "type": "integer",
8153
+ "exclusiveMinimum": 0
8154
+ }
8155
+ }
8156
+ ],
8157
+ "responses": {
8158
+ "200": {
8159
+ "description": "StarReel envelope",
8160
+ "content": {
8161
+ "application/json": {
8162
+ "schema": {
8163
+ "type": "object",
8164
+ "properties": {
8165
+ "code": {
8166
+ "type": "integer"
8167
+ },
8168
+ "message": {
8169
+ "type": "string"
8170
+ },
8171
+ "data": {
8172
+ "description": "Operation result payload"
8173
+ }
8174
+ }
8175
+ }
8176
+ }
8177
+ }
8178
+ },
8179
+ "402": {
8180
+ "description": "Insufficient prepaid balance (never overdrafts); response carries `needed` points"
8181
+ }
8182
+ }
8183
+ }
8184
+ },
7851
8185
  "/products/{product_id}/sheet": {
7852
8186
  "post": {
7853
8187
  "operationId": "generate_product_sheet",
@@ -8840,6 +9174,100 @@
8840
9174
  "schema": {
8841
9175
  "type": "object",
8842
9176
  "properties": {
9177
+ "product_text_contract": {
9178
+ "type": "object",
9179
+ "properties": {
9180
+ "version": {
9181
+ "type": "number",
9182
+ "const": 1
9183
+ },
9184
+ "strategy": {
9185
+ "type": "string",
9186
+ "enum": [
9187
+ "generate_with_review",
9188
+ "original_media",
9189
+ "postproduction"
9190
+ ]
9191
+ },
9192
+ "targets": {
9193
+ "type": "array",
9194
+ "items": {
9195
+ "type": "object",
9196
+ "properties": {
9197
+ "asset_kind": {
9198
+ "type": "string",
9199
+ "enum": [
9200
+ "prop",
9201
+ "product"
9202
+ ]
9203
+ },
9204
+ "asset_id": {
9205
+ "type": "integer",
9206
+ "exclusiveMinimum": 0
9207
+ },
9208
+ "reference_url": {
9209
+ "type": "string",
9210
+ "format": "uri"
9211
+ },
9212
+ "region": {
9213
+ "type": "object",
9214
+ "properties": {
9215
+ "x": {
9216
+ "type": "number"
9217
+ },
9218
+ "y": {
9219
+ "type": "number"
9220
+ },
9221
+ "width": {
9222
+ "type": "number"
9223
+ },
9224
+ "height": {
9225
+ "type": "number"
9226
+ }
9227
+ },
9228
+ "required": [
9229
+ "x",
9230
+ "y",
9231
+ "width",
9232
+ "height"
9233
+ ],
9234
+ "additionalProperties": false
9235
+ },
9236
+ "exact_text": {
9237
+ "type": "string",
9238
+ "minLength": 1,
9239
+ "maxLength": 256
9240
+ }
9241
+ },
9242
+ "required": [
9243
+ "asset_kind",
9244
+ "asset_id",
9245
+ "reference_url",
9246
+ "region",
9247
+ "exact_text"
9248
+ ],
9249
+ "additionalProperties": false
9250
+ },
9251
+ "minItems": 1,
9252
+ "maxItems": 4
9253
+ }
9254
+ },
9255
+ "required": [
9256
+ "version",
9257
+ "strategy",
9258
+ "targets"
9259
+ ],
9260
+ "additionalProperties": false
9261
+ },
9262
+ "continuity_intent": {
9263
+ "type": "string",
9264
+ "enum": [
9265
+ "continuous_action",
9266
+ "shot_change",
9267
+ "insert_detail",
9268
+ "jump_cut"
9269
+ ]
9270
+ },
8843
9271
  "character_ids": {
8844
9272
  "type": "array",
8845
9273
  "items": {
@@ -9621,6 +10049,9 @@
9621
10049
  },
9622
10050
  "allow_missing_terminal": {
9623
10051
  "type": "boolean"
10052
+ },
10053
+ "allow_era_ref_conflict": {
10054
+ "type": "boolean"
9624
10055
  }
9625
10056
  },
9626
10057
  "required": [
@@ -9919,6 +10350,107 @@
9919
10350
  }
9920
10351
  }
9921
10352
  },
10353
+ "/storyboards/{storyboard_id}/product-text-review": {
10354
+ "post": {
10355
+ "operationId": "record_product_text_review",
10356
+ "summary": "免费记录用户对当前产品原字的人工核对结果",
10357
+ "description": "免费记录用户对当前产品原字的人工核对结果。只有用户明确打开文件逐字核对并确认结果后才能调用;不得用模型判断冒充人工核对。从 get_storyboards 的 product_text 取当前 contract_hash 和 reviews 中的帧位、文件、generation_id;上传素材 generation_id 为 null,必须传同一响应的 media_version。任一变化后旧审核不能沿用。不重生、不换采用版本、不代表整片客户验收。",
10358
+ "tags": [
10359
+ "storyboards"
10360
+ ],
10361
+ "parameters": [
10362
+ {
10363
+ "name": "storyboard_id",
10364
+ "in": "path",
10365
+ "required": true,
10366
+ "schema": {
10367
+ "type": "integer",
10368
+ "exclusiveMinimum": 0
10369
+ }
10370
+ }
10371
+ ],
10372
+ "requestBody": {
10373
+ "required": true,
10374
+ "content": {
10375
+ "application/json": {
10376
+ "schema": {
10377
+ "type": "object",
10378
+ "properties": {
10379
+ "frame_type": {
10380
+ "type": "string",
10381
+ "enum": [
10382
+ "first_frame",
10383
+ "last_frame",
10384
+ "video_identity"
10385
+ ]
10386
+ },
10387
+ "generation_id": {
10388
+ "type": "integer",
10389
+ "exclusiveMinimum": 0
10390
+ },
10391
+ "media_version": {
10392
+ "type": "string"
10393
+ },
10394
+ "image_url": {
10395
+ "type": "string"
10396
+ },
10397
+ "contract_hash": {
10398
+ "type": "string"
10399
+ },
10400
+ "passed": {
10401
+ "type": "boolean"
10402
+ },
10403
+ "confirm_visual_review": {
10404
+ "type": "boolean",
10405
+ "const": true
10406
+ },
10407
+ "note": {
10408
+ "type": "string",
10409
+ "minLength": 1,
10410
+ "maxLength": 1000
10411
+ }
10412
+ },
10413
+ "required": [
10414
+ "frame_type",
10415
+ "generation_id",
10416
+ "image_url",
10417
+ "contract_hash",
10418
+ "passed",
10419
+ "confirm_visual_review",
10420
+ "note"
10421
+ ]
10422
+ }
10423
+ }
10424
+ }
10425
+ },
10426
+ "responses": {
10427
+ "200": {
10428
+ "description": "StarReel envelope",
10429
+ "content": {
10430
+ "application/json": {
10431
+ "schema": {
10432
+ "type": "object",
10433
+ "properties": {
10434
+ "code": {
10435
+ "type": "integer"
10436
+ },
10437
+ "message": {
10438
+ "type": "string"
10439
+ },
10440
+ "data": {
10441
+ "description": "Operation result payload"
10442
+ }
10443
+ }
10444
+ }
10445
+ }
10446
+ }
10447
+ },
10448
+ "402": {
10449
+ "description": "Insufficient prepaid balance (never overdrafts); response carries `needed` points"
10450
+ }
10451
+ }
10452
+ }
10453
+ },
9922
10454
  "/storyboards/{storyboard_id}/prompts": {
9923
10455
  "get": {
9924
10456
  "operationId": "get_shot_prompts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starreel/mcp",
3
- "version": "0.1.106",
3
+ "version": "0.1.108",
4
4
  "mcpName": "ai.starreel/starreel",
5
5
  "description": "StarReel MCP server — drive the AI short-drama production pipeline (script → storyboards → frames → video → final cut) from Claude, Cursor, or any MCP client",
6
6
  "license": "MIT",
package/server.json CHANGED
@@ -2,13 +2,13 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
3
3
  "name": "ai.starreel/starreel",
4
4
  "description": "Turn a script into a finished, downloadable short-drama episode — via MCP or REST.",
5
- "version": "0.1.106",
5
+ "version": "0.1.108",
6
6
  "websiteUrl": "https://starreel.ai",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@starreel/mcp",
11
- "version": "0.1.106",
11
+ "version": "0.1.108",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  },