@starreel/mcp 0.1.2 → 0.1.4

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/dist/client.js CHANGED
@@ -11,7 +11,16 @@
11
11
  * 任何厂商直链字段)。
12
12
  */
13
13
  import { createReadStream, statSync } from 'node:fs';
14
+ import { basename } from 'node:path';
14
15
  import { Readable } from 'node:stream';
16
+ /** 扩展名 → MIME(presign 需要 content_type)。 */
17
+ const MIME_BY_EXT = {
18
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp',
19
+ '.gif': 'image/gif', '.bmp': 'image/bmp',
20
+ '.mp4': 'video/mp4', '.mov': 'video/quicktime',
21
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.m4a': 'audio/mp4', '.aac': 'audio/aac',
22
+ '.ogg': 'audio/ogg', '.flac': 'audio/flac',
23
+ };
15
24
  const AUTH_BASE = process.env.STARREEL_AUTH_BASE ?? 'https://api.shortreelai.com';
16
25
  const LOC_BASE = process.env.STARREEL_LOC_BASE ?? 'https://translator.starreel.ai';
17
26
  export class StarReelClient {
@@ -130,11 +139,11 @@ export class StarReelClient {
130
139
  * 把本地文件流式 PUT 到 COS 预签名 URL(字节不经我们任何服务器)。
131
140
  * Content-Length 必带 —— 预签名 PUT 对 chunked 编码不友好。
132
141
  */
133
- async uploadFile(uploadUrl, filePath) {
142
+ async uploadFile(uploadUrl, filePath, extraHeaders = {}) {
134
143
  const size = statSync(filePath).size;
135
144
  const res = await fetch(uploadUrl, {
136
145
  method: 'PUT',
137
- headers: { 'content-length': String(size) },
146
+ headers: { 'content-length': String(size), ...extraHeaders },
138
147
  body: Readable.toWeb(createReadStream(filePath)),
139
148
  // Node fetch(undici) 流式请求体必须显式声明半双工
140
149
  duplex: 'half',
@@ -142,4 +151,19 @@ export class StarReelClient {
142
151
  if (!res.ok)
143
152
  throw new Error(`upload failed: HTTP ${res.status} (${(await res.text()).slice(0, 200)})`);
144
153
  }
154
+ /**
155
+ * 本地文件 → 我方 COS:presign(门面 /upload-url)→ 直传字节到预签名 URL → 返回 public_url。
156
+ * 字节不经我们的业务服务器,只走 COS。kind: image(默认)/video/audio。
157
+ */
158
+ async uploadLocalFile(filePath, kind = 'image') {
159
+ const size = statSync(filePath).size;
160
+ const filename = basename(filePath);
161
+ const ext = (filename.match(/\.[a-z0-9]+$/i)?.[0] || '').toLowerCase();
162
+ const content_type = MIME_BY_EXT[ext] || 'application/octet-stream';
163
+ const res = await this.producePost('/upload-url', { filename, content_type, size, kind });
164
+ // presign 可能要求 PUT 带特定头(content-type 等)——按 presign 返回的 headers 送;
165
+ // 未返回则至少带 content-type(COS 签名常把它纳入)。
166
+ await this.uploadFile(res.put_url, filePath, res.headers ?? { 'content-type': content_type });
167
+ return res.public_url;
168
+ }
145
169
  }
@@ -16,6 +16,25 @@ function jsonResult(data) {
16
16
  }
17
17
  const CONFIRM_HINT = '⚠️ 批量报价确认流程:先调对应的 quote_* 工具,把返回的 estimated_points 原样告诉用户,' +
18
18
  '用户明确同意后,才用返回的 quote_id 调本工具。不要擅自确认。';
19
+ // 完整工作流顺序(照 get_pipeline_status 的 10 步真相走,不要跳步):
20
+ // create_drama → set_script(原始) → rewrite_script(AI改写) → [get_script/edit_rewritten_script 审改]
21
+ // → extract_assets(角色/场景/道具) → generate_character_portraits(定妆图·一致性关键)
22
+ // → quote/generate_storyboards → quote/generate_frames → quote/generate_videos
23
+ // → compose_episode → get_final_cut / get_export
24
+ // 项目设定随时可 update_project_settings;剧目级资产(色彩脚本/动作模板/世界观图/美术圣经)可选增强。
25
+ const WORKFLOW_HINT = '照完整工作流按序推进,别跳步:set_script(原始)→ rewrite_script(AI改写)→ extract_assets →' +
26
+ ' generate_character_portraits → storyboards → frames → videos → compose。用 get_pipeline_status 查进度。';
27
+ const ETHNICITY_CODES = [
28
+ 'east_asian', 'southeast_asian', 'south_asian', 'central_asian',
29
+ 'caucasian', 'middle_eastern', 'african', 'latin_american',
30
+ 'native_american', 'aboriginal_australian', 'mixed', 'custom',
31
+ ];
32
+ /** 把友好 ethnicity 枚举 + note 组装成后端认的结构化族裔锁 { code, note? }。 */
33
+ function buildEthnicityLock(ethnicity, note) {
34
+ if (!ethnicity)
35
+ return undefined;
36
+ return ethnicity === 'custom' ? { code: 'custom', note: note ?? '' } : { code: ethnicity };
37
+ }
19
38
  export function registerProduceTools(server, client) {
20
39
  // ---------- 建剧 ----------
21
40
  // 参数=「项目设定 / 新建项目」页对外能设的全部通用字段(不含内部产线开关)。
@@ -40,30 +59,46 @@ export function registerProduceTools(server, client) {
40
59
  .optional()
41
60
  .describe('世界观 Brief:一段话锁定语言/文化/时代/地点,作为 AI 强制约束(ERA LOCK),' +
42
61
  '影响剧本改写·分镜·所有出图。不设则按剧本语言与文化自动推断。'),
43
- ethnicity: z
44
- .enum([
45
- 'east_asian', 'southeast_asian', 'south_asian', 'central_asian',
46
- 'caucasian', 'middle_eastern', 'african', 'latin_american',
47
- 'native_american', 'aboriginal_australian', 'mixed', 'custom',
48
- ])
49
- .optional()
62
+ ethnicity: z.enum(ETHNICITY_CODES).optional()
50
63
  .describe('全剧角色族裔锁(单一真相源)。不设=按剧本自动推断;custom 时用 ethnicity_note 写自由描述'),
51
64
  ethnicity_note: z.string().optional().describe("ethnicity='custom' 时的自由文本(如 北欧/波斯);其余取值忽略"),
65
+ project_type: z.enum(['drama', 'ad', 'mv', 'brand_film']).optional()
66
+ .describe('项目类型(默认 drama)。ad=广告(改写走 ad_script_rewriter);mv=音乐(走歌词→故事→剧本子流程);brand_film=品牌微电影(默认16:9)'),
67
+ rewrite_mode: z.enum(['standard', 'director']).optional().describe('AI改写深度:standard 或 director(导演级)'),
68
+ director_style: z.string().optional().describe('导演风格包 key'),
52
69
  }, async (args) => {
53
70
  const { ethnicity, ethnicity_note, ...rest } = args;
54
71
  const body = { ...rest };
55
- // 结构化族裔锁:后端认 { code, note? };custom 无 note 派生不出锚,这里如实带上。
56
- if (ethnicity) {
57
- body.ethnicity_lock =
58
- ethnicity === 'custom' ? { code: 'custom', note: ethnicity_note ?? '' } : { code: ethnicity };
59
- }
72
+ const lock = buildEthnicityLock(ethnicity, ethnicity_note);
73
+ if (lock)
74
+ body.ethnicity_lock = lock;
60
75
  return jsonResult(await client.producePost('/dramas', body));
61
76
  });
62
- // ---------- 灌本 ----------
63
- server.tool('set_script', '给某一集设置可拍剧本文本(拆镜的输入)。免费。', {
77
+ // ---------- 灌本(原始内容) ----------
78
+ server.tool('set_script', '给某一集设置**原始剧本**(content)。这是 AI 改写的输入,不是最终可拍稿。免费。' +
79
+ '设完调 rewrite_script 做 AI 改写。' + WORKFLOW_HINT, {
80
+ episode_id: z.number().int().positive(),
81
+ script: z.string().min(1).describe('该集的原始剧本文本(原稿)'),
82
+ }, async ({ episode_id, script }) => jsonResult(await client.producePut(`/episodes/${episode_id}/script`, { content: script })));
83
+ // ---------- AI 改写 / 审阅 / 改稿 ----------
84
+ server.tool('rewrite_script', 'AI 改写:把原始剧本改写成可拍稿(读 content → 写 script_content)。按项目类型自动选改写 agent' +
85
+ '(广告走 ad 改写;MV 不走标准改写会被拦)。后台异步(分钟级),文本步按 token 后付、不欠费,无需报价。' +
86
+ '完成后用 get_script 审阅、edit_rewritten_script 改稿。' + WORKFLOW_HINT, { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/rewrite`)));
87
+ server.tool('get_script', '读某一集的原始内容 + AI 改写后的可拍稿 + 改写状态(供审阅、决定是否 edit_rewritten_script)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/script`)));
88
+ server.tool('edit_rewritten_script', '客户改 AI 改写后的可拍稿(写 script_content)。用于人工润色/纠正 AI 改写结果。免费。', {
64
89
  episode_id: z.number().int().positive(),
65
- script: z.string().min(1).describe('该集的完整剧本文本'),
66
- }, async ({ episode_id, script }) => jsonResult(await client.producePut(`/episodes/${episode_id}/script`, { script_content: script })));
90
+ script: z.string().min(1).describe('改好的可拍剧本(覆盖 AI 改写稿)'),
91
+ }, async ({ episode_id, script }) => jsonResult(await client.producePut(`/episodes/${episode_id}/rewritten-script`, { script_content: script })));
92
+ // ---------- 提取(角色/场景/道具) ----------
93
+ server.tool('extract_assets', '从可拍稿提取角色/场景/道具(一次写三表,是下游一致性的地基)。后台异步,文本步后付不欠费。' +
94
+ '前置:已 rewrite_script(或至少 set_script)。' + WORKFLOW_HINT, { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/extract`)));
95
+ // ---------- 完整工作流进度 ----------
96
+ server.tool('get_pipeline_status', '查某一集完整工作流 10 步的进度(script_rewrite/提取/分镜/出图/出视频/合成…各步 done/ready/pending)。' +
97
+ '照它按序推进、不跳步。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/pipeline-status`)));
98
+ // ---------- 角色定妆图(一致性锚) ----------
99
+ server.tool('quote_character_portraits', '报价:给缺定妆图的角色批量出定妆图要多少点。返回 portraits_to_generate、estimated_points、quote_id。零扣费。' +
100
+ '定妆图是身份一致性的锚(缺它角色会漂移),强烈建议出视频前先出。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/portraits/quote`)));
101
+ server.tool('generate_character_portraits', '确认后批量出角色定妆图:后台异步。用 get_pipeline_status/get_storyboards 查进度。' + CONFIRM_HINT, { episode_id: z.number().int().positive(), quote_id: z.string().describe('来自 quote_character_portraits') }, async ({ episode_id, quote_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/portraits/generate`, { quote_id })));
67
102
  // ---------- 拆镜(storyboards) ----------
68
103
  server.tool('quote_storyboards', '报价:把某一集的剧本拆成分镜(storyboards)要多少点。返回 estimated_points 与 quote_id。零扣费。' +
69
104
  '拿到后把点数告诉用户征求同意,再用 quote_id 调 generate_storyboards。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/storyboards/quote`)));
@@ -82,4 +117,123 @@ export function registerProduceTools(server, client) {
82
117
  server.tool('compose_episode', '把某一集所有镜头视频拼成一条成片(终拼)。**免费**(纯拼接,无需报价确认),后台异步。' +
83
118
  '要求本集至少有一个镜有视频;部分就绪会拼出部分成片并在返回里标 partial。发起后用 get_final_cut 轮询。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/compose`)));
84
119
  server.tool('get_final_cut', '查某一集成片状态与下载链接。status=completed 时返回 download_url(我方 COS 直链,可直接下载)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/final-cut`)));
120
+ server.tool('get_export', '查某一集导出/母版状态(成片终拼后的可下载母版)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/export`)));
121
+ // ========== 项目设定(建后可改)==========
122
+ server.tool('update_project_settings', '建剧后修改项目设定(画幅/分辨率/世界观Brief/族裔/题材/改写深度/导演风格/字幕/长镜,及各类型专属字段)。免费。' +
123
+ '⚠️ 世界观 Brief/族裔/画幅等改后,已生成的内容不会自动更新,需重生对应内容。', {
124
+ drama_id: z.number().int().positive(),
125
+ genre: z.string().optional(),
126
+ style: z.string().optional(),
127
+ description: z.string().optional(),
128
+ tags: z.array(z.string()).optional(),
129
+ aspect_ratio: z.enum(['9:16', '16:9', '1:1', '4:5', '4:3', '21:9']).optional(),
130
+ video_resolution: z.enum(['480p', '720p', '1080p', '4k']).optional(),
131
+ setting_brief: z.string().optional().describe('世界观 Brief(ERA LOCK)'),
132
+ ethnicity: z.enum(ETHNICITY_CODES).optional().describe('角色族裔锁'),
133
+ ethnicity_note: z.string().optional(),
134
+ rewrite_mode: z.enum(['standard', 'director']).optional(),
135
+ director_style: z.string().optional(),
136
+ theme_statement: z.string().optional().describe('一句话主题'),
137
+ subtitle_preset: z.string().optional(),
138
+ scene_group_mode: z.boolean().optional().describe('长镜模式(连续动作/电影级长镜)'),
139
+ // 广告专属
140
+ cta_text: z.string().optional(),
141
+ target_duration_s: z.number().optional(),
142
+ brand_voice: z.string().optional(),
143
+ // 品牌微电影专属
144
+ narrative_tone: z.string().optional(),
145
+ story_structure: z.string().optional(),
146
+ end_card_variant: z.string().optional(),
147
+ // MV 专属
148
+ mv_lyrics: z.string().optional(),
149
+ mv_audio_url: z.string().optional(),
150
+ }, async (args) => {
151
+ const { drama_id, ethnicity, ethnicity_note, ...rest } = args;
152
+ const body = { ...rest };
153
+ const lock = buildEthnicityLock(ethnicity, ethnicity_note);
154
+ if (lock)
155
+ body.ethnicity_lock = lock;
156
+ return jsonResult(await client.producePut(`/dramas/${drama_id}`, body));
157
+ });
158
+ // ========== 项目设定页子资源(可选增强,提升一致性/风格锁)==========
159
+ server.tool('generate_world_concept', '生成世界观概念图(固定4格:全景/时代/主场景/色彩)。需 setting_brief ≥30字。图片步,按固定成本后付、不欠费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/world-concept`)));
160
+ server.tool('generate_art_bible', '生成美术圣经(色调/材质/气质,读角色+元数据)。文本步后付不欠费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/art-bible`)));
161
+ server.tool('extract_visual_lock', '从世界观 Brief + 角色抽取「视觉锁定」规则(写 visual_lock,统一出图风格/族裔)。文本步后付不欠费。', {
162
+ drama_id: z.number().int().positive(),
163
+ setting_brief: z.string().optional().describe('可选:临时覆盖世界观原文'),
164
+ }, async ({ drama_id, setting_brief }) => jsonResult(await client.producePost(`/dramas/${drama_id}/visual-lock`, setting_brief ? { setting_brief } : {})));
165
+ server.tool('extract_setting_brief', '从一段世界观原文提炼简洁的 setting_brief(并存回项目)。文本步后付不欠费。', {
166
+ drama_id: z.number().int().positive(),
167
+ raw_text: z.string().min(1).describe('世界观原文'),
168
+ }, async ({ drama_id, raw_text }) => jsonResult(await client.producePost(`/dramas/${drama_id}/extract-setting-brief`, { raw_text })));
169
+ server.tool('generate_video_style', '生成视频风格锁 prompt(读 brief/artBible/visualLock,统一视频生成方向)。文本步后付不欠费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/video-style`)));
170
+ // ========== 剧目级共享资产(可选增强)==========
171
+ server.tool('generate_color_script', '生成剧目色彩脚本(统一全片配色情绪)。需该剧/集已有剧本文本。文本步后付不欠费。', {
172
+ drama_id: z.number().int().positive(),
173
+ episode_id: z.number().int().positive().optional().describe('可选:按某一集生成'),
174
+ }, async ({ drama_id, episode_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/color-script`, episode_id ? { episode_id } : {})));
175
+ server.tool('generate_motion_templates', '从分镜自动抽取动作模板(统一全片运动语言)。**需先有分镜**(先 generate_storyboards)。文本步后付不欠费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/motion-templates`)));
176
+ server.tool('quote_scene_images', '报价:给缺图场景批量出场景图要多少点。返回 images_to_generate、estimated_points、quote_id。零扣费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/scene-images/quote`)));
177
+ server.tool('generate_scene_images', '确认后批量出场景图:后台异步。' + CONFIRM_HINT, { drama_id: z.number().int().positive(), quote_id: z.string().describe('来自 quote_scene_images') }, async ({ drama_id, quote_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/scene-images/generate`, { quote_id })));
178
+ // ========== A · 客户上传自己的图片 ==========
179
+ server.tool('upload_image', '把本地图片上传到我方 COS,返回 image_url(字节直传 COS、不过业务服务器)。' +
180
+ '用于给视频编辑当参考图,或先拿 URL 再登记。免费。', {
181
+ file_path: z.string().describe('本地图片绝对路径(jpg/png/webp)'),
182
+ kind: z.enum(['image', 'video', 'audio']).optional().describe('默认 image;视频编辑的参考视频/音频可选 video/audio'),
183
+ }, async ({ file_path, kind }) => {
184
+ const url = await client.uploadLocalFile(file_path, kind ?? 'image');
185
+ return jsonResult({ image_url: url });
186
+ });
187
+ server.tool('set_character_portrait', '用客户自有图片作为角色定妆图(身份锚,优先级高于 AI 生成;之后 AI 重生默认不覆盖)。' +
188
+ '自动上传本地图 + 登记。免费。', { character_id: z.number().int().positive(), file_path: z.string().describe('本地定妆图路径') }, async ({ character_id, file_path }) => {
189
+ const image_url = await client.uploadLocalFile(file_path, 'image');
190
+ return jsonResult(await client.producePost(`/characters/${character_id}/portrait`, { image_url }));
191
+ });
192
+ server.tool('upload_shot_frame', '用客户自有图片作为某镜的首帧或尾帧(会把该镜已有视频标为过期待重生)。自动上传+登记。免费。', {
193
+ storyboard_id: z.number().int().positive(),
194
+ file_path: z.string().describe('本地帧图路径'),
195
+ frame_type: z.enum(['first_frame', 'last_frame']).optional().describe('默认 first_frame'),
196
+ }, async ({ storyboard_id, file_path, frame_type }) => {
197
+ const image_url = await client.uploadLocalFile(file_path, 'image');
198
+ return jsonResult(await client.producePost(`/storyboards/${storyboard_id}/frame`, { image_url, frame_type: frame_type ?? 'first_frame' }));
199
+ });
200
+ server.tool('upload_scene_image', '用客户自有图片作为某场景的参考图。自动上传+登记。免费。', { scene_id: z.number().int().positive(), file_path: z.string().describe('本地场景图路径') }, async ({ scene_id, file_path }) => {
201
+ const image_url = await client.uploadLocalFile(file_path, 'image');
202
+ return jsonResult(await client.producePost(`/scenes/${scene_id}/image`, { image_url }));
203
+ });
204
+ server.tool('upload_prop_sheet', '用客户自有图片作为某道具的设定图。自动上传+登记。免费。', { prop_id: z.number().int().positive(), file_path: z.string().describe('本地道具图路径') }, async ({ prop_id, file_path }) => {
205
+ const image_url = await client.uploadLocalFile(file_path, 'image');
206
+ return jsonResult(await client.producePost(`/props/${prop_id}/sheet`, { image_url }));
207
+ });
208
+ // ========== B · 视频就地编辑 / 剪辑 ==========
209
+ server.tool('get_edit_capabilities', '查视频编辑能力开关(就地编辑 / 区间替换是否可用)。免费。', {}, async () => jsonResult(await client.produceGet('/videos/edit-capabilities')));
210
+ server.tool('quote_edit_video_shot', '报价:对某镜做就地编辑/区间替换(Seedance 2.5)要多少点。返回 quote_id。零扣费。' +
211
+ '本镜须已有视频。', { storyboard_id: z.number().int().positive() }, async ({ storyboard_id }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/edit/quote`)));
212
+ server.tool('edit_video_shot', '确认后就地编辑某镜视频:按 instruction 改,可带参考图/视频/音频,或用 start_sec/end_sec 做区间替换。' +
213
+ CONFIRM_HINT, {
214
+ storyboard_id: z.number().int().positive(),
215
+ quote_id: z.string().describe('来自 quote_edit_video_shot'),
216
+ instruction: z.string().describe('编辑指令(如"把背景换成夜晚")'),
217
+ reference_image_urls: z.array(z.string()).max(9).optional().describe('参考图 URL(先 upload_image 拿)'),
218
+ reference_video_urls: z.array(z.string()).max(2).optional(),
219
+ reference_audio_urls: z.array(z.string()).max(3).optional(),
220
+ start_sec: z.number().optional().describe('区间替换起点秒'),
221
+ end_sec: z.number().optional().describe('区间替换终点秒'),
222
+ }, async ({ storyboard_id, ...rest }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/edit/generate`, rest)));
223
+ server.tool('quote_regenerate_shot_video', '报价:重生某镜视频要多少点。返回 quote_id。零扣费。', { storyboard_id: z.number().int().positive() }, async ({ storyboard_id }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/regen/quote`)));
224
+ server.tool('regenerate_shot_video', '确认后重生某镜视频(可选新 prompt)。' + CONFIRM_HINT, {
225
+ storyboard_id: z.number().int().positive(),
226
+ quote_id: z.string().describe('来自 quote_regenerate_shot_video'),
227
+ prompt: z.string().optional().describe('可选:覆盖该镜视频 prompt'),
228
+ }, async ({ storyboard_id, quote_id, prompt }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/regen/generate`, prompt ? { quote_id, prompt } : { quote_id })));
229
+ server.tool('split_shot', '把某镜按首尾帧拆成两镜(结构操作)。免费。', { storyboard_id: z.number().int().positive() }, async ({ storyboard_id }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/split`)));
230
+ server.tool('trim_shot', '裁剪某镜时长(in_ms/out_ms)。免费;返回后需 rerender_episode 重拼成片。', {
231
+ storyboard_id: z.number().int().positive(),
232
+ in_ms: z.number().int().min(0).optional(),
233
+ out_ms: z.number().int().min(0).optional(),
234
+ }, async ({ storyboard_id, in_ms, out_ms }) => jsonResult(await client.producePut(`/storyboards/${storyboard_id}/trim`, { in_ms, out_ms })));
235
+ server.tool('rerender_episode', '按当前 timeline 重拼成片(改完镜/裁剪后用)。**免费**(纯 ffmpeg+COS),后台异步。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/re-render`)));
236
+ server.tool('replace_shot_dialogue', '换某镜对白音色/声线(转写+克隆重配)。后台异步,按用量后付不欠费。要求本镜有原声视频+角色声线定妆音。', { storyboard_id: z.number().int().positive() }, async ({ storyboard_id }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/dialogue-replace`)));
237
+ server.tool('generate_bgm', '给整集生成/更换 AI 配乐(按情绪弧线)。后台异步,按用量后付不欠费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/bgm`)));
238
+ server.tool('translate_subtitles', '把某集字幕翻译成第二语言并开启双语。后台异步,按用量后付不欠费。', { episode_id: z.number().int().positive(), lang: z.string().describe('目标语言(如 en/ja/ko)') }, async ({ episode_id, lang }) => jsonResult(await client.producePost(`/episodes/${episode_id}/subtitles/translate`, { lang })));
85
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starreel/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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.2",
5
+ "version": "0.1.4",
6
6
  "websiteUrl": "https://starreel.ai",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@starreel/mcp",
11
- "version": "0.1.2",
11
+ "version": "0.1.4",
12
12
  "transport": { "type": "stdio" },
13
13
  "environmentVariables": [
14
14
  {