sapiens-mcp 1.65.5 → 1.66.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/dist/registry.js CHANGED
@@ -21,6 +21,7 @@ import { musicator, musicatorSchema } from "./tools/musicator.js";
21
21
  import { video, videoSchema } from "./tools/video.js";
22
22
  import { write, writeSchema } from "./tools/write.js";
23
23
  import { stockAudio, stockAudioSchema } from "./tools/stockAudio.js";
24
+ import { voiceJp, voiceJpSchema } from "./tools/voiceJp.js";
24
25
  import { stockVideo, stockVideoSchema } from "./tools/stockVideo.js";
25
26
  import { brand, brandSchema } from "./tools/brand.js";
26
27
  import { character, characterSchema } from "./tools/character.js";
@@ -224,6 +225,11 @@ export const TOOLS = {
224
225
  schema: resumeSchema,
225
226
  handler: resume,
226
227
  },
228
+ sapiens_voice_jp: {
229
+ description: "Voz japonesa (TTS Irodori), motor LOCAL da casa, ADMIN-ONLY e de GRAÇA (zero Sinapse: roda numa GPU nossa, não numa API paga). Sub-actions: 'speak' (enfileira o take e por padrão ESPERA o áudio, devolvendo a audioUrl), 'status' (1 job por jobId), 'list' (histórico com a seed usada em cada take), 'queue' (a fila está andando? o worker está de pé?), 'models' (catálogo + limites + as duas notas de uso). O CORTE: só JAPONÊS. Texto em outra língua sai como leitura errada, não como sotaque; não tente 'português com sotaque japonês'. O QUE MAIS ERRA, e é medido, não achado: caption longo e descritivo pedindo 'homem de voz grave' empurra a voz pro AGUDO neste fine-tune (98-126 Hz sem caption contra 127-179 Hz com caption longo, mesma frase e mesma seed), e subir cfgScaleCaption NÃO conserta (4 -> 7 não move nada). Registro grave (seinen) é texto japonês + caption CURTO ou nenhum, e a identidade da voz vem da SEED: sem seed o worker sorteia e devolve a usada em usedSeed, que é como se repete a MESMA voz depois. Diferente das outras gerações da casa, esta é uma FILA: o áudio só sai enquanto o worker do outro lado estiver ligado. Com ele de pé um take de ~6s fica pronto em 2 a 4s; com ele desligado o job espera (não se perde, e não cobra nada). Chame action=queue antes de enfileirar em série, e action=models antes de escrever um caption caprichado.",
230
+ schema: voiceJpSchema,
231
+ handler: voiceJp,
232
+ },
227
233
  };
228
234
  // Guia de uso server-level: o protocolo MCP devolve isto no handshake (initialize)
229
235
  // e TODO cliente (Helen no Hermes, Gemini CLI, Cursor, Antigravity) injeta no modelo.
