beplus-mcp 0.37.1 → 0.40.0

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.md CHANGED
@@ -89,7 +89,8 @@ Reinicie o cliente. Rode a tool **`whoami`** para confirmar o vínculo.
89
89
  | `list_custom_voices` | Lista suas vozes próprias do TTS 3.8 (máx. 3) com o `voice_xxx` para usar em `voice`. Clonar a própria voz só no app web. Somente leitura. |
90
90
  | `design_voice` | Cria uma voz própria a partir de uma descrição. Gasta 💎 e leva ~30 s. |
91
91
  | `delete_custom_voice` | Apaga uma voz própria (destrutivo, sem volta). |
92
- | `generate_music` | Gera música completa (Suno v5.5 / v4.5) — modo descrição ou letra própria, tags, instrumental, vocal_gender, controles criativos. Async. |
92
+ | `generate_music` | Gera música completa (Suno v6 / v6-wild / v6-mini; v5-5 e v4-5 são nomes legados) — modo descrição ou letra própria, tags, instrumental, vocal_gender, duration, auto_lyrics e task_type (extend, cover, upload_extend, upload_cover, sample). Devolve clip_id por faixa. Async. |
93
+ | `music_tool` | Ferramentas por faixa de uma música pronta: wav, midi, stems-basic, stems-full, bpm, lyrics-timeline. Consome diamantes; repetir não cobra. |
93
94
  | `check_generation` | Status de uma geração async por id. |
94
95
  | `cancel_generation` | Cancela uma geração em fila/processamento (estorna diamantes). |
95
96
  | `estimate_cost` | Custo em diamantes antes de gerar. |
