@starreel/mcp 0.1.103 → 0.1.105

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/README.zh-CN.md CHANGED
@@ -137,3 +137,22 @@ env = { "STARREEL_API_KEY" = "srk_live_xxx" }
137
137
  |---|---|---|
138
138
  | `STARREEL_API_KEY` | ✅ | — |
139
139
  | `STARREEL_AUTH_BASE` | | `https://api.shortreelai.com` |
140
+
141
+ ## 本地调色与第三方 AI 后期
142
+
143
+ 先调用 `get_capabilities_guide(section="local_postproduction")`。
144
+ `export_handoff_pack` 默认仍导出视频剪辑包;设置 `purpose="local_color"`
145
+ 可导出本地调色交接包,只有镜头图片时加 `media_type="images"`,无需生成视频。
146
+ `color_goal` 可填写客户确认的色彩目标和保护项,不会发送给生成模型。
147
+
148
+ 将清单保存为 `manifest.json`,用 `save_handoff_toolchain` 保存工具链后运行:
149
+
150
+ ```sh
151
+ python3 fetch_pack.py manifest.json -o ./pack
152
+ ```
153
+
154
+ 本地调色使用新的空目录,输出源素材、`source-manifest.json`、`checksums.json`、
155
+ `POSTPRODUCTION.md` 和 `comparison.html`。在对比页选择本地参考图及处理结果,
156
+ 文件只在浏览器中预览。交接不收费生成、不上传第三方、不回填平台。
157
+ 外部工具收费和上传须另行授权;导出包关闭自动 LUT,平台项目设定保持不变。
158
+ 已有素材是否曾调色需要核对来源,不能把导出等同于恢复未调色原件。
package/SKILL.md CHANGED
@@ -121,6 +121,17 @@ Don't ask the user at every step. Sort work into three tiers:
121
121
  `video_style_prompt`) are all free and the foundation that steers every later
122
122
  generation. Set them up front via `create_drama` / `update_project_settings`
123
123
  — don't build an empty shell, or all downstream generation drifts.
124
+ **Honor explicit creative requirements.** `creative_contract` on
125
+ `create_drama` / `update_project_settings` accepts `version: 1`, optional
126
+ `speech` (`unspecified` / `forbidden` / `required`), `music`
127
+ (`unspecified` / `forbidden`), and `pacing` (`unspecified` / `quiet`).
128
+ Set these only from the customer's explicit choice, never infer them from
129
+ genre. Omission preserves settings; `null` clears; an object replaces the
130
+ whole contract. Forbidden speech covers dialogue, narration and singing;
131
+ environmental/action sounds remain allowed. Forbidden music applies to
132
+ video output and final composition; customers may still generate, save and
133
+ select music assets. Music already mixed into native clip audio needs
134
+ separate handling. Quiet pacing suppresses generic climax/valley advice.
124
135
  **Set the structured era contract for anything non-contemporary.**
125
136
  `setting_brief` is free text: it supplies background but cannot hold the
126
137
  frame. The one mechanism that outranks the visual lock is the structured
@@ -1059,6 +1070,22 @@ to close") tells the vendor to fit that entire sequence into each 3-second shot.
1059
1070
  changed outside in `manifest`. `get_final_cut` then returns it as `delivery`
1060
1071
  next to the platform's own `download_url`; `list_deliveries` /
1061
1072
  `set_current_delivery` manage versions.
1073
+ - **Local color grading / third-party AI finishing**: call `export_handoff_pack`
1074
+ with `purpose="local_color"`; for existing still frames use `media_type="images"`
1075
+ (no video generation required). Save the result as `manifest.json`, obtain the
1076
+ toolchain, and run `python3 fetch_pack.py manifest.json -o ./pack` in a new empty
1077
+ directory on the customer's computer. Read `POSTPRODUCTION.md` and use
1078
+ `comparison.html` to select local references and processed versions. Selection
1079
+ stays in the browser; no third-party upload or billed generation is performed.
1080
+ The package disables automatic LUT application, preserving the project LUT as
1081
+ metadata without changing project settings. Existing uploaded frames may already
1082
+ be graded: an export cannot undo prior processing. Preserve originals and save
1083
+ edits separately. Color scripts describe targets and protected regions, not a
1084
+ guarantee of precise color control. Obtain authorization before third-party
1085
+ uploads or paid processing. Inspect actual results (video also needs flicker,
1086
+ duration, frame-rate and audio checks); downloading is not finishing or approval.
1087
+ This path does not automatically replace platform assets. Image-only packs must
1088
+ not be passed to the video assembler.
1062
1089
  - **Assemble it yourself**: `export_handoff_pack`, `get_handoff_toolchain` —
