@starreel/mcp 0.1.3 → 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
  }
@@ -175,4 +175,65 @@ 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 })));
178
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starreel/mcp",
3
- "version": "0.1.3",
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.3",
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.3",
11
+ "version": "0.1.4",
12
12
  "transport": { "type": "stdio" },
13
13
  "environmentVariables": [
14
14
  {