@@ -275,6 +281,7 @@ const TOOL_TITLES = {
275
281
  sapiens_pipeline: "Pipeline de Conteúdo",
276
282
  sapiens_image: "Gerar Imagem",
277
283
  sapiens_meta: "Conta & Utilitários",
284
+ sapiens_voice_jp: "Voz Japonesa (local)",
278
285
  sapiens_skill: "Skills da Casa",
279
286
  sapiens_repertorio: "Repertório",
280
287
  sapiens_gallery: "Galeria de Imagens",
@@ -324,6 +331,10 @@ const ADMIN_ONLY_TOOLS = new Set([
324
331
  // Despacho depende de rede conectada, e hoje so a casa tem: a rota tambem e
325
332
  // adminOnly. Quando abrir pra membro, sai daqui e do gate da rota juntos.
326
333
  "sapiens_distribution",
334
+ // Motor de voz japonesa: roda numa GPU da casa, por fila, e enquanto for
335
+ // japones-so nenhum membro precisa. O backend cobra admin no servidor
336
+ // (requireAdmin em convex/voiceJp.ts): sai daqui e do gate de la juntos.
337
+ "sapiens_voice_jp",
327
338
  ]);
328
339
  /**
329
340
  * Monta o payload do tools/list pro tier dado. Tier "user" esconde as
@@ -17,6 +17,20 @@ import { httpUrl } from "../schema.js";
17
17
  * qualquer resposta síncrona aguenta). Nesse caso NÃO gere de novo,
18
18
  * que cobraria duas vezes: acompanhe com `action=status` no imageId
19
19
  * que veio junto, ou olhe a galeria depois.
20
+ * - extend: CONTINUA um take SEU no tempo: o pedaço seguinte, colado no frame
21
+ * onde o anterior parou (mesmo elenco, mesma luz, mesma câmera,
22
+ * sem recomeçar a cena). Não escolhe motor nem config: proporção,
23
+ * resolução, som e modelo vêm do take-pai, porque é isso que faz o
24
+ * pedaço colar. `prompt` diz só O QUE ACONTECE em seguida (opcional:
25
+ * sem ele, a ação continua sozinha). Cada rodada é uma geração
26
+ * cheia e cobra como tal (no Omni, um clipe inteiro por +10s).
27
+ * Hoje só os motores Omni continuam; os outros recusam antes de
28
+ * cobrar. Assíncrono: volta imageId + parte, acompanhe em
29
+ * action=status. Encadeia (a parte 3 sai da parte 2), mas UMA POR
30
+ * VEZ: a próxima precisa da anterior pronta pra continuar dela.
31
+ * - chain: a corrente inteira de um take (as partes na ordem, duração somada
32
+ * quando as rows registraram, e qual parte serve de base pra
33
+ * próxima continuação). Sem custo. Qualquer parte serve de imageId.
20
34
  * - models: catálogo vivo dos motores ativos com a FAIXA de preço (o que uma
21
35
  * chamada sem config debita + o canto mais barato e o mais caro).
22
36
  * Sem custo, sem login.
@@ -188,6 +202,8 @@ const FILM_STATUSES = ["rascunho", "na_fila", "renderizando", "pronto"];
188
202
  export const videoSchema = z.object({
189
203
  action: z.enum([
190
204
  "create",
205
+ "extend",
206
+ "chain",
191
207
  "generate",
192
208
  "status",
193
209
  "models",
@@ -355,7 +371,9 @@ export const videoSchema = z.object({
355
371
  .optional()
356
372
  .describe("action=generate ou action=status: generatedImages:_id do vídeo. O create devolve o imageId; " +
357
373
  "ou um vídeo já criado no site (o row define modelo + custo). Use sapiens_gallery action=list pra descobrir. " +
358
- "action=sonorize: o imageId do clipe SEU (status completed) que vai ganhar som."),
374
+ "action=sonorize: o imageId do clipe SEU (status completed) que vai ganhar som. " +
375
+ "action=extend: o take SEU (completed) que ganha o pedaço seguinte; pode ser a raiz ou a última parte " +
376
+ "da corrente (o servidor resolve a posição). action=chain: qualquer parte da corrente."),
359
377
  // --- comum ---
360
378
  prompt: z
361
379
  .string()
@@ -750,6 +768,27 @@ export async function video(args) {
750
768
  prompt: args.prompt,
751
769
  });
752
770
  }
771
+ if (args.action === "extend") {
772
+ if (!args.imageId) {
773
+ throw new Error("action=extend exige imageId: o take SEU (já completed) que vai ganhar o pedaço seguinte. " +
774
+ "Pode ser a raiz ou a última parte de uma corrente; o servidor resolve a posição.");
775
+ }
776
+ return await convexAction("mcpExtrasActions:mcpVideoExtend", {
777
+ sessionToken,
778
+ imageId: args.imageId,
779
+ prompt: args.prompt,
780
+ durationSec: args.durationSec,
781
+ });
782
+ }
783
+ if (args.action === "chain") {
784
+ if (!args.imageId) {
785
+ throw new Error("action=chain exige imageId (qualquer parte da corrente serve).");
786
+ }
787
+ return await convexQuery("mcpExtras:mcpVideoChain", {
788
+ sessionToken,
789
+ imageId: args.imageId,
790
+ });
791
+ }
753
792
  if (args.action === "create") {
754
793
  if (!args.model && !args.templateSlug) {
755
794
  throw new Error("action=create exige model (ex: sapiens-video-seedance, sapiens-video-kling, sapiens-video-wan, sapiens-video-kling-motion) ou templateSlug (a receita traz o motor; veja action=templates).");
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ import { convexMutation, convexQuery, getSessionToken } from "../convexClient.js";
3
+ /**
4
+ * Voz japonesa (TTS Irodori), motor LOCAL da casa. ADMIN-only.
5
+ *
6
+ * O QUE É: um motor de fala japonesa que roda numa GPU nossa, não numa API
7
+ * paga. Custa ZERO Sinapse. Em compensação ele não é instantâneo por natureza:
8
+ * o job entra numa fila e um worker do outro lado puxa. Com o worker ligado,
9
+ * um take de 6 segundos fica pronto em 2 a 4 segundos.
10
+ *
11
+ * O CORTE: só japonês. Texto em qualquer outra língua sai como leitura errada,
12
+ * não como sotaque. Não tente "português com sotaque japonês" aqui.
13
+ *
14
+ * O QUE MAIS ERRA (medido, não achado): caption longo e descritivo pedindo
15
+ * "homem de voz grave" empurra a voz pro AGUDO neste fine-tune. Registro grave
16
+ * é texto + caption CURTO ou nenhum, e a voz vem da SEED. Chame action=models
17
+ * antes de escrever um caption caprichado.
18
+ *
19
+ * Fluxo típico:
20
+ * 1. action=models -> motores, limites e as duas notas que evitam take ruim
21
+ * 2. action=queue -> o worker está vivo? (não enfileire dez takes no vazio)
22
+ * 3. action=speak -> gera (espera o áudio por padrão)
23
+ * 4. action=list -> o histórico, com a seed usada em cada take
24
+ */
25
+ export const voiceJpSchema = z.object({
26
+ action: z
27
+ .enum(["speak", "status", "list", "queue", "models"])
28
+ .describe("speak = enfileira e espera o áudio; status = 1 job por id; list = histórico; queue = saúde da fila (worker vivo?); models = catálogo + limites + as notas de uso."),
29
+ text: z
30
+ .string()
31
+ .optional()
32
+ .describe("O que vai ser falado, EM JAPONÊS (obrigatório em speak). Outra língua sai como leitura errada. Teto de 300 caracteres: texto maior vira vários takes."),
33
+ caption: z
34
+ .string()
35
+ .optional()
36
+ .describe("Estilo da fala, em japonês. CURTO ganha: caption longo e descritivo empurra a voz pro agudo neste motor. Pra registro grave (seinen), prefira omitir e variar a seed."),
37
+ modelId: z
38
+ .string()
39
+ .optional()
40
+ .describe("id do catálogo (action=models). Default: irodori-4-1-anime."),
41
+ seed: z
42
+ .number()
43
+ .int()
44
+ .nonnegative()
45
+ .optional()
46
+ .describe("Trava a identidade da voz. Sem seed o worker sorteia e devolve a usada em usedSeed: é assim que se repete a MESMA voz num próximo take."),
47
+ numSteps: z
48
+ .number()
49
+ .int()
50
+ .optional()
51
+ .describe("Passos de amostragem, 8 a 120. Default 40; abaixo disso a fala perde definição e o ganho de tempo é pequeno."),
52
+ cfgScaleText: z.number().optional().describe("Aderência ao texto. Default 3."),
53
+ cfgScaleCaption: z
54
+ .number()
55
+ .optional()
56
+ .describe("Força do caption. Default 4. Subir NÃO conserta caption que desobedece (medimos 4 -> 7 sem mudança): reescreva o caption menor."),
57
+ cfgScaleSpeaker: z.number().optional().describe("Aderência à voz. Default 5."),
58
+ wait: z
59
+ .boolean()
60
+ .optional()
61
+ .describe("Default true: espera o worker terminar e já devolve a audioUrl. false devolve só o jobId, pra você seguir e checar depois com action=status."),
62
+ waitSeconds: z
63
+ .number()
64
+ .int()
65
+ .positive()
66
+ .max(180)
67
+ .optional()
68
+ .describe("Quanto esperar em speak. Default 60. Estourar não perde o job: ele continua na fila."),
69
+ jobId: z.string().optional().describe("obrigatório em action=status."),
70
+ status: z
71
+ .enum(["pending", "claimed", "completed", "error"])
72
+ .optional()
73
+ .describe("Filtra o histórico (action=list)."),
74
+ limit: z.number().int().positive().max(100).optional().describe("Default 20, max 100 (action=list)."),
75
+ });
76
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
77
+ export async function voiceJp(args) {
78
+ const sessionToken = getSessionToken();
79
+ if (args.action === "models") {
80
+ return await convexQuery("voiceJp:mcpModels", { sessionToken });
81
+ }
82
+ if (args.action === "queue") {
83
+ const health = await convexQuery("voiceJp:mcpQueueHealth", { sessionToken });
84
+ // Traduz o número em recado: "worker parado" é o que o agente precisa
85
+ // saber antes de enfileirar, e ninguém quer ler milissegundo cru.
86
+ const paradoHa = health?.ultimoTakeCompletoMs;
87
+ const workerProvavelmenteVivo = typeof paradoHa === "number" ? paradoHa < 15 * 60 * 1000 : health?.pendentes === 0;
88
+ return {
89
+ ...health,
90
+ leitura: workerProvavelmenteVivo
91
+ ? "Worker parece vivo."
92
+ : "Nenhum take fechou nos últimos 15 min. O worker pode estar desligado: confirme antes de enfileirar em série.",
93
+ };
94
+ }
95
+ if (args.action === "status") {
96
+ if (!args.jobId)
97
+ throw new Error("action=status exige jobId.");
98
+ return await convexQuery("voiceJp:mcpStatus", { sessionToken, jobId: args.jobId });
99
+ }
100
+ if (args.action === "list") {
101
+ return await convexQuery("voiceJp:mcpList", {
102
+ sessionToken,
103
+ limit: args.limit,
104
+ status: args.status,
105
+ });
106
+ }
107
+ // action=speak
108
+ if (!args.text)
109
+ throw new Error("action=speak exige text (em japonês).");
110
+ const enqueued = await convexMutation("voiceJp:mcpEnqueue", {
111
+ sessionToken,
112
+ text: args.text,
113
+ caption: args.caption,
114
+ modelId: args.modelId,
115
+ seed: args.seed,
116
+ numSteps: args.numSteps,
117
+ cfgScaleText: args.cfgScaleText,
118
+ cfgScaleCaption: args.cfgScaleCaption,
119
+ cfgScaleSpeaker: args.cfgScaleSpeaker,
120
+ });
121
+ if (args.wait === false) {
122
+ return {
123
+ ...enqueued,
124
+ dica: "Job na fila. Chame action=status com este jobId pra pegar a audioUrl.",
125
+ };
126
+ }
127
+ const teto = (args.waitSeconds ?? 60) * 1000;
128
+ const inicio = Date.now();
129
+ let espera = 1000;
130
+ while (Date.now() - inicio < teto) {
131
+ await sleep(espera);
132
+ // Sobe o intervalo devagar: o take típico fecha em 2 a 4s, mas fila com
133
+ // vários takes na frente não merece uma rajada de polling.
134
+ espera = Math.min(5000, Math.round(espera * 1.4));
135
+ const job = await convexQuery("voiceJp:mcpStatus", { sessionToken, jobId: enqueued.jobId });
136
+ if (job?.status === "completed") {
137
+ return { ...job, esperouSegundos: Math.round((Date.now() - inicio) / 1000) };
138
+ }
139
+ if (job?.status === "error") {
140
+ throw new Error(`Geração falhou: ${job.errorMessage || "sem detalhe"}`);
141
+ }
142
+ }
143
+ return {
144
+ jobId: enqueued.jobId,
145
+ status: "pending",
146
+ aviso: `Passou de ${args.waitSeconds ?? 60}s sem fechar. O job NÃO foi perdido, segue na fila. Cheque action=status, e action=queue pra ver se o worker está de pé.`,
147
+ };
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapiens-mcp",
3
- "version": "1.65.5",
3
+ "version": "1.66.0",
4
4
  "mcpName": "com.sapiensinteticos/sapiens",
5
5
  "description": "MCP server pra operar o Sapiens Sintéticos (sapiensinteticos.com) pelo Claude Code: gerar imagem, escrever artigo, voz, música e mais, na sua conta. Login pelo código de sapiensinteticos.com/conectar-claude.",
6
6
  "type": "module",