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 +11 -0
- package/dist/tools/image.js +10 -0
- package/dist/tools/video.js +48 -1
- package/dist/tools/voiceJp.js +148 -0
- package/package.json +1 -1
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
|
package/dist/tools/image.js
CHANGED
|
@@ -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.
|
package/dist/tools/video.js
CHANGED
|
@@ -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.
|
|
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",
|