package/dist/index.js CHANGED
@@ -21634,6 +21634,23 @@ var BeplusClient = class {
21634
21634
  deleteCustomVoice(id) {
21635
21635
  return this.request("DELETE", `/tts/custom-voices/${encodeURIComponent(id)}`, { timeoutMs: 3e4 });
21636
21636
  }
21637
+ // request() já desembrulha `result`; o estado vem em `{ state }`.
21638
+ async startMusicTool(generationId, body2) {
21639
+ const r = await this.request(
21640
+ "POST",
21641
+ `/generations/${encodeURIComponent(generationId)}/music-tools`,
21642
+ { body: body2, timeoutMs: 6e4 }
21643
+ );
21644
+ return r?.state ?? null;
21645
+ }
21646
+ async getMusicTool(generationId, clipId, tool) {
21647
+ const r = await this.request(
21648
+ "GET",
21649
+ `/generations/${encodeURIComponent(generationId)}/music-tools`,
21650
+ { query: { clip_id: clipId, tool }, timeoutMs: 2e4 }
21651
+ );
21652
+ return r?.state ?? null;
21653
+ }
21637
21654
  models() {
21638
21655
  return this.request("GET", "/models");
21639
21656
  }
@@ -22145,18 +22162,21 @@ async function buildImageBlocks(urls, cfg) {
22145
22162
 
22146
22163
  // src/schemas/enums.ts
22147
22164
  var IMAGE_MODELS = [
22165
+ "google/nano-banana-2.1",
22148
22166
  "google/nano-banana-2",
22149
22167
  "google/nano-banana-pro",
22150
22168
  "openai/gpt-image-2",
22151
22169
  "openai/gpt-image-1",
22152
22170
  "bytedance/seedream-5.0-lite",
22153
22171
  "bytedance/seedream-5.0-pro",
22172
+ "bytedance/seedream-5.0-pro-layers",
22154
22173
  "midjourney"
22155
22174
  ];
22156
22175
  var SEEDREAM_LITE_SIZES = ["2K", "3K", "4K"];
22157
22176
  var SEEDREAM_PRO_SIZES = ["1K", "1.5K", "2K"];
22158
22177
  var SEEDREAM_PRO_PIXELS = { min: 921600, max: 4624220 };
22159
22178
  var SEEDREAM_PRO_MAX_IMAGES = 4;
22179
+ var SEEDREAM_LAYERS_SIZES = ["auto", "1K", "1.5K", "2K"];
22160
22180
  var VIDEO_MODELS = [
22161
22181
  "bytedance/seedance-2.5",
22162
22182
  "bytedance/seedance-2.0",
@@ -22187,9 +22207,14 @@ var UPSCALE_VIDEO_VARIANTS = [
22187
22207
  "Starlight Fast 2"
22188
22208
  ];
22189
22209
  var MUSIC_MODELS = [
22210
+ "suno/v6",
22211
+ "suno/v6-wild",
22212
+ "suno/v6-mini",
22190
22213
  "suno/v5-5",
22191
22214
  "suno/v4-5"
22192
22215
  ];
22216
+ var MUSIC_TASK_TYPES = ["create", "extend", "cover", "upload_extend", "upload_cover", "sample"];
22217
+ var MUSIC_TOOLS = ["wav", "midi", "stems-basic", "stems-full", "bpm", "lyrics-timeline"];
22193
22218
  var VOCAL_GENDERS = ["f", "m"];
22194
22219
  var TTS_MODELS = [
22195
22220
  "gemini-3.8-flash-lite-tts",
@@ -22237,7 +22262,7 @@ var VIDEO_ASPECT_RATIOS = [...ASPECT_RATIOS, "adaptive"];
22237
22262
  var IMAGE_RESOLUTIONS = ["1K", "2K", "4K"];
22238
22263
  var VIDEO_RESOLUTIONS = ["480p", "720p", "1080p", "4K"];
22239
22264
  var DEFAULTS = {
22240
- imageModel: "google/nano-banana-2",
22265
+ imageModel: "google/nano-banana-2.1",
22241
22266
  imageAspect: "1:1",
22242
22267
  imageResolution: "1K",
22243
22268
  videoModel: "bytedance/seedance-2.0-fast",
@@ -22246,9 +22271,254 @@ var DEFAULTS = {
22246
22271
  videoDuration: 5,
22247
22272
  ttsModel: "gemini-3.8-flash-lite-tts",
22248
22273
  voice: "Kore",
22249
- musicModel: "suno/v5-5"
22274
+ musicModel: "suno/v6"
22250
22275
  };
22251
22276
 
22277
+ // src/tools/music.ts
22278
+ var MUSIC_BUDGET_MS = 15e4;
22279
+ var TOOL_POLL_MS = 5e3;
22280
+ var TOOL_BUDGET_MS = 24e4;
22281
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
22282
+ function validateMusicArgs(a) {
22283
+ const type = a.task_type ?? "create";
22284
+ const customMode = a.custom_mode ?? false;
22285
+ if (a.title && a.title.trim().length > 79) return "`title` aceita no m\xE1ximo 79 caracteres.";
22286
+ if (a.auto_lyrics && !customMode) {
22287
+ return "`auto_lyrics` s\xF3 vale no modo letra (custom_mode=true). No modo descri\xE7\xE3o a IA j\xE1 escreve a letra.";
22288
+ }
22289
+ if (customMode && !a.lyrics?.trim() && !(a.auto_lyrics && a.description?.trim())) {
22290
+ return a.auto_lyrics ? "Com `auto_lyrics`, passe o tema em `lyrics` (ou `description`)." : "Modo letra (custom_mode=true) exige `lyrics`.";
22291
+ }
22292
+ if (!customMode && !a.description?.trim()) return "Modo descri\xE7\xE3o (custom_mode=false) exige `description`.";
22293
+ const needsSource = type === "extend" || type === "cover";
22294
+ const needsReference = type === "upload_extend" || type === "upload_cover" || type === "sample";
22295
+ const needsContinueAt = type === "extend" || type === "upload_extend";
22296
+ const usesWeight = type === "cover" || type === "upload_cover" || type === "sample";
22297
+ if (needsSource) {
22298
+ if (!a.source_generation_id?.trim() || !a.source_clip_id?.trim()) {
22299
+ return `task_type=${type} exige \`source_generation_id\` e \`source_clip_id\` (o clip_id sai em generate_music/check_generation da gera\xE7\xE3o original).`;
22300
+ }
22301
+ } else if (a.source_generation_id || a.source_clip_id) {
22302
+ return "`source_generation_id` e `source_clip_id` s\xF3 valem com task_type=extend ou cover.";
22303
+ }
22304
+ if (needsReference) {
22305
+ if (!a.reference_audio_url?.trim()) return `task_type=${type} exige \`reference_audio_url\` (URL ou caminho local do \xE1udio).`;
22306
+ } else if (a.reference_audio_url) {
22307
+ return "`reference_audio_url` s\xF3 vale com task_type=upload_extend, upload_cover ou sample.";
22308
+ }
22309
+ if (needsContinueAt) {
22310
+ if (a.continue_at == null) return `task_type=${type} exige \`continue_at\` (segundo em que a m\xFAsica continua).`;
22311
+ } else if (a.continue_at != null) {
22312
+ return "`continue_at` s\xF3 vale com task_type=extend ou upload_extend.";
22313
+ }
22314
+ if (a.audio_weight != null && !usesWeight) {
22315
+ return "`audio_weight` s\xF3 vale com task_type=cover, upload_cover ou sample.";
22316
+ }
22317
+ if (type === "sample") {
22318
+ if (a.sample_start_s == null || a.sample_end_s == null) return "task_type=sample exige `sample_start_s` e `sample_end_s`.";
22319
+ if (a.sample_end_s <= a.sample_start_s) return "`sample_end_s` precisa ser maior que `sample_start_s`.";
22320
+ if (!customMode && a.description && a.description.trim().length > 200) {
22321
+ return "No task_type=sample a `description` aceita no m\xE1ximo 200 caracteres.";
22322
+ }
22323
+ if (a.tags && a.tags.trim().length > 200) return "No task_type=sample as `tags` aceitam no m\xE1ximo 200 caracteres.";
22324
+ } else if (a.sample_start_s != null || a.sample_end_s != null) {
22325
+ return "`sample_start_s` e `sample_end_s` s\xF3 valem com task_type=sample.";
22326
+ }
22327
+ return null;
22328
+ }
22329
+ async function getMusicClips(client, generationId, status) {
22330
+ try {
22331
+ const st = status ?? await client.getStatus(generationId);
22332
+ return Array.isArray(st.music_clips) ? st.music_clips : [];
22333
+ } catch {
22334
+ return [];
22335
+ }
22336
+ }
22337
+ function formatMusicClips(generationId, urls, clips) {
22338
+ const lines = (urls.length ? urls : clips.map((c) => c.audio_url ?? "")).map((u, i) => {
22339
+ const clip = clips[i];
22340
+ return `${i + 1}. ${u || "(sem URL)"}${clip?.clip_id ? `
22341
+ clip_id: ${clip.clip_id}` : ""}`;
22342
+ });
22343
+ const chain = clips.length ? `
22344
+ Para estender/cobrir ou usar music_tool: generation_id=${generationId} + o clip_id da faixa.` : "";
22345
+ return `${lines.join("\n") || "(sem URLs)"}${chain}`;
22346
+ }
22347
+ function registerMusicTools(server, client, cfg) {
22348
+ server.registerTool(
22349
+ "generate_music",
22350
+ {
22351
+ title: "Gerar m\xFAsica",
22352
+ description: 'Gera uma m\xFAsica completa (com vocais ou instrumental) usando o Suno no IA Lab da BePlus. ASS\xCDNCRONO e LENTO (geralmente 30s\u20132+ min). Aguarda at\xE9 ~2,5 min; se n\xE3o terminar, retorna o generation_id e o status \u2014 ent\xE3o use check_generation. Consome diamantes. DOIS modos: (a) DESCRI\xC7\xC3O (custom_mode=false, padr\xE3o) \u2192 passe `description` ("uma balada triste de piano"); (b) LETRA PR\xD3PRIA (custom_mode=true) \u2192 passe `lyrics` (a letra) e opcionalmente `title`. Use `tags` para g\xEAnero/estilo em ambos os modos (ex.: "sertanejo, animado, vocal feminino"). `vocal_gender` (f/m) vale para todos os modelos e \xE9 ignorado quando `make_instrumental=true`. Cada gera\xE7\xE3o produz 2 faixas, cada uma com `clip_id`. `duration` (10 a 360s) \xE9 s\xF3 uma meta aproximada; com letra, a letra manda. `task_type` muda a opera\xE7\xE3o: create (padr\xE3o), extend (continuar uma faixa sua, exige source_generation_id + source_clip_id + continue_at), cover (nova vers\xE3o de uma faixa sua, exige source_generation_id + source_clip_id), upload_extend / upload_cover (partem de um \xE1udio seu em `reference_audio_url`, URL ou caminho local) e sample (usa um trecho do \xE1udio, exige reference_audio_url + sample_start_s + sample_end_s). Extend e upload_extend custam o pre\xE7o "suno/extend"; o resto, o pre\xE7o do modelo. Depois de pronta, use music_tool para WAV, MIDI, stems, BPM e letra sincronizada de uma faixa.' + TEAM_GATING_NOTE,
22353
+ inputSchema: {
22354
+ model: external_exports.enum(MUSIC_MODELS).default(DEFAULTS.musicModel).describe(
22355
+ "suno/v6 = padr\xE3o, polido e previs\xEDvel; suno/v6-wild = o mesmo, de prop\xF3sito menos previs\xEDvel; suno/v6-mini = menor e mais r\xE1pido. suno/v5-5 e suno/v4-5 s\xE3o nomes legados, hoje atendidos pelo v6."
22356
+ ),
22357
+ custom_mode: external_exports.boolean().optional().describe("false (padr\xE3o) = gera a partir de `description`; true = usa sua `lyrics` pr\xF3pria."),
22358
+ description: external_exports.string().max(2500).optional().describe("Modo descri\xE7\xE3o (custom_mode=false): descreva a m\xFAsica desejada. Obrigat\xF3rio nesse modo."),
22359
+ lyrics: external_exports.string().max(5e3).optional().describe("Modo letra (custom_mode=true): a letra completa da m\xFAsica. Obrigat\xF3rio nesse modo."),
22360
+ title: external_exports.string().max(79).optional().describe("T\xEDtulo da m\xFAsica (modo letra). M\xE1ximo 79 caracteres."),
22361
+ tags: external_exports.string().max(1e3).optional().describe('G\xEAnero/estilo, separados por v\xEDrgula (ex.: "pop, energ\xE9tico, vocal masculino").'),
22362
+ make_instrumental: external_exports.boolean().optional().describe("true = sem vocais (apenas instrumental). Padr\xE3o false."),
22363
+ vocal_gender: external_exports.enum(VOCAL_GENDERS).optional().describe("G\xEAnero do vocal: f (feminino) ou m (masculino). Vale para todos os modelos; ignorado se instrumental."),
22364
+ negative_tags: external_exports.string().max(1e3).optional().describe("Estilos/elementos a evitar."),
22365
+ style_weight: external_exports.number().min(0).max(1).optional().describe("Peso do estilo (0\u20131). Maior = segue mais as tags. Omita para o modelo decidir."),
22366
+ weirdness_constraint: external_exports.number().min(0).max(1).optional().describe("Criatividade/imprevisibilidade (0\u20131). Omita para o modelo decidir."),
22367
+ duration: external_exports.number().int().min(10).max(360).optional().describe("Dura\xE7\xE3o-alvo aproximada em segundos (10 a 360). Com `lyrics`, a letra domina o tamanho."),
22368
+ auto_lyrics: external_exports.boolean().optional().describe("S\xF3 no modo letra (custom_mode=true): a IA escreve letra, t\xEDtulo e tags usando `lyrics` como TEMA."),
22369
+ task_type: external_exports.enum(MUSIC_TASK_TYPES).optional().describe("create (padr\xE3o) | extend | cover | upload_extend | upload_cover | sample. Veja a descri\xE7\xE3o da ferramenta."),
22370
+ source_generation_id: external_exports.string().min(1).optional().describe("extend/cover: id da gera\xE7\xE3o de m\xFAsica original."),
22371
+ source_clip_id: external_exports.string().min(1).optional().describe("extend/cover: clip_id da faixa original (aparece no resultado de generate_music / check_generation)."),
22372
+ continue_at: external_exports.number().min(0).optional().describe("extend/upload_extend: segundo da faixa a partir do qual continuar."),
22373
+ audio_weight: external_exports.number().min(0).max(1).optional().describe("cover/upload_cover/sample: fidelidade ao \xE1udio original (0 a 1)."),
22374
+ reference_audio_url: external_exports.string().min(1).optional().describe("upload_extend/upload_cover/sample: \xE1udio de origem. URL p\xFAblica ou caminho local (sobe para o storage da BePlus; outros hosts s\xE3o recusados)."),
22375
+ sample_start_s: external_exports.number().min(0).optional().describe("sample: in\xEDcio do trecho, em segundos."),
22376
+ sample_end_s: external_exports.number().min(0).optional().describe("sample: fim do trecho, em segundos (maior que o in\xEDcio)."),
22377
+ project: external_exports.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). S\xF3 omita se o usu\xE1rio J\xC1 confirmou o projeto ativo nesta sess\xE3o.'),
22378
+ billing: external_exports.enum(["personal"]).optional().describe(
22379
+ '"personal" = cobra do seu diamante pessoal, IGNORANDO o projeto ativo (mesmo se houver um). Use quando quiser garantir que isto N\xC3O sai do budget de nenhum projeto. Incompat\xEDvel com `project`.'
22380
+ )
22381
+ }
22382
+ },
22383
+ async (args) => {
22384
+ try {
22385
+ const customMode = args.custom_mode ?? false;
22386
+ const invalid = validateMusicArgs(args);
22387
+ if (invalid) return errorResult(invalid);
22388
+ const input = {
22389
+ custom_mode: customMode,
22390
+ make_instrumental: args.make_instrumental ?? false
22391
+ };
22392
+ if (args.vocal_gender) input.vocal_gender = args.vocal_gender;
22393
+ if (args.negative_tags?.trim()) input.negative_tags = args.negative_tags.trim();
22394
+ if (args.style_weight != null) input.style_weight = args.style_weight;
22395
+ if (args.weirdness_constraint != null) input.weirdness_constraint = args.weirdness_constraint;
22396
+ if (args.tags?.trim()) input.tags = args.tags.trim();
22397
+ if (args.duration != null) input.duration = args.duration;
22398
+ if (args.auto_lyrics != null) input.auto_lyrics = args.auto_lyrics;
22399
+ if (args.task_type) input.task_type = args.task_type;
22400
+ if (args.source_generation_id) input.source_generation_id = args.source_generation_id.trim();
22401
+ if (args.source_clip_id) input.source_clip_id = args.source_clip_id.trim();
22402
+ if (args.continue_at != null) input.continue_at = args.continue_at;
22403
+ if (args.audio_weight != null) input.audio_weight = args.audio_weight;
22404
+ if (args.sample_start_s != null) input.sample_start_s = args.sample_start_s;
22405
+ if (args.sample_end_s != null) input.sample_end_s = args.sample_end_s;
22406
+ if (args.reference_audio_url) input.reference_audio_url = await resolveRef(cfg, args.reference_audio_url.trim());
22407
+ if (customMode) {
22408
+ input.prompt = args.lyrics?.trim() || args.description.trim();
22409
+ if (args.title?.trim()) input.title = args.title.trim();
22410
+ } else {
22411
+ const desc = args.description.trim();
22412
+ input.gpt_description_prompt = desc;
22413
+ input.prompt = desc;
22414
+ }
22415
+ const start = await client.startGeneration(
22416
+ compact({
22417
+ model: args.model,
22418
+ input,
22419
+ project: resolveProjectArg(args.project, args.billing, cfg.activeProject),
22420
+ billing: args.billing
22421
+ })
22422
+ );
22423
+ if (!start.success || !start.generation_id) {
22424
+ if (isTeamGatingError(start.message)) return errorResult(await buildPickProjectMessage(client));
22425
+ return errorResult(start.message || "Falha ao iniciar a gera\xE7\xE3o.");
22426
+ }
22427
+ const footer = costFooter(start, cfg);
22428
+ const { settled, status } = await pollUntilDone(client, start.generation_id, MUSIC_BUDGET_MS);
22429
+ if (!settled) {
22430
+ return textResult(
22431
+ `\u23F3 M\xFAsica ainda em produ\xE7\xE3o (id: ${start.generation_id}, ${status.progress ?? 0}%). Suno costuma levar alguns minutos \u2014 use check_generation com esse id.
22432
+ ${footer}`
22433
+ );
22434
+ }
22435
+ if (status.status === "failed") {
22436
+ return errorResult(`Gera\xE7\xE3o falhou: ${status.error_message || "erro desconhecido"}.
22437
+ ${footer}`);
22438
+ }
22439
+ const urls = outputUrls(status);
22440
+ const clips = await getMusicClips(client, start.generation_id, status);
22441
+ const list = formatMusicClips(start.generation_id, urls, clips);
22442
+ return textResult(`\u2705 M\xFAsica gerada (${args.model}), generation_id: ${start.generation_id}.
22443
+ ${list}
22444
+ ${footer}`);
22445
+ } catch (e) {
22446
+ const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "";
22447
+ if (isTeamGatingError(msg)) return errorResult(await buildPickProjectMessage(client));
22448
+ if (isBudgetBlockedError(msg)) {
22449
+ return errorResult(`${apiErrorToUserMessage(e)}
22450
+ ${BUDGET_BLOCKED_GUIDANCE}`);
22451
+ }
22452
+ return errorResult(apiErrorToUserMessage(e));
22453
+ }
22454
+ }
22455
+ );
22456
+ server.registerTool(
22457
+ "music_tool",
22458
+ {
22459
+ title: "Ferramenta de faixa de m\xFAsica",
22460
+ description: "Roda uma ferramenta numa faixa de m\xFAsica Suno j\xE1 pronta: wav (2 cr\xE9ditos AIMusic), midi (10), stems-basic (20, 4 stems: vocal e instrumental), stems-full (50, at\xE9 24 stems), bpm (1) e lyrics-timeline (1, palavras com tempo). Consome diamantes (o valor vem do backend); pedir de novo uma ferramenta j\xE1 conclu\xEDda devolve o resultado salvo sem cobrar. Aguarda at\xE9 ~4 min. Informe generation_id e a faixa por `clip_id` ou `clip_index` (1 ou 2).",
22461
+ inputSchema: {
22462
+ generation_id: external_exports.string().min(1).describe("Id da gera\xE7\xE3o de m\xFAsica conclu\xEDda."),
22463
+ tool: external_exports.enum(MUSIC_TOOLS).describe("wav | midi | stems-basic | stems-full | bpm | lyrics-timeline."),
22464
+ clip_id: external_exports.string().min(1).optional().describe("clip_id da faixa (veja generate_music / check_generation)."),
22465
+ clip_index: external_exports.number().int().min(1).max(2).optional().describe("Alternativa ao clip_id: 1 ou 2."),
22466
+ full_words: external_exports.boolean().optional().describe("lyrics-timeline: true devolve todas as palavras com tempo. Padr\xE3o: s\xF3 um resumo.")
22467
+ }
22468
+ },
22469
+ async (args) => {
22470
+ try {
22471
+ let clipId = args.clip_id?.trim();
22472
+ if (!clipId) {
22473
+ if (!args.clip_index) return errorResult("Informe `clip_id` ou `clip_index` (1 ou 2).");
22474
+ const clips = await getMusicClips(client, args.generation_id);
22475
+ clipId = clips[args.clip_index - 1]?.clip_id;
22476
+ if (!clipId) return errorResult("N\xE3o achei o clip_id dessa faixa. Passe `clip_id` explicitamente.");
22477
+ }
22478
+ const tool = args.tool;
22479
+ let state = await client.startMusicTool(args.generation_id, { clip_id: clipId, tool });
22480
+ const started = Date.now();
22481
+ while (state?.status === "processing" && Date.now() - started < TOOL_BUDGET_MS) {
22482
+ await sleep2(TOOL_POLL_MS);
22483
+ state = await client.getMusicTool(args.generation_id, clipId, tool) ?? state;
22484
+ }
22485
+ if (!state) return errorResult("O backend n\xE3o devolveu o estado da ferramenta.");
22486
+ if (state.status === "failed") {
22487
+ return errorResult(`${tool} falhou: ${state.error || "erro desconhecido"}.`);
22488
+ }
22489
+ if (state.status === "processing") {
22490
+ return textResult(
22491
+ `\u23F3 ${tool} ainda processando (custo ${state.cost} \u{1F48E}). Chame music_tool de novo com os mesmos argumentos em instantes: n\xE3o cobra de novo.`
22492
+ );
22493
+ }
22494
+ return textResult(`\u2705 ${tool} pronto (${state.cost} \u{1F48E}).
22495
+ ${formatToolResult(state, args.full_words ?? false)}`);
22496
+ } catch (e) {
22497
+ return errorResult(apiErrorToUserMessage(e));
22498
+ }
22499
+ }
22500
+ );
22501
+ }
22502
+ function formatToolResult(state, fullWords) {
22503
+ const r = state.result ?? {};
22504
+ const lines = [];
22505
+ if (r.url) lines.push(r.url);
22506
+ if (r.stems?.length) lines.push(...r.stems.map((s) => `${s.title}: ${s.url}`));
22507
+ if (r.bpm) lines.push(`BPM m\xE9dio ${r.bpm.avg} (m\xEDn ${r.bpm.min}, m\xE1x ${r.bpm.max})`);
22508
+ if (r.words?.length) {
22509
+ if (fullWords) {
22510
+ lines.push(r.words.map((w) => `${w.s.toFixed(2)}-${w.e.toFixed(2)} ${w.w}`).join("\n"));
22511
+ } else {
22512
+ const first = r.words.slice(0, 12).map((w) => w.w).join(" ");
22513
+ const end = r.words[r.words.length - 1].e;
22514
+ lines.push(
22515
+ `${r.words.length} palavras sincronizadas, at\xE9 ${end.toFixed(1)}s. In\xEDcio: "${first}". Use full_words=true para a lista completa.`
22516
+ );
22517
+ }
22518
+ }
22519
+ return lines.join("\n") || "(sem resultado)";
22520
+ }
22521
+
22252
22522
  // src/tools/generation.ts
22253
22523
  var IMAGE_BUDGET_MS = 9e4;
22254
22524
  var VIDEO_BUDGET_MS = 12e4;
@@ -22281,9 +22551,11 @@ function registerGenerationTools(server, client, cfg) {
22281
22551
  // Sem teto local: quem conhece o limite de cada provedor é o backend, e
22282
22552
  // ele responde com quanto reduzir. Um `.max()` aqui rejeitava prompts
22283
22553
  // que o modelo aceita (Seedream/nano-banana não têm limite publicado).
22284
- prompt: external_exports.string().min(1).describe("Descri\xE7\xE3o da imagem desejada."),
22554
+ prompt: external_exports.string().default("").describe(
22555
+ 'Descri\xE7\xE3o da imagem desejada. No seedream-5.0-pro-layers \xE9 opcional: diga o que separar (ex.: "o produto, o texto e o fundo") ou deixe vazio para o modelo escolher.'
22556
+ ),
22285
22557
  model: external_exports.enum(IMAGE_MODELS).default(DEFAULTS.imageModel).describe(
22286
- "nano-banana-2 = barato/r\xE1pido; nano-banana-pro = melhor qualidade; gpt-image-2 = OpenAI; gpt-image-1 = OpenAI, \xDANICO que faz fundo transparente de verdade (PNG/WebP com alpha) \u2014 o gpt-image-2 N\xC3O faz, use este pra recorte/sticker; seedream-5.0-lite = mais barato do cat\xE1logo, \xFAnico que chega a 4K, gera s\xE9ries e mant\xE9m o ESTILO da imagem de refer\xEAncia; seedream-5.0-pro = acabamento fotorrealista, s\xF3 at\xE9 2K (1K/1.5K/2K, propor\xE7\xE3o, tamanho livre e at\xE9 4 imagens por pedido) \u2014 preserva o personagem da refer\xEAncia mas costuma reinterpretar o estilo (um vetor flat volta em 3D)."
22558
+ "nano-banana-2.1 = padr\xE3o, o mais novo e o mais barato do Google (metade do 2, texto mais n\xEDtido); nano-banana-2 = vers\xE3o anterior, mesmo comportamento; nano-banana-pro = melhor qualidade; gpt-image-2 = OpenAI; gpt-image-1 = OpenAI, \xDANICO que faz fundo transparente de verdade (PNG/WebP com alpha) \u2014 o gpt-image-2 N\xC3O faz, use este pra recorte/sticker; seedream-5.0-lite = mais barato do cat\xE1logo, \xFAnico que chega a 4K, gera s\xE9ries e mant\xE9m o ESTILO da imagem de refer\xEAncia; seedream-5.0-pro = acabamento fotorrealista, s\xF3 at\xE9 2K (1K/1.5K/2K, propor\xE7\xE3o, tamanho livre e at\xE9 4 imagens por pedido) \u2014 preserva o personagem da refer\xEAncia mas costuma reinterpretar o estilo (um vetor flat volta em 3D); seedream-5.0-pro-layers = N\xC3O gera do zero: separa UMA imagem (reference_images com 1 item) em base + at\xE9 16 camadas PNG transparentes do mesmo tamanho, prontas para empilhar ou virar PSD. Cobra o teto de 17 imagens e estorna as que n\xE3o vierem."
22287
22559
  ),
22288
22560
  aspect_ratio: external_exports.enum(ASPECT_RATIOS).optional().describe(
22289
22561
  "Propor\xE7\xE3o. nano-banana: padr\xE3o 1:1. Seedream Pro: combina com size (1K/1.5K/2K) e vira LARGURAxALTURA no backend; sem ela o modelo escolhe o formato pelo prompt."
@@ -22292,8 +22564,8 @@ function registerGenerationTools(server, client, cfg) {
22292
22564
  reference_images: external_exports.array(external_exports.string().min(1)).max(16).optional().describe(
22293
22565
  "Imagens de refer\xEAncia (image-to-image): URL p\xFAblica OU caminho de arquivo local (o MCP faz upload pro R2 automaticamente). At\xE9 14 (nano-banana) / 16 (gpt-image-1/2)."
22294
22566
  ),
22295
- google_search: external_exports.boolean().optional().describe("nano-banana-2: aterra a imagem em busca web do Google (fatos atuais). Padr\xE3o false."),
22296
- image_search: external_exports.boolean().optional().describe("nano-banana-2: usa busca de imagens do Google como refer\xEAncia visual. Padr\xE3o false."),
22567
+ google_search: external_exports.boolean().optional().describe("nano-banana-2.1 e 2: aterra a imagem em busca web do Google (fatos atuais). Padr\xE3o false."),
22568
+ image_search: external_exports.boolean().optional().describe("nano-banana-2.1 e 2: usa busca de imagens do Google como refer\xEAncia visual. Padr\xE3o false."),
22297
22569
  size: external_exports.union([
22298
22570
  external_exports.enum(["1024x1024", "1536x1024", "1024x1536", "auto", "1K", "1.5K", "2K", "3K", "4K"]),
22299
22571
  external_exports.string().regex(/^\d{3,5}x\d{3,5}$/, "use LARGURAxALTURA, ex.: 2048x1152")
@@ -22340,6 +22612,25 @@ function registerGenerationTools(server, client, cfg) {
22340
22612
  const isSeedreamLite = args.model === "bytedance/seedream-5.0-lite";
22341
22613
  const isSeedreamPro = args.model === "bytedance/seedream-5.0-pro";
22342
22614
  const isSeedream = isSeedreamLite || isSeedreamPro;
22615
+ const isSeedreamLayers = args.model === "bytedance/seedream-5.0-pro-layers";
22616
+ if (!isSeedreamLayers && !args.prompt.trim()) {
22617
+ return errorResult("prompt \xE9 obrigat\xF3rio (s\xF3 o seedream-5.0-pro-layers aceita sem prompt).");
22618
+ }
22619
+ if (isSeedreamLayers) {
22620
+ if (args.reference_images?.length !== 1) {
22621
+ return errorResult(
22622
+ "seedream-5.0-pro-layers separa UMA imagem em camadas: passe exatamente 1 item em reference_images."
22623
+ );
22624
+ }
22625
+ if (args.size && !SEEDREAM_LAYERS_SIZES.includes(args.size)) {
22626
+ return errorResult(
22627
+ `seedream-5.0-pro-layers aceita size ${SEEDREAM_LAYERS_SIZES.join("/")} (voc\xEA pediu ${args.size}).`
22628
+ );
22629
+ }
22630
+ if (args.num_images || args.batch_images) {
22631
+ return errorResult("seedream-5.0-pro-layers devolve sempre base + camadas; num_images e batch_images n\xE3o se aplicam.");
22632
+ }
22633
+ }
22343
22634
  if (isSeedream) {
22344
22635
  const allowed = isSeedreamPro ? SEEDREAM_PRO_SIZES : SEEDREAM_LITE_SIZES;
22345
22636
  const free = args.size?.match(/^(\d+)x(\d+)$/);
@@ -22404,7 +22695,7 @@ function registerGenerationTools(server, client, cfg) {
22404
22695
  reference_images,
22405
22696
  google_search: args.google_search,
22406
22697
  image_search: args.image_search,
22407
- size: args.size ?? (isSeedream ? "2K" : void 0),
22698
+ size: args.size ?? (isSeedream ? "2K" : isSeedreamLayers ? "auto" : void 0),
22408
22699
  // Série do Seedream Lite: o backend lê o par sequential_image_generation
22409
22700
  // + max_images (nomes da API da BytePlus). `batch_images` é só o nome
22410
22701
  // amigável exposto aqui.
@@ -22771,7 +23062,8 @@ ${BUDGET_BLOCKED_GUIDANCE}`);
22771
23062
  if (status.status === "completed") {
22772
23063
  const urls = outputUrls(status);
22773
23064
  const blocks = await buildImageBlocks(urls, cfg);
22774
- const list = urls.map((u, i) => `${i + 1}. ${u}`).join("\n") || "(sem URLs)";
23065
+ const isAudio = urls.length > 0 && urls.every((u) => /\.(mp3|m4a|wav|ogg)(\?|$)/i.test(u));
23066
+ const list = isAudio ? formatMusicClips(generation_id, urls, await getMusicClips(client, generation_id, status)) : urls.map((u, i) => `${i + 1}. ${u}`).join("\n") || "(sem URLs)";
22775
23067
  const draftProvider = status.seedance_provider ?? "byteplus";
22776
23068
  const draftHint = status.draft_task_id ? `
22777
23069
 
@@ -23700,100 +23992,6 @@ Use o valor de "voice" no generate_audio com um modelo 3.8 (sem multi_speaker).`
23700
23992
  );
23701
23993
  }
23702
23994
 
23703
- // src/tools/music.ts
23704
- var MUSIC_BUDGET_MS = 15e4;
23705
- function registerMusicTools(server, client, cfg) {
23706
- server.registerTool(
23707
- "generate_music",
23708
- {
23709
- title: "Gerar m\xFAsica",
23710
- description: 'Gera uma m\xFAsica completa (com vocais ou instrumental) usando o Suno no IA Lab da BePlus. ASS\xCDNCRONO e LENTO (geralmente 30s\u20132+ min). Aguarda at\xE9 ~2,5 min; se n\xE3o terminar, retorna o generation_id e o status \u2014 ent\xE3o use check_generation. Consome diamantes. DOIS modos: (a) DESCRI\xC7\xC3O (custom_mode=false, padr\xE3o) \u2192 passe `description` ("uma balada triste de piano"); (b) LETRA PR\xD3PRIA (custom_mode=true) \u2192 passe `lyrics` (a letra) e opcionalmente `title`. Use `tags` para g\xEAnero/estilo em ambos os modos (ex.: "sertanejo, animado, vocal feminino"). `vocal_gender` (f/m) s\xF3 \xE9 aceito pelo Suno v4.5 e \xE9 ignorado quando `make_instrumental=true`.' + TEAM_GATING_NOTE,
23711
- inputSchema: {
23712
- model: external_exports.enum(MUSIC_MODELS).default(DEFAULTS.musicModel).describe("suno/v5-5 = mais novo; suno/v4-5 = r\xE1pido/consistente (\xFAnico que aceita vocal_gender)."),
23713
- custom_mode: external_exports.boolean().optional().describe("false (padr\xE3o) = gera a partir de `description`; true = usa sua `lyrics` pr\xF3pria."),
23714
- description: external_exports.string().max(2500).optional().describe("Modo descri\xE7\xE3o (custom_mode=false): descreva a m\xFAsica desejada. Obrigat\xF3rio nesse modo."),
23715
- lyrics: external_exports.string().max(5e3).optional().describe("Modo letra (custom_mode=true): a letra completa da m\xFAsica. Obrigat\xF3rio nesse modo."),
23716
- title: external_exports.string().max(120).optional().describe("T\xEDtulo da m\xFAsica (modo letra)."),
23717
- tags: external_exports.string().max(1e3).optional().describe('G\xEAnero/estilo, separados por v\xEDrgula (ex.: "pop, energ\xE9tico, vocal masculino").'),
23718
- make_instrumental: external_exports.boolean().optional().describe("true = sem vocais (apenas instrumental). Padr\xE3o false."),
23719
- vocal_gender: external_exports.enum(VOCAL_GENDERS).optional().describe("G\xEAnero do vocal: f (feminino) ou m (masculino). S\xF3 Suno v4.5; ignorado se instrumental."),
23720
- negative_tags: external_exports.string().max(1e3).optional().describe("Estilos/elementos a evitar."),
23721
- style_weight: external_exports.number().min(0).max(1).optional().describe("Peso do estilo (0\u20131). Maior = segue mais as tags. Omita para o modelo decidir."),
23722
- weirdness_constraint: external_exports.number().min(0).max(1).optional().describe("Criatividade/imprevisibilidade (0\u20131). Omita para o modelo decidir."),
23723
- project: external_exports.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). S\xF3 omita se o usu\xE1rio J\xC1 confirmou o projeto ativo nesta sess\xE3o.'),
23724
- billing: external_exports.enum(["personal"]).optional().describe(
23725
- '"personal" = cobra do seu diamante pessoal, IGNORANDO o projeto ativo (mesmo se houver um). Use quando quiser garantir que isto N\xC3O sai do budget de nenhum projeto. Incompat\xEDvel com `project`.'
23726
- )
23727
- }
23728
- },
23729
- async (args) => {
23730
- try {
23731
- const customMode = args.custom_mode ?? false;
23732
- if (customMode && !args.lyrics?.trim()) {
23733
- return errorResult("Modo letra (custom_mode=true) exige `lyrics`.");
23734
- }
23735
- if (!customMode && !args.description?.trim()) {
23736
- return errorResult("Modo descri\xE7\xE3o (custom_mode=false) exige `description`.");
23737
- }
23738
- const input = {
23739
- custom_mode: customMode,
23740
- make_instrumental: args.make_instrumental ?? false
23741
- };
23742
- if (args.vocal_gender) input.vocal_gender = args.vocal_gender;
23743
- if (args.negative_tags?.trim()) input.negative_tags = args.negative_tags.trim();
23744
- if (args.style_weight != null) input.style_weight = args.style_weight;
23745
- if (args.weirdness_constraint != null) input.weirdness_constraint = args.weirdness_constraint;
23746
- if (args.tags?.trim()) input.tags = args.tags.trim();
23747
- if (customMode) {
23748
- input.prompt = args.lyrics.trim();
23749
- if (args.title?.trim()) input.title = args.title.trim();
23750
- } else {
23751
- const desc = args.description.trim();
23752
- input.gpt_description_prompt = desc;
23753
- input.prompt = desc;
23754
- }
23755
- const start = await client.startGeneration(
23756
- compact({
23757
- model: args.model,
23758
- input,
23759
- project: resolveProjectArg(args.project, args.billing, cfg.activeProject),
23760
- billing: args.billing
23761
- })
23762
- );
23763
- if (!start.success || !start.generation_id) {
23764
- if (isTeamGatingError(start.message)) return errorResult(await buildPickProjectMessage(client));
23765
- return errorResult(start.message || "Falha ao iniciar a gera\xE7\xE3o.");
23766
- }
23767
- const footer = costFooter(start, cfg);
23768
- const { settled, status } = await pollUntilDone(client, start.generation_id, MUSIC_BUDGET_MS);
23769
- if (!settled) {
23770
- return textResult(
23771
- `\u23F3 M\xFAsica ainda em produ\xE7\xE3o (id: ${start.generation_id}, ${status.progress ?? 0}%). Suno costuma levar alguns minutos \u2014 use check_generation com esse id.
23772
- ${footer}`
23773
- );
23774
- }
23775
- if (status.status === "failed") {
23776
- return errorResult(`Gera\xE7\xE3o falhou: ${status.error_message || "erro desconhecido"}.
23777
- ${footer}`);
23778
- }
23779
- const urls = outputUrls(status);
23780
- const list = urls.map((u, i) => `${i + 1}. ${u}`).join("\n") || "(sem URLs)";
23781
- return textResult(`\u2705 M\xFAsica gerada (${args.model}).
23782
- ${list}
23783
- ${footer}`);
23784
- } catch (e) {
23785
- const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "";
23786
- if (isTeamGatingError(msg)) return errorResult(await buildPickProjectMessage(client));
23787
- if (isBudgetBlockedError(msg)) {
23788
- return errorResult(`${apiErrorToUserMessage(e)}
23789
- ${BUDGET_BLOCKED_GUIDANCE}`);
23790
- }
23791
- return errorResult(apiErrorToUserMessage(e));
23792
- }
23793
- }
23794
- );
23795
- }
23796
-
23797
23995
  // src/tools/account.ts
23798
23996
  function registerAccountTools(server, client, _cfg) {
23799
23997
  server.registerTool(
@@ -23883,6 +24081,7 @@ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xED
23883
24081
  quality: external_exports.string().optional(),
23884
24082
  aspect_ratio: external_exports.string().optional().describe("Seedream Pro: a propor\xE7\xE3o muda os pixels e a faixa de pre\xE7o."),
23885
24083
  num_images: external_exports.number().int().min(1).optional().describe("Seedream Pro: imagens por pedido; cada uma \xE9 cobrada."),
24084
+ task_type: external_exports.string().optional().describe('[m\xFAsica] extend e upload_extend usam o pre\xE7o "suno/extend"; os demais, o do modelo.'),
23886
24085
  provider: external_exports.enum(["byteplus", "fal"]).optional().describe("Seedance: provedor que vai gerar (padr\xE3o byteplus). `fal` tem pre\xE7o pr\xF3prio, mais caro."),
23887
24086
  // Upscale: o preço não sai de uma chave fixa — vem das medidas do
23888
24087
  // arquivo (megapixel de saída na imagem, bloco de 10s no vídeo). Sem
@@ -23919,7 +24118,7 @@ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xED
23919
24118
  }
23920
24119
  const c = await client.calculateCost(
23921
24120
  compact({
23922
- model: args.model,
24121
+ model: args.model.startsWith("suno/") && (args.task_type === "extend" || args.task_type === "upload_extend") ? "suno/extend" : args.model,
23923
24122
  resolution: args.resolution,
23924
24123
  duration: args.duration,
23925
24124
  mode: args.mode,
@@ -24513,7 +24712,7 @@ var KINDS = {
24513
24712
  music: {
24514
24713
  title: "Trilha",
24515
24714
  size: { w: 320, h: 212 },
24516
- data: { model: "suno/v5-5", instrumental: true, negativeTags: ["vocals", "choir", "whistling", "marching band", "epic orchestral"] },
24715
+ data: { model: "suno/v6", instrumental: true, negativeTags: ["vocals", "choir", "whistling", "marching band", "epic orchestral"] },
24517
24716
  inputs: ["prompt", "negative"],
24518
24717
  outputs: ["audio", "approved"],
24519
24718
  dynamic: "lyrics quando n\xE3o \xE9 instrumental"
@@ -27558,7 +27757,7 @@ N\xF3s: ${Object.entries(ids).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : ""
27558
27757
  import { createRequire } from "module";
27559
27758
  var require2 = createRequire(import.meta.url);
27560
27759
  function readVersion() {
27561
- if ("0.37.1") return "0.37.1";
27760
+ if ("0.40.0") return "0.40.0";
27562
27761
  try {
27563
27762
  const pkg = require2("../package.json");
27564
27763
  return pkg.version ?? "0.0.0-unknown";