@starreel/mcp 0.1.3 → 0.1.5

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 {
@@ -126,15 +135,18 @@ export class StarReelClient {
126
135
  body: JSON.stringify(body),
127
136
  });
128
137
  }
138
+ produceDelete(path) {
139
+ return this.produce(path, { method: 'DELETE' });
140
+ }
129
141
  /**
130
142
  * 把本地文件流式 PUT 到 COS 预签名 URL(字节不经我们任何服务器)。
131
143
  * Content-Length 必带 —— 预签名 PUT 对 chunked 编码不友好。
132
144
  */
133
- async uploadFile(uploadUrl, filePath) {
145
+ async uploadFile(uploadUrl, filePath, extraHeaders = {}) {
134
146
  const size = statSync(filePath).size;
135
147
  const res = await fetch(uploadUrl, {
136
148
  method: 'PUT',
137
- headers: { 'content-length': String(size) },
149
+ headers: { 'content-length': String(size), ...extraHeaders },
138
150
  body: Readable.toWeb(createReadStream(filePath)),
139
151
  // Node fetch(undici) 流式请求体必须显式声明半双工
140
152
  duplex: 'half',
@@ -142,4 +154,19 @@ export class StarReelClient {
142
154
  if (!res.ok)
143
155
  throw new Error(`upload failed: HTTP ${res.status} (${(await res.text()).slice(0, 200)})`);
144
156
  }
157
+ /**
158
+ * 本地文件 → 我方 COS:presign(门面 /upload-url)→ 直传字节到预签名 URL → 返回 public_url。
159
+ * 字节不经我们的业务服务器,只走 COS。kind: image(默认)/video/audio。
160
+ */
161
+ async uploadLocalFile(filePath, kind = 'image') {
162
+ const size = statSync(filePath).size;
163
+ const filename = basename(filePath);
164
+ const ext = (filename.match(/\.[a-z0-9]+$/i)?.[0] || '').toLowerCase();
165
+ const content_type = MIME_BY_EXT[ext] || 'application/octet-stream';
166
+ const res = await this.producePost('/upload-url', { filename, content_type, size, kind });
167
+ // presign 可能要求 PUT 带特定头(content-type 等)——按 presign 返回的 headers 送;
168
+ // 未返回则至少带 content-type(COS 签名常把它纳入)。
169
+ await this.uploadFile(res.put_url, filePath, res.headers ?? { 'content-type': content_type });
170
+ return res.public_url;
171
+ }
145
172
  }
@@ -175,4 +175,108 @@ export function registerProduceTools(server, client) {
175
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
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
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 })));
239
+ // ========== P1 · 读取层(闭环必需:能读回自己建的东西)==========
240
+ server.tool('list_dramas', '列出我名下的所有短剧(drama_id/剧名/类型/集数/进度)。免费。', {}, async () => jsonResult(await client.produceGet('/dramas')));
241
+ server.tool('get_drama', '读一部剧的详情(设定/角色数/场景数/资产统计等)。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}`)));
242
+ server.tool('get_characters', '读某一集的角色列表(名字/外貌/定妆图/身份锚是否就绪)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/characters`)));
243
+ server.tool('get_scenes', '读某一集的场景列表(名字/描述/场景图是否就绪)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/scenes`)));
244
+ server.tool('get_assets', '读一部剧的全部资产聚合(角色/场景/道具/图片)。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/assets`)));
245
+ // ========== P1 · 任务进度 + 成本/预算 ==========
246
+ server.tool('get_jobs', '查一部剧的任务队列进度(出图/出视频/合成各阶段的 pending/processing/done/failed)。' +
247
+ '异步生成后用它看进度。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/jobs`)));
248
+ server.tool('get_cost_estimate', '查一部剧的整体成本预估(点数)。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/cost-estimate`)));
249
+ server.tool('get_budget_status', '查一部剧的预算状态(预算/已花费/剩余)。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/budget-status`)));
250
+ // ========== P1 · 整集配音(成片前的强制音频步)==========
251
+ server.tool('generate_tts', '给整集所有对白批量配音(TTS)。成片前的音频步——不配音成片会缺对白。后台异步,按用量后付不欠费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/tts`)));
252
+ // ========== P1 · MV 子流程(project_type=mv 专用)==========
253
+ server.tool('set_mv_lyrics', 'MV 项目:设置歌词(整曲音频另在建剧时传)。免费。之后 generate_mv_story → generate_mv_script。', { drama_id: z.number().int().positive(), lyrics: z.string().min(1).describe('歌词全文') }, async ({ drama_id, lyrics }) => jsonResult(await client.producePost(`/dramas/${drama_id}/mv/lyrics`, { lyrics })));
254
+ server.tool('generate_mv_story', 'MV 项目:据歌词 AI 编一个 MV 故事线。后台/同步,按用量后付。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/mv/story`)));
255
+ server.tool('generate_mv_script', 'MV 项目:据歌词+故事写首集可拍剧本(写 scriptContent)。之后走标准 extract_assets → 分镜 → …。按用量后付。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/mv/script`)));
256
+ server.tool('get_mv', 'MV 项目:读歌词/故事/剧本当前状态。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/mv`)));
257
+ // ========== P1 · 资产精修(提取后改设定再出图)==========
258
+ server.tool('update_character', '改角色设定(名字/外貌/年龄段/族裔/性别/性格/戏份)。改后需重出定妆图/相关图。免费。', {
259
+ character_id: z.number().int().positive(),
260
+ name: z.string().optional(),
261
+ description: z.string().optional(),
262
+ appearance: z.string().optional().describe('外貌描述'),
263
+ age_stage: z.string().optional(),
264
+ ethnicity: z.string().optional(),
265
+ gender: z.string().optional(),
266
+ personality: z.string().optional(),
267
+ role: z.string().optional(),
268
+ }, async ({ character_id, ...fields }) => jsonResult(await client.producePut(`/characters/${character_id}`, fields)));
269
+ server.tool('delete_character', '删除一个角色(提取误建/合并时用)。免费。', { character_id: z.number().int().positive() }, async ({ character_id }) => jsonResult(await client.produceDelete(`/characters/${character_id}`)));
270
+ server.tool('update_scene', '改场景设定(名字/描述/地点/时段/氛围)。免费。', {
271
+ scene_id: z.number().int().positive(),
272
+ name: z.string().optional(),
273
+ description: z.string().optional(),
274
+ location: z.string().optional(),
275
+ physical_location: z.string().optional(),
276
+ time_of_day: z.string().optional(),
277
+ mood: z.string().optional(),
278
+ }, async ({ scene_id, ...fields }) => jsonResult(await client.producePut(`/scenes/${scene_id}`, fields)));
279
+ server.tool('delete_scene', '删除一个场景。免费。', { scene_id: z.number().int().positive() }, async ({ scene_id }) => jsonResult(await client.produceDelete(`/scenes/${scene_id}`)));
280
+ server.tool('generate_character_sheet', '给角色出三视图设定图(比定妆图更完整的身份锚,一致性更稳)。图片步,按用量后付不欠费。', { character_id: z.number().int().positive(), episode_id: z.number().int().positive().optional() }, async ({ character_id, episode_id }) => jsonResult(await client.producePost(`/characters/${character_id}/sheet`, episode_id ? { episode_id } : {})));
281
+ server.tool('generate_prop_sheet', '给道具 AI 出设定图。图片步,按用量后付不欠费。', { prop_id: z.number().int().positive() }, async ({ prop_id }) => jsonResult(await client.producePost(`/props/${prop_id}/generate-sheet`)));
178
282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starreel/mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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.3",
5
+ "version": "0.1.5",
6
6
  "websiteUrl": "https://starreel.ai",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@starreel/mcp",
11
- "version": "0.1.3",
11
+ "version": "0.1.5",
12
12
  "transport": { "type": "stdio" },
13
13
  "environmentVariables": [
14
14
  {