sapiens-mcp 1.65.5 → 1.66.1

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
@@ -34,6 +34,16 @@ const MODELS = [
34
34
  "wavespeed-klein-anime", // Flux.2 Klein + LoRA anime (inteligente + controlável)
35
35
  "wavespeed-klein-anime-plus", // Klein Anime + SNOFS uncensored (+18) · Ousadia regulável
36
36
  "wavespeed-klein-celular", // O MESMO Klein 9B + LoRA de foto de celular (candid), segura o rosto da ref
37
+ "wavespeed-klein-nu", // O MESMO Klein 9B + SNOFS no registro de FOTO (a LoRA sozinha e forte, não
38
+ // casada com o traço anime como no klein-anime-plus). O Klein em edit VESTE a modelo sozinho mesmo
39
+ // sem safety checker: é esta LoRA que destrava o nível, e a referência é que segura a identidade.
40
+ // Ousadia regulável.
41
+ "wavespeed-klein-transparencia", // O MESMO Klein 9B + Transparent Clothes: tecido que a luz
42
+ // atravessa de verdade. Aqui o efeito é a LoRA e não a descrição (prompt caprichado sobre renda e
43
+ // trama, sem ela, devolve tecido opaco). Trigger no começo do prompt: "sheer clothes, revealing her
44
+ // breasts underneath". Segura o rosto da ref.
45
+ "wavespeed-krea2-transparencia", // A MESMA LoRA de transparência na base Krea 2: fotografia mais
46
+ // rica (luz com caráter, grão, profundidade) e identidade mais frouxa que a irmã Klein.
37
47
  "wavespeed-krea2", // Krea-2 Turbo pela WaveSpeed: MESMA base das fal-krea2-*, sem o
38
48
  // classificador de entrada da fal (que barra corpo nu como assunto mesmo sem palavra
39
49
  // de nudez no prompt). Aceita referência de VERDADE (img2img) + Ousadia. 2K nativo.
@@ -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.
@@ -103,6 +117,13 @@ import { httpUrl } from "../schema.js";
103
117
  * Som nativo. 4 a 15s em 480p e 720p; em 1080p para em 10s até
104
118
  * alguém medir o render nesse tier (15s ali é take de 9 mil
105
119
  * Sinapses que ninguém viu sair).
120
+ * - sapiens-video-ltx-spicy LTX 2.3 Spicy — o TAKE LONGO da linha sem freio, e o segundo de
121
+ * vídeo mais barato da casa. i2v puro, som nativo, e o único spicy
122
+ * que chega a 20s em TODOS os tiers (480p 125 Sinapses/s, 720p 250,
123
+ * 1080p 375: o 1080p daqui custa menos que o 480p do Seedance
124
+ * Spicy). Sem frame final. Em take longo, escreva o prompt em BEATS
125
+ * (primeiro ela faz X, depois Y, então Z): sem isso o motor inventa
126
+ * quando o texto acaba antes do tempo.
106
127
  * - sapiens-video-wan WAN 2.5 — imagem que fala/canta (áudio+lip-sync nativo), 5/10s (i2v)
107
128
  * - sapiens-video-wan-3 WAN 3.0: a geração seguinte da linha WAN. Texto OU imagem (t2v/i2v),
108
129
  * som nativo com interruptor (mesmo preço ligado ou desligado), frame
@@ -162,6 +183,7 @@ const VIDEO_MODELS = [
162
183
  "sapiens-video-h3",
163
184
  "sapiens-video-h3-spicy",
164
185
  "sapiens-video-seedance-spicy",
186
+ "sapiens-video-ltx-spicy",
165
187
  "sapiens-video-wan",
166
188
  "sapiens-video-wan-3",
167
189
  "sapiens-video-kling-motion",
@@ -188,6 +210,8 @@ const FILM_STATUSES = ["rascunho", "na_fila", "renderizando", "pronto"];
188
210
  export const videoSchema = z.object({
189
211
  action: z.enum([
190
212
  "create",
213
+ "extend",
214
+ "chain",
191
215
  "generate",
192
216
  "status",
193
217
  "models",
@@ -355,7 +379,9 @@ export const videoSchema = z.object({
355
379
  .optional()
356
380
  .describe("action=generate ou action=status: generatedImages:_id do vídeo. O create devolve o imageId; " +
357
381
  "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."),
382
+ "action=sonorize: o imageId do clipe SEU (status completed) que vai ganhar som. " +
383
+ "action=extend: o take SEU (completed) que ganha o pedaço seguinte; pode ser a raiz ou a última parte " +
384
+ "da corrente (o servidor resolve a posição). action=chain: qualquer parte da corrente."),
359
385
  // --- comum ---
360
386
  prompt: z
361
387
  .string()
@@ -750,6 +776,27 @@ export async function video(args) {
750
776
  prompt: args.prompt,
751
777
  });
752
778
  }
779
+ if (args.action === "extend") {
780
+ if (!args.imageId) {
781
+ throw new Error("action=extend exige imageId: o take SEU (já completed) que vai ganhar o pedaço seguinte. " +
782
+ "Pode ser a raiz ou a última parte de uma corrente; o servidor resolve a posição.");
783
+ }
784
+ return await convexAction("mcpExtrasActions:mcpVideoExtend", {
785
+ sessionToken,
786
+ imageId: args.imageId,
787
+ prompt: args.prompt,
788
+ durationSec: args.durationSec,
789
+ });
790
+ }
791
+ if (args.action === "chain") {
792
+ if (!args.imageId) {
793
+ throw new Error("action=chain exige imageId (qualquer parte da corrente serve).");
794
+ }
795
+ return await convexQuery("mcpExtras:mcpVideoChain", {
796
+ sessionToken,
797
+ imageId: args.imageId,
798
+ });
799
+ }
753
800
  if (args.action === "create") {
754
801
  if (!args.model && !args.templateSlug) {
755
802
  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.1",
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",