1063
1090
  download the per-shot raw clips, dialogue tracks, SFX, BGM and subtitles, then
1064
1091
  decide transitions and assemble the cut on your own side. Use this instead of
@@ -26,6 +26,9 @@ fetch_pack.py — 把 export_handoff_pack 返回的 manifest 拉成一个本地
26
26
  只用标准库,无第三方依赖。
27
27
  """
28
28
  import argparse
29
+ import hashlib
30
+ import html
31
+ from urllib.parse import urlparse
29
32
  import json
30
33
  import os
31
34
  import sys
@@ -82,6 +85,57 @@ def write_srt(path: str, cues) -> int:
82
85
  return n
83
86
 
84
87
 
88
+
89
+ def write_postproduction(out, manifest, jobs):
90
+ """仅写本地交接说明与人工对比页,不执行调色、不上传素材。"""
91
+ post = manifest["postproduction"]
92
+ post["status"] = "downloaded_not_processed"
93
+ lines = ["# 本地后期交接", "", "状态:素材已下载,尚未调色或验收。", "",
94
+ "## 客户目标(仅作素材说明)", "", post.get("goal") or "待客户确认", "",
95
+ "## 操作步骤", ""]
96
+ lines.extend(f"{i+1}. {step}" for i, step in enumerate(post.get("steps", [])))
97
+ lines.extend(["", post.get("assembly_policy", ""), "", post.get("limitations", ""), "",
98
+ "源文件保持不变,处理结果另存。参考图只表达意图,批准样张才是本项目调色基准。",
99
+ "对比后记录采用文件、配方/软件版本及未完成检查;此页面不会自动批准或回传。"])
100
+ if post.get("missing_shots"):
101
+ lines.append("缺少图片的镜号:" + ", ".join(map(str, post["missing_shots"])))
102
+ with open(os.path.join(out, "POSTPRODUCTION.md"), "w", encoding="utf-8") as f:
103
+ f.write("\n".join(lines))
104
+ hashes = {}
105
+ for _, rel in jobs:
106
+ digest = hashlib.sha256()
107
+ with open(os.path.join(out, rel), "rb") as f:
108
+ for chunk in iter(lambda: f.read(1 << 20), b""):
109
+ digest.update(chunk)
110
+ hashes[rel] = digest.hexdigest()
111
+ with open(os.path.join(out, "checksums.json"), "w", encoding="utf-8") as f:
112
+ json.dump(hashes, f, ensure_ascii=False, indent=2)
113
+ page = ['''<!doctype html><meta charset="utf-8"><title>本地后期对比</title>
114
+ <style>body{background:#16191d;color:#eee;font:16px sans-serif;margin:24px}.pair{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:30px}img,video{max-width:100%;max-height:65vh}input{margin:12px}pre{white-space:pre-wrap}a{color:#ade}</style>
115
+ <h1>本地后期对比</h1><p>源素材与处理后版本人工对比;选择文件仅在浏览器本地预览,不会上传。未选择结果不代表已完成。</p>
116
+ <p>参考图:<input type="file" accept="image/*" data-target="reference"></p><img id="reference">
117
+ <pre>''', html.escape(post.get("goal") or "目标待确认"), "</pre>"]
118
+ count = 0
119
+ for shot in manifest.get("shots", []):
120
+ media = list(shot.get("images") or [])
121
+ if (shot.get("clip") or {}).get("file"):
122
+ media.append(shot["clip"])
123
+ for item in media:
124
+ rel = item.get("file")
125
+ if not rel:
126
+ continue
127
+ count += 1
128
+ tag = "video" if item is shot.get("clip") else "img"
129
+ controls = " controls" if tag == "video" else ""
130
+ accept = "video/*" if tag == "video" else "image/*"
131
+ label = html.escape(f"镜头 {shot['shot_number']} {item.get('frame_type', 'video')}")
132
+ page.append(f'<h2>{label}</h2><div class="pair"><div><p>源素材</p><{tag}{controls} src="{html.escape(rel, quote=True)}"></{tag}></div>'
133
+ f'<div><p>处理后(待选择)</p><input type="file" accept="{accept}" data-target="result-{count}"><{tag}{controls} id="result-{count}"></{tag}></div></div>')
134
+ page.append('''<script>document.querySelectorAll('input[data-target]').forEach(input=>input.addEventListener('change',()=>{const f=input.files[0];if(!f)return;const el=document.getElementById(input.dataset.target);if(el.dataset.url)URL.revokeObjectURL(el.dataset.url);el.dataset.url=URL.createObjectURL(f);el.src=el.dataset.url}));</script>''')
135
+ with open(os.path.join(out, "comparison.html"), "w", encoding="utf-8") as f:
136
+ f.write("".join(page))
137
+
138
+
85
139
  def main():
86
140
  ap = argparse.ArgumentParser()
87
141
  ap.add_argument("manifest", help="manifest.json 路径,或 - 表示从 stdin 读")
@@ -98,12 +152,29 @@ def main():
98
152
  sys.exit(f"不认识的 manifest_version: {m.get('manifest_version')!r},拒绝猜测")
99
153
 
100
154
  out = a.outdir
155
+ local_color = (m.get("postproduction") or {}).get("workflow") == "local_color"
156
+ if local_color and os.path.exists(out) and os.listdir(out):
157
+ sys.exit("本地后期请使用新的空目录,避免覆盖源素材或误用旧版本缓存")
101
158
  os.makedirs(out, exist_ok=True)
159
+ if local_color:
160
+ with open(os.path.join(out, "source-manifest.json"), "w", encoding="utf-8") as f:
161
+ json.dump(m, f, ensure_ascii=False, indent=2)
162
+ # 再次兜底:本地后期不自动烘焙项目 LUT。
163
+ m.setdefault("render_target", {})["color_lut"] = None
102
164
  jobs = [] # (url, 包内相对路径)
103
165
 
104
166
  for i, sh in enumerate(m.get("shots", [])):
105
167
  n = int(sh["shot_number"])
106
168
  tag = f"{n:03d}"
169
+ for k, image in enumerate(sh.get("images") or []):
170
+ if not image.get("url"):
171
+ continue
172
+ ext = os.path.splitext(urlparse(image["url"]).path)[1].lower()
173
+ if ext not in (".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"):
174
+ ext = ".img"
175
+ rel = f"originals/shot_{tag}_{k}{ext}"
176
+ jobs.append((image.pop("url"), rel))
177
+ image["file"] = rel
107
178
  clip = sh.get("clip") or {}
108
179
  if clip.get("url"):
109
180
  rel = f"clips/shot_{tag}.mp4"
@@ -161,6 +232,9 @@ def main():
161
232
  with ThreadPoolExecutor(max_workers=max(1, a.jobs)) as ex:
162
233
  list(ex.map(one, jobs))
163
234
 
235
+ if local_color and not errors:
236
+ write_postproduction(out, m, jobs)
237
+
164
238
  with open(os.path.join(out, "manifest.json"), "w", encoding="utf-8") as f:
165
239
  json.dump(m, f, ensure_ascii=False, indent=2)
166
240
 
@@ -171,6 +245,10 @@ def main():
171
245
  print("URL 有有效期,过期就重新调 export_handoff_pack 拿新的 manifest。", file=sys.stderr)
172
246
  sys.exit(1)
173
247
 
248
+ if local_color:
249
+ print("素材下载完成,尚未调色。下一步:阅读 POSTPRODUCTION.md,在本地工具处理后打开 comparison.html 对比。")
250
+ return
251
+
174
252
  print(f"完成。下一步:\n python3 compile_timeline.py {out}\n ./assemble.sh {out} out.mp4")
175
253
 
176
254
 
@@ -322,6 +322,7 @@ export const COMMON_REQUESTS = [
322
322
  { customer_says: '预算多少 / 怎么更便宜', do: '各步 quote_* + `get_cost_estimate`;降本:hailuo-3(约 1/3)或 wan3.0(约 4 折,仅风格化/空镜/产品镜,写实真人绝不选)+ 草稿期低分辨率。' },
323
323
  { customer_says: '镜头之间接不上 / 画面跳 / 衔接不顺', do: '`get_storyboards` 看 seam_state:unchained / broken 才要修(unknown 是系统没判、intended 是有意切换,都别动)。还没出视频:整集 `chain_frames`(先 dry_run),单道同景别 `chain_shot_from_prev`(免费)、景别有变化 `quote_chain_shot_from` → `chain_shot_from` mode=reframe。上一镜视频已出好、不想动本镜首帧:`quote_align_prev_shot` → `align_prev_shot`(重生上一镜视频)。改了首帧的镜视频要重生。' },
324
324
  { customer_says: '成片里话没说完 / 切太快', do: '先 `scan_dialogue_coverage` / `scan_intra_shot_cuts` 定病因,再按 qa_tools 的 then 修;别默认去加长镜头。' },
325
+ { customer_says: '下载原图或视频到本地调色、用第三方 AI 后期', do: '`export_handoff_pack` purpose=local_color;只有图片传 media_type=images;按 local_postproduction.color_handoff 执行。' },
325
326
  { customer_says: '我想自己剪', do: '`export_handoff_pack` + `get_handoff_toolchain`,不走 `compose_episode`。' },
326
327
  { customer_says: '要配音 / 不要视频原声', do: '`update_project_settings` use_clip_audio=false → `assign_voices` → `generate_tts` → `compose_episode`。' },
327
328
  ];
@@ -331,12 +332,21 @@ export const COMMON_REQUESTS = [
331
332
  * 不单开一个 guide 工具:入口越多,agent 越要先猜「该调哪个」。
332
333
  */
333
334
  export const LOCAL_POSTPRODUCTION = {
334
- when: '客户要把镜头下载到自己电脑上剪、配乐、烧字幕、优化转场、做字卡、补旁白时走这条;' +
335
+ when: '客户要把镜头下载到自己电脑上调色、使用第三方 AI 后期、剪辑、配乐、烧字幕、优化转场、做字卡、补旁白时走这条;' +
335
336
  '想让平台代拼并要平台级质量闸(终拼预检 / 音画等长 / 响度母带)用 `compose_episode`。两条二选一。',
336
337
  where_it_runs: '★脚本与 ffmpeg 全部跑在**客户自己的机器**上,平台只发素材 URL 与工具链源码。' +
337
338
  'manifest 里是远程 URL,不是服务器上的本地路径;`save_handoff_toolchain` 的 dir 也是客户机器上的绝对路径。' +
338
339
  '别把服务器路径当成客户电脑上的路径,也别替客户声称"已经在本地跑完了"——真正执行的是客户那侧。',
339
340
  tools: ['export_handoff_pack', 'save_handoff_toolchain', 'get_handoff_toolchain'],
341
+ color_handoff: {
342
+ entry: 'export_handoff_pack(purpose=local_color);图片用 media_type=images,无需出视频。color_goal 填客户目标和保护项。',
343
+ download: 'save_handoff_toolchain → python3 fetch_pack.py manifest.json -o ./pack;得到源素材、POSTPRODUCTION.md、comparison.html。',
344
+ reference: '客户在本地选择参考图,批准本项目试调样张后再扩到其他镜头;色彩脚本作为后期交接说明,不承诺精准控制。',
345
+ tools: '客户选择第三方 AI 或传统调色软件;仅调色优先非生成式处理,重绘需核对身份与构图变化。上传或收费须明确授权。',
346
+ verify: '保留源文件,另存版本;逐镜检查肤色、高光与蒙版,视频检查闪烁、时长与音轨;下载与退出码不代表验收。',
347
+ lut: 'local_color 包禁用自动 LUT,仅记录平台原设置,避免重复调色;不更改平台项目。',
348
+ delivery: '本地交付并记录采用版本;不自动回传覆盖,不承诺尚未支持的回传流程。',
349
+ },
340
350
  stages: [
341
351
  {
342
352
  stage: '1 对需求',
@@ -11,7 +11,7 @@ export function registerGuideTools(server) {
11
11
  'pipeline(10 步产线每步的工具、免费还是收费、哪道审查闸)、review_gates(三道免费硬闸规则)、' +
12
12
  'qa_tools(按客户描述的症状选检测工具与修法)、optional_boosts(可选增强及何时做)、billing(报价确认与免费族)、' +
13
13
  'common_requests(客户常见原话 → 该做什么)、' +
14
- 'local_postproduction(★客户要把镜头下载到**自己电脑**上剪 / 配乐 / 烧字幕 / 优化转场 / 补旁白时的完整流程:' +
14
+ 'local_postproduction(★客户要把镜头下载到**自己电脑**上调色 / 第三方 AI 后期 / 剪辑 / 配乐 / 烧字幕 / 优化转场 / 补旁白时的完整流程:' +
15
15
  '阶段顺序、执行位置、逐镜逐句怎么核对声音、什么才算验收完成)、' +
16
16
  'model_guide(★图片模型与视频引擎的实测优劣势、价格、选型决策步骤与推荐组合——建剧选型、客户问「哪个模型好 / 为什么手画坏了 / 怎么省钱」时先看)。传 section 只取一段。' +
17
17
  '★工具描述回答"这个工具做什么",本工具回答"什么情况下该用哪个"——客户交来的是成品分镜表却被 set_script→rewrite_script 改写成散文,就是没先看这张表。', {
@@ -0,0 +1,48 @@
1
+ /** 只读交接:保留源素材,不调用生成、上传或平台回填。 */
2
+ export function localColorHandoff(manifest, goal = '', missingShots = []) {
3
+ return {
4
+ ...manifest,
5
+ render_target: { ...manifest.render_target, color_lut: null },
6
+ postproduction: {
7
+ version: 1,
8
+ workflow: 'local_color',
9
+ status: 'not_processed',
10
+ goal,
11
+ platform_lut: manifest.render_target?.color_lut ?? null,
12
+ missing_shots: missingShots,
13
+ steps: [
14
+ '将本清单保存为 manifest.json,用 save_handoff_toolchain 保存工具链,再运行 python3 fetch_pack.py manifest.json -o ./pack;使用新的输出目录。',
15
+ '下载后保留 originals;核对色彩配置与来源,未标记的配置不能凭空认定。外部上传素材可能已经调色,本次导出不会撤销既有处理。',
16
+ '从客户确认的参考图确定目标与保护项;色彩脚本只作后期说明,不提高生成提示词优先级。参考图文件由客户在本地选择。',
17
+ '先对代表性素材试调,保存轻、中、强版本到独立目录。优先可撤销的调色;生成式重绘可能改变身份、衣料与构图。',
18
+ '按客户选择使用本地软件或第三方 AI;上传第三方、收费调用均须先说明数据去向和费用并取得授权。没有连接或本地执行能力时只交付操作说明,不声称已经处理。',
19
+ '打开 comparison.html 选择本地参考图和处理后文件逐镜对比;选择文件只在浏览器本地预览。核对肤色、高光、暗部、蒙版边缘;视频再查闪烁、时长、帧率、音轨和同步。',
20
+ '人工确认后交付新版本与检查结果;本工具不自动上传或覆盖平台资产。需要回传时另行确认支持的入口。',
21
+ ],
22
+ assembly_policy: '本包关闭平台 LUT 自动应用,避免外部调色叠加。平台项目设定不变;平台 LUT 仅作为记录保留。未处理源片不得冒充已调色交付。',
23
+ limitations: '下载成功不是调色完成;比较页不自动判定质量;仅有图片时不能装配视频;视频需继续遵守逐镜 voice_track、裁剪和字幕时间基准。',
24
+ },
25
+ };
26
+ }
27
+ export function imageHandoff(episodeId, rows, goal = '') {
28
+ if (!Array.isArray(rows))
29
+ throw new Error('分镜返回格式异常,无法导出图片清单');
30
+ const missing = [];
31
+ const shots = rows.flatMap((row) => {
32
+ const images = [];
33
+ for (const frame of ['first', 'last']) {
34
+ const url = row[`${frame}_frame_image`];
35
+ if (!url || images.some(image => image.url === url))
36
+ continue;
37
+ images.push({ frame_type: `${frame}_frame`, url, source: row[`${frame}_frame_source`] ?? 'unknown', prior_grade: 'unknown' });
38
+ }
39
+ if (!images.length) {
40
+ missing.push(row.storyboard_number);
41
+ return [];
42
+ }
43
+ return [{ storyboard_id: row.id, shot_number: row.storyboard_number, scene_id: row.scene_id ?? null, images }];
44
+ });
45
+ if (!shots.length)
46
+ throw new Error('本集没有可导出的现有镜头图片;未发起生成或扣费');
47
+ return localColorHandoff({ manifest_version: '0.1', media_type: 'images', episode_id: episodeId, shots, render_target: { color_lut: null } }, goal, missing);
48
+ }
@@ -11,6 +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
15
  import { readFileSync, writeFileSync, chmodSync, mkdirSync } from 'node:fs';
15
16
  import { tmpdir } from 'node:os';
16
17
  import { dirname, join } from 'node:path';
@@ -20,10 +21,12 @@ const TOOLCHAIN_USAGE = [
20
21
  '1. 三个脚本放同一目录(assemble.sh 需可执行位)。',
21
22
  '2. 把 export_handoff_pack 的返回体存成 manifest.json。',
22
23
  '3. python3 fetch_pack.py manifest.json -o ./pack # 下载素材 + 内联字幕落成逐镜 SRT + 改写成本地路径',
24
+ '本地调色包:下载后读 POSTPRODUCTION.md 并打开 comparison.html;图片包不执行视频装配。',
23
25
  '4. python3 compile_timeline.py ./pack [--transitions plan.json] # 展开时间轴',
24
26
  '5. ./assemble.sh ./pack out.mp4 [plan.json] # 装配成片',
25
27
  ];
26
- const TOOLCHAIN_NOTE = 'assemble.sh 会先自检 ffmpeg 是否带 libass;不带则字幕以软字幕轨输出而非烧录(竖屏发布必须烧录)。' +
28
+ const TOOLCHAIN_NOTE = '本地调色包不自动套平台 LUT,只下载与交接,未执行调色。' +
29
+ 'assemble.sh 会先自检 ffmpeg 是否带 libass;不带则字幕以软字幕轨输出而非烧录(竖屏发布必须烧录)。' +
27
30
  '包里若带 render_target.color_lut,fetch_pack.py 会把调色查找表一并下载,assemble.sh 自动施加——' +
28
31
  '不施加的话你的成片与平台成片会有色差。';
29
32
  /**
@@ -218,6 +221,12 @@ const VIDEO_ENGINES = ['seedance-2.5', 'hailuo-3', 'wan3.0', 'wan3.0-prime'];
218
221
  // 后端内部 PUT /dramas 已接受写库,facade 白名单 Wave3 已放行(produce-create-fields.ts)。
219
222
  // 不含内部产线/成本开关(strict_mode/best_of_n/shoppable/budget_points 等,需产品决策)。
220
223
  const PROJECT_SETTINGS_FIELDS = {
224
+ creative_contract: z.object({
225
+ version: z.literal(1),
226
+ speech: z.enum(['unspecified', 'forbidden', 'required']).optional(),
227
+ music: z.enum(['unspecified', 'forbidden']).optional(),
228
+ pacing: z.enum(['unspecified', 'quiet']).optional(),
229
+ }).strict().nullable().optional().describe('客户明确创作约束:speech=forbidden 禁止对白/旁白/演唱,music=forbidden 仅禁止本片输出混入配乐(允许生成/保存/选择音乐素材),pacing=quiet 保持平静节奏。不推断;整对象替换,null 清除,省略保持。环境声/音效不受禁止人声影响。'),
221
230
  // drama 级图片模型(整剧统一画风):默认 ChatGPT Image 2.5 Flare(v0.9.1558)
222
231
  image_model: z.string().optional().describe('图片模型(★drama级·整剧统一画风·默认 ChatGPT Image 2.5 Flare)。可选:' +
223
232
  'gpt-image-2.5-flare(默认·基础11点+每张参考图18点)/gpt-image-2.5-sunburst(同价·中文字形与细节更准·慢约5秒)/' +
@@ -1763,6 +1772,9 @@ export function registerProduceTools(server, client) {
1763
1772
  // 你自己决定转场、自己拼」。两条路都不丢客户的对白/音效/配乐/字幕成果。
1764
1773
  server.tool('export_handoff_pack', '导出本集「素材交接包」清单:逐镜裸片 + 对白音轨 + 音效 + 配乐 + 字幕的可下载 URL,' +
1765
1774
  '交给你在**自己那边**完成转场决策、拼接、混音、烧字幕——平台不参与终拼。免费,零扣费。\n' +
1775
+ '★本地调色/第三方 AI 后期:purpose=local_color;只有图片时传 media_type=images,无需生成视频。' +
1776
+ '本模式保留源文件、关闭导出包的自动 LUT、附带后期说明和本地对比页流程;不上传第三方、不收费生成、不回填平台。' +
1777
+ '图片来源可能已有处理,不能把所有导出图承诺为未调色原片。\n' +
1766
1778
  '\n【推荐流程】① 调本工具拿 manifest;② 按 clips[].url 把裸片下载到本地;' +
1767
1779
  '③ 你自己看片判断每个接缝该用什么转场(manifest 给了 scene_boundary 场景边界作判据),' +
1768
1780
  '写一份 plan.json;④ 用 get_handoff_toolchain 拿到 compile_timeline.py 展开时间轴、' +
@@ -1780,8 +1792,19 @@ export function registerProduceTools(server, client) {
1780
1792
  '首尾形变帧一起拼进去。\n' +
1781
1793
  '③ 字幕 cue、dialogue_audio.offset_ms、sfx[].offset_ms 的基准都是「该镜 trim 之后的第 0 毫秒」,' +
1782
1794
  '不是成片绝对时间。你加多少重叠转场都不用改它们——交给 compile_timeline.py 展开,别手算累加。\n' +
1783
- '\n想让平台代拼、要平台级质量闸(终拼预检/音画等长/响度母带),改用 compose_episode。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => {
1795
+ '\n想让平台代拼、要平台级质量闸(终拼预检/音画等长/响度母带),改用 compose_episode。', {
1796
+ episode_id: z.number().int().positive(),
1797
+ media_type: z.enum(['videos', 'images']).optional().describe('默认 videos;images 导出现有首尾帧并自动走本地后期流程'),
1798
+ purpose: z.enum(['assembly', 'local_color']).optional().describe('默认 assembly 保持平台装配;local_color 交接本地调色,关闭本包自动 LUT'),
1799
+ color_goal: z.string().max(4000).optional().describe('客户的后期目标与保护项;作为说明保存,不发送给模型'),
1800
+ }, async ({ episode_id, media_type, purpose, color_goal }) => {
1801
+ if (media_type === 'images') {
1802
+ const rows = await client.produceGet(`/episodes/${episode_id}/storyboards`);
1803
+ return jsonResult(imageHandoff(episode_id, rows, color_goal));
1804
+ }
1784
1805
  const manifest = await client.produceGet(`/episodes/${episode_id}/handoff-pack`);
1806
+ if (purpose === 'local_color')
1807
+ return jsonResult(localColorHandoff(manifest, color_goal));
1785
1808
  return jsonResult({
1786
1809
  ...manifest,
1787
1810
  assembly_guide: {
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.103",
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.103 tool surface (operationIds match MCP tool names 1:1)."
5
+ "version": "0.1.105",
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.105 tool surface (operationIds match MCP tool names 1:1)."
7
7
  },
8
8
  "servers": [
9
9
  {
@@ -722,6 +722,41 @@
722
722
  "director_style": {
723
723
  "type": "string"
724
724
  },
725
+ "creative_contract": {
726
+ "type": "object",
727
+ "properties": {
728
+ "version": {
729
+ "type": "number",
730
+ "const": 1
731
+ },
732
+ "speech": {
733
+ "type": "string",
734
+ "enum": [
735
+ "unspecified",
736
+ "forbidden",
737
+ "required"
738
+ ]
739
+ },
740
+ "music": {
741
+ "type": "string",
742
+ "enum": [
743
+ "unspecified",
744
+ "forbidden"
745
+ ]
746
+ },
747
+ "pacing": {
748
+ "type": "string",
749
+ "enum": [
750
+ "unspecified",
751
+ "quiet"
752
+ ]
753
+ }
754
+ },
755
+ "required": [
756
+ "version"
757
+ ],
758
+ "additionalProperties": false
759
+ },
725
760
  "image_model": {
726
761
  "type": "string"
727
762
  },
@@ -1022,6 +1057,41 @@
1022
1057
  "mv_audio_url": {
1023
1058
  "type": "string"
1024
1059
  },
1060
+ "creative_contract": {
1061
+ "type": "object",
1062
+ "properties": {
1063
+ "version": {
1064
+ "type": "number",
1065
+ "const": 1
1066
+ },
1067
+ "speech": {
1068
+ "type": "string",
1069
+ "enum": [
1070
+ "unspecified",
1071
+ "forbidden",
1072
+ "required"
1073
+ ]
1074
+ },
1075
+ "music": {
1076
+ "type": "string",
1077
+ "enum": [
1078
+ "unspecified",
1079
+ "forbidden"
1080
+ ]
1081
+ },
1082
+ "pacing": {
1083
+ "type": "string",
1084
+ "enum": [
1085
+ "unspecified",
1086
+ "quiet"
1087
+ ]
1088
+ }
1089
+ },
1090
+ "required": [
1091
+ "version"
1092
+ ],
1093
+ "additionalProperties": false
1094
+ },
1025
1095
  "image_model": {
1026
1096
  "type": "string"
1027
1097
  },
@@ -4625,7 +4695,7 @@
4625
4695
  "get": {
4626
4696
  "operationId": "export_handoff_pack",
4627
4697
  "summary": "导出本集「素材交接包」清单:逐镜裸片 + 对白音轨 + 音效 + 配乐 + 字幕的可下载 URL,交给你在**自己那边**完成转场决策、拼接、混音、烧字幕——平台不参与终拼",
4628
- "description": "导出本集「素材交接包」清单:逐镜裸片 + 对白音轨 + 音效 + 配乐 + 字幕的可下载 URL,交给你在**自己那边**完成转场决策、拼接、混音、烧字幕——平台不参与终拼。免费,零扣费。\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。",
4698
+ "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。",
4629
4699
  "tags": [
4630
4700
  "episodes"
4631
4701
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starreel/mcp",
3
- "version": "0.1.103",
3
+ "version": "0.1.105",
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",
@@ -22,7 +22,8 @@
22
22
  "dev": "tsx src/index.ts",
23
23
  "typecheck": "tsc --noEmit",
24
24
  "prepublishOnly": "npm run build && node scripts/release-check.mjs",
25
- "generate:openapi": "tsx scripts/generate-openapi.ts"
25
+ "generate:openapi": "tsx scripts/generate-openapi.ts",
26
+ "test": "node --test test/*.test.mjs"
26
27
  },
27
28
  "engines": {
28
29
  "node": ">=18"
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.103",
5
+ "version": "0.1.105",
6
6
  "websiteUrl": "https://starreel.ai",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@starreel/mcp",
11
- "version": "0.1.103",
11
+ "version": "0.1.105",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  },