sapiens-mcp 1.33.0 → 1.36.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/convexClient.js +12 -0
- package/dist/index.js +40 -1
- package/dist/registry.js +3 -3
- package/dist/tools/meta.js +11 -7
- package/dist/tools/persona.js +56 -15
- package/dist/tools/pipeline.js +89 -1
- package/dist/tools/video.js +228 -10
- package/package.json +1 -1
package/dist/convexClient.js
CHANGED
|
@@ -167,6 +167,18 @@ const requestTokenContext = new AsyncLocalStorage();
|
|
|
167
167
|
export function runWithSessionToken(token, fn) {
|
|
168
168
|
return requestTokenContext.run(token, fn);
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* True quando a chamada corre dentro do transporte REMOTO (streamable HTTP):
|
|
172
|
+
* cada request remota roda embrulhada em runWithSessionToken, então o
|
|
173
|
+
* AsyncLocalStorage tem valor. No stdio (processo local, na máquina do dono do
|
|
174
|
+
* token) o contexto NUNCA é setado. É o sinal confiável pra recusar operações
|
|
175
|
+
* que tocam o disco do HOST (ex: ler um arquivo local pra usar como frame de
|
|
176
|
+
* vídeo): no servidor multi-tenant da Vercel, um caminho arbitrário seria
|
|
177
|
+
* leitura de arquivo do próprio servidor, nunca do PC do usuário.
|
|
178
|
+
*/
|
|
179
|
+
export function isRemoteContext() {
|
|
180
|
+
return requestTokenContext.getStore() !== undefined;
|
|
181
|
+
}
|
|
170
182
|
export function getSessionToken() {
|
|
171
183
|
// Prioridade -1: token da REQUEST (transporte remoto, ver acima). Vence tudo:
|
|
172
184
|
// identidade no remoto é sempre do bearer, nunca do estado do host.
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,33 @@ import { MCP_VERSION } from "./version.js";
|
|
|
6
6
|
import { buildToolList, callTool, SAPIENS_INSTRUCTIONS, TOOLS, } from "./registry.js";
|
|
7
7
|
import { getCachedTier, onTierVisibilityChange, probeTierInBackground, } from "./tier.js";
|
|
8
8
|
import { getPrompt, listPrompts } from "./prompts.js";
|
|
9
|
+
import { convexMutation, getSessionToken } from "./convexClient.js";
|
|
10
|
+
// Telemetria stdio (fire-and-forget, NUNCA derruba a chamada). O stdio sempre
|
|
11
|
+
// foi caixa-preta (cada cliente roda a própria cópia via npx), então o
|
|
12
|
+
// transporte que mais roda — Helen/Cursor/Gemini CLI — era invisível na
|
|
13
|
+
// mcpUsage, que só via o remoto. Loga o MESMO shape que o remote.ts, com
|
|
14
|
+
// transport:"stdio". Identidade do login local (getSessionToken), nunca args
|
|
15
|
+
// (só tool/action/ok/ms). Opt-out por SAPIENS_MCP_NO_TELEMETRY=1.
|
|
16
|
+
function logStdioUsage(fields) {
|
|
17
|
+
try {
|
|
18
|
+
if (process.env.SAPIENS_MCP_NO_TELEMETRY === "1")
|
|
19
|
+
return;
|
|
20
|
+
const token = getSessionToken();
|
|
21
|
+
if (!token)
|
|
22
|
+
return; // sem login não há identidade pra vincular
|
|
23
|
+
convexMutation("mcpUsage:logCall", {
|
|
24
|
+
sessionToken: token,
|
|
25
|
+
tool: fields.tool,
|
|
26
|
+
action: fields.action ?? undefined,
|
|
27
|
+
ok: fields.ok,
|
|
28
|
+
ms: Math.round(fields.ms),
|
|
29
|
+
transport: "stdio",
|
|
30
|
+
}).catch(() => { });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// telemetria nunca vira erro pro cliente
|
|
34
|
+
}
|
|
35
|
+
}
|
|
9
36
|
// Transporte STDIO (o pacote npm/npx que os clients locais rodam). O catálogo,
|
|
10
37
|
// as instructions e o dispatch vivem no registry.ts, compartilhados com o
|
|
11
38
|
// transporte remoto (remote.ts). Aqui fica só o que é do stdio: o tier vem do
|
|
@@ -24,7 +51,19 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
|
24
51
|
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
25
52
|
return getPrompt(req.params.name, (req.params.arguments ?? {}));
|
|
26
53
|
});
|
|
27
|
-
server.setRequestHandler(CallToolRequestSchema, async (req) =>
|
|
54
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
55
|
+
const name = req.params.name;
|
|
56
|
+
const args = (req.params.arguments ?? {});
|
|
57
|
+
const t0 = Date.now();
|
|
58
|
+
const result = await callTool(name, args);
|
|
59
|
+
logStdioUsage({
|
|
60
|
+
tool: name,
|
|
61
|
+
action: typeof args.action === "string" ? args.action : null,
|
|
62
|
+
ok: !result.isError,
|
|
63
|
+
ms: Date.now() - t0,
|
|
64
|
+
});
|
|
65
|
+
return result;
|
|
66
|
+
});
|
|
28
67
|
// Tier mudou de um jeito que altera a lista visível (login/logout/probe):
|
|
29
68
|
// avisa o client pra re-listar. Best-effort: client que não suporta ignora.
|
|
30
69
|
onTierVisibilityChange(() => {
|
package/dist/registry.js
CHANGED
|
@@ -38,7 +38,7 @@ import { describeConvexError } from "./convexClient.js";
|
|
|
38
38
|
*/
|
|
39
39
|
export const TOOLS = {
|
|
40
40
|
sapiens_pipeline: {
|
|
41
|
-
description: "CRUD do content pipeline Sapiens (sources/productions/publishables). Sub-actions: list_sources, list_articles (use includeDrafts pra incluir drafts; onlyAvailable pra esconder os já virados em source), get_source, get_production, list_versions, add_article_as_source, create_draft_article_and_source (seed), create_production (sourceId+format → productionId draft), update_production (substitui payload, opcionalmente muda status), finalize_production (cria publishable v1, v2... com snapshot), remove_production, remove_source, set_source_done, update_source_notes, restore_version (volta payload duma versão antiga), set_publishable_title (renomeia um publishable), backfill_via (rotula em lote o campo 'via' das productions antigas; dryRun=true só lista), propose_mega_grafico_plan (granular: só gera plano via Gemini, devolve fullPrompt+spec), run_mega_grafico_full (ONE-SHOT, recomendado: cria production+propõe plano+gera imagem+aplica selo Sapiens+finaliza publishable numa chamada só), generate_carousel (gera um carrossel editorial standalone a partir de brief OU articleId — 7-9 slides na voz Sapiens + imagens do banco; devolve id + url do editor pro humano abrir, ajustar e exportar; admin-only, cobra Sinapses, reembolsa se falhar). Pra mega_grafico, SEMPRE prefira run_mega_grafico_full em vez de sequenciar manualmente (menos drift). Idempotente só na production (passa productionId pra reusar a MESMA row), mas cada run RE-GERA a imagem e cobra de novo (~900 Sinapses): não é grátis re-rodar. OBRIGATÓRIO perguntar ao user antes se withHelen=true (cartoon Helen interage com tema, ~15-25% do poster) ou false (poster 100% diagramático). Custo ~900-1000 sinapses por geração. run_mega_grafico_full e
|
|
41
|
+
description: "CRUD do content pipeline Sapiens (sources/productions/publishables). Sub-actions: list_sources, list_articles (use includeDrafts pra incluir drafts; onlyAvailable pra esconder os já virados em source), get_source, get_production, list_versions, add_article_as_source, create_draft_article_and_source (seed), create_production (sourceId+format → productionId draft), update_production (substitui payload, opcionalmente muda status), finalize_production (cria publishable v1, v2... com snapshot), remove_production, remove_source, set_source_done, update_source_notes, restore_version (volta payload duma versão antiga), set_publishable_title (renomeia um publishable), backfill_via (rotula em lote o campo 'via' das productions antigas; dryRun=true só lista), propose_mega_grafico_plan (granular: só gera plano via Gemini, devolve fullPrompt+spec), run_mega_grafico_full (ONE-SHOT, recomendado: cria production+propõe plano+gera imagem+aplica selo Sapiens+finaliza publishable numa chamada só), generate_carousel (gera um carrossel editorial standalone a partir de brief OU articleId — 7-9 slides na voz Sapiens + imagens do banco; devolve id + url do editor pro humano abrir, ajustar e exportar; admin-only, cobra Sinapses, reembolsa se falhar). generate_carousel_production (o IRMÃO PIPELINE do generate_carousel: recebe um sourceId de artigo, CRIA a production carrossel_ig e a preenche pelo MESMO motor da casa, já no shape editorial (templateId+slots) que o editor de pipeline renderiza — prefira ESTE a create_production+payload cru pro carrossel, que abre VAZIO no editor; devolve productionId + url do editor de pipeline; admin-only, cobra Sinapses, reembolsa se falhar). CARROSSEL FINO (edição sem tela, admin-only): list_carousels (seus carrosséis standalone), get_carousel (payload completo + catálogo de templates com slots/limites + paletas — tudo pra VOCÊ escrever os slides), update_carousel (payload inteiro de volta, sanitizado no servidor; preserve os campos image dos slides que não mexeu), carousel_auto_images (IA escolhe imagens do banco pros slides de foto, 60 Sinapses; makeVisual=true converte slides de texto pra layouts de foto antes — ritmo visual; generateMissing=true gera imagem nova pros que o banco não cobrir, ~450 Sinapses cada, até 4, avise o custo antes), carousel_generate_image (imagem nova pra UM slide, ~450 Sinapses, customPrompt opcional). Fluxo confortável (standalone): generate_carousel → get_carousel → update_carousel (afia os textos) → carousel_auto_images → humano abre a url pra exportar. Fluxo pipeline (carrossel atrelado a um artigo/source): generate_carousel_production (sourceId) → humano abre a url do editor de pipeline pra revisar e finalizar (finalize_production). Pra mega_grafico, SEMPRE prefira run_mega_grafico_full em vez de sequenciar manualmente (menos drift). Idempotente só na production (passa productionId pra reusar a MESMA row), mas cada run RE-GERA a imagem e cobra de novo (~900 Sinapses): não é grátis re-rodar. OBRIGATÓRIO perguntar ao user antes se withHelen=true (cartoon Helen interage com tema, ~15-25% do poster) ou false (poster 100% diagramático). Custo ~900-1000 sinapses por geração. run_mega_grafico_full, generate_carousel, generate_carousel_production, carousel_auto_images (com generateMissing) e carousel_generate_image são SÍNCRONAS e pesadas: vale a REGRA DO TIMEOUT (podem cobrar mesmo voltando 'Timeout'; cheque get_carousel/dashboard antes de repetir). Use skipFinalize=true se quiser deixar production em 'ready' pro admin revisar antes de publishable. Payload livre por formato — chame sapiens_meta action=formats pra ver schemas sugeridos.",
|
|
42
42
|
schema: pipelineSchema,
|
|
43
43
|
handler: pipeline,
|
|
44
44
|
},
|
|
@@ -113,7 +113,7 @@ export const TOOLS = {
|
|
|
113
113
|
handler: shorts,
|
|
114
114
|
},
|
|
115
115
|
sapiens_video: {
|
|
116
|
-
description: "Sapiens Video — gera vídeo (qualquer membro logado; vídeo é caro, cobra as Sinapses da sua conta). Sub-action 'create' (recomendada): escolhe modelo + config e gera num call (cria a row + renderiza). Modelos: 'sapiens-video-seedance' (Seedance 2.0, cena+áudio nativo, 4-15s, 480/720/1080p, t2v/i2v), 'sapiens-video-kling' (Kling 3.0 Pro, anima imagem, 3-15s, sound opcional, i2v/t2v), 'sapiens-video-wan' (WAN 2.5, imagem que fala/canta com áudio+lip-sync, 5/10s, i2v), 'sapiens-video-kling-motion' (Motion transfer: passa o movimento de um vídeo pra uma imagem, PRECISA de pessoa com tronco visível na imagem E no vídeo), 'sapiens-video-shot-mimic' (Shot Mimic: recria o plano/câmera/cortes de um vídeo de referência como cena nova), 'sapiens-video-omni' (Gemini Omni: texto vira vídeo 10s 720p com áudio nativo; NÃO aceita mídia do user, ignora references/durationSec/resolution; editOfImageId aponta um vídeo Omni seu e o prompt edita a MESMA cena, preservando câmera e ambiente), 'sapiens-video-lite/fast/quality' (Veo 3.1). Args create: model, prompt, durationSec, resolution ('480p'/'720p'/'1080p'), audio, aspectRatio. FRAME INICIAL/FINAL POR REFERÊNCIA (recomendado): startImageId/endImageId (id da sua galeria) ou startImageUrl/endImageUrl (url de galeria/Acervo/personagem) — resolvidos server-side igual à imagem, descubra via sapiens_reference. Alternativa base64: references (role 'start'=imagem i2v, 'end'=frame final, 'driving'=vídeo de movimento do Motion). Suporte a frame final varia por modelo. Custo server-side por config. Sub-action 'generate' (legado): renderiza um imageId de vídeo já criado no site. Retorna {success, url, imageId, cost}. VITRINE (sem custo): sub-action 'demos' lista os SEUS demo films (kind=demo do Estúdio de Vídeo) com slug + estado de vitrine; sub-action 'showcase' põe/tira um demo (por slug) do mini-cinema da /conectar-claude, com showcaseTag (chip de capacidade) e showcaseOrder (ordem asc). Fluxo: 'demos' pra achar o slug, depois 'showcase' com showcase=true. Só entra na vitrine pública se for a conta da casa. create é ASSÍNCRONA: cria o row, debita e volta NA HORA com {imageId, status:'rendering', cost} (não espera o render, que leva de segundos a minutos). Acompanhe com a sub-action 'status' (imageId) até status='completed' (traz a url) ou 'error'/'blocked'. NÃO chame create de novo enquanto renderiza (cria outro vídeo e cobra de novo); falha de provider refunda sozinha. SOM: 'sonorize' (imageId de vídeo SEU completed + prompt do som da cena) gera uma VARIANTE nova com trilha sincronizada (20 Sinapses/s, o original fica intacto; sonorize sempre o original, nunca uma variante). ADMIN: 'shadows' (videoUrl + title) extrai a sombra/depth-map de um vídeo pro Acervo como driving reutilizável; 'shadows-list' lista as sombras prontas. Sub-action 'models' (sem custo, sem login): lista os modelos de vídeo ativos + preço-piso + config (durações/resoluções) + disponibilidade (Omni depende de env).",
|
|
116
|
+
description: "Sapiens Video — gera vídeo (qualquer membro logado; vídeo é caro, cobra as Sinapses da sua conta). Sub-action 'create' (recomendada): escolhe modelo + config e gera num call (cria a row + renderiza). Modelos: 'sapiens-video-seedance' (Seedance 2.0, cena+áudio nativo, 4-15s, 480/720/1080p, t2v/i2v), 'sapiens-video-kling' (Kling 3.0 Pro, anima imagem, 3-15s, sound opcional, i2v/t2v), 'sapiens-video-wan' (WAN 2.5, imagem que fala/canta com áudio+lip-sync, 5/10s, i2v), 'sapiens-video-kling-motion' (Motion transfer: passa o movimento de um vídeo pra uma imagem, PRECISA de pessoa com tronco visível na imagem E no vídeo), 'sapiens-video-shot-mimic' (Shot Mimic: recria o plano/câmera/cortes de um vídeo de referência como cena nova), 'sapiens-video-omni' (Gemini Omni: texto vira vídeo 10s 720p com áudio nativo; NÃO aceita mídia do user, ignora references/durationSec/resolution; editOfImageId aponta um vídeo Omni seu e o prompt edita a MESMA cena, preservando câmera e ambiente), 'sapiens-video-lite/fast/quality' (Veo 3.1). Args create: model, prompt, durationSec, resolution ('480p'/'720p'/'1080p'), audio, aspectRatio. FRAME INICIAL/FINAL POR REFERÊNCIA (recomendado): startImageId/endImageId (id da sua galeria) ou startImageUrl/endImageUrl (url de galeria/Acervo/personagem) — resolvidos server-side igual à imagem, descubra via sapiens_reference. FRAME POR ARQUIVO LOCAL (só no MCP instalado/stdio, não no remoto): startImagePath/endImagePath = caminho absoluto de uma imagem no seu PC (PNG/JPEG/WebP até 8MB); o processo lê o arquivo e sobe como frame inicial/final, igual a subir no gerador do site — 1 imagem inicial + 1 final por vídeo, então pra vários vídeos rode create uma vez por imagem. No remoto use id/url. Alternativa base64: references (role 'start'=imagem i2v, 'end'=frame final, 'driving'=vídeo de movimento do Motion). Suporte a frame final varia por modelo. Custo server-side por config. Sub-action 'generate' (legado): renderiza um imageId de vídeo já criado no site. Retorna {success, url, imageId, cost}. VITRINE (sem custo): sub-action 'demos' lista os SEUS demo films (kind=demo do Estúdio de Vídeo) com slug + estado de vitrine; sub-action 'showcase' põe/tira um demo (por slug) do mini-cinema da /conectar-claude, com showcaseTag (chip de capacidade) e showcaseOrder (ordem asc). Fluxo: 'demos' pra achar o slug, depois 'showcase' com showcase=true. Só entra na vitrine pública se for a conta da casa. VÍDEOS PROGRAMÁTICOS (ADMIN, sem custo): a mesa do Estúdio de Vídeo (/experimentos/films, tabela videoSpecs, 5 kinds: demo | aula-tour | essay | tipografia-musical | dataviz) opera por aqui sem browser — 'film-list' (todos os kinds; filtros filmKind/filmStatus), 'film-get' (spec inteiro por slug), 'film-upsert' (cria/atualiza por slug, idempotente; spec = objeto JSON no shape do 'Copiar spec' da tela, validação no servidor), 'film-status' (produção por slug: filmStatus + videoUrl + durationSecMeasured; o fecho do render é os três num call), 'film-publish' (Acervo aba Fitas + portfólio; exige pronto+URL), 'film-delete' (limpar rascunho). O RENDER do filme segue no agente local (skill /film, repo da casa): o MCP registra e fecha o ciclo, não renderiza. create é ASSÍNCRONA: cria o row, debita e volta NA HORA com {imageId, status:'rendering', cost} (não espera o render, que leva de segundos a minutos). Acompanhe com a sub-action 'status' (imageId) até status='completed' (traz a url) ou 'error'/'blocked'. NÃO chame create de novo enquanto renderiza (cria outro vídeo e cobra de novo); falha de provider refunda sozinha. SOM: 'sonorize' (imageId de vídeo SEU completed + prompt do som da cena) gera uma VARIANTE nova com trilha sincronizada (20 Sinapses/s, o original fica intacto; sonorize sempre o original, nunca uma variante). ADMIN: 'shadows' (videoUrl + title) extrai a sombra/depth-map de um vídeo pro Acervo como driving reutilizável; 'shadows-list' lista as sombras prontas. Sub-action 'models' (sem custo, sem login): lista os modelos de vídeo ativos + preço-piso + config (durações/resoluções) + disponibilidade (Omni depende de env).",
|
|
117
117
|
schema: videoSchema,
|
|
118
118
|
handler: video,
|
|
119
119
|
},
|
|
@@ -212,7 +212,7 @@ Efeito sonoro (sapiens_stock_audio): primeiro procure pronto (action=list kind=s
|
|
|
212
212
|
|
|
213
213
|
Sonorizar clipe (sapiens_video action=sonorize): dá som a um vídeo SEU já gerado (status completed). Passe imageId + prompt descrevendo o som da cena (ambiente, materiais, impactos); sai uma VARIANTE nova com trilha sincronizada ao movimento (MMAudio, 20 Sinapses/s do clipe, mín 100), o original fica intacto. Acompanhe com action=status no imageId NOVO que o sonorize devolve. Não re-sonorize uma variante: sonorize sempre o original.
|
|
214
214
|
|
|
215
|
-
Vídeo (sapiens_video) action=create EXIGE 'model': sapiens-video-seedance | kling | wan | kling-motion | lite | fast | quality | omni. Sem model, falha de cara. O 'omni' (Gemini Omni) gera clipe de 10s 720p com áudio nativo a partir de TEXTO (não aceita imagem/vídeo do user, ignora duração/resolução) e EDITA os próprios vídeos: passe editOfImageId com o imageId de um vídeo Omni já gerado e o prompt vira instrução de edição sobre a MESMA cena (troca item/personagem, preserva câmera e ambiente; cada edição debita como geração nova). É o caminho pra variações com continuidade: gera a base uma vez, edita N vezes. Vídeo é o mais caro: confirme com o usuário antes. action=shadows (ADMIN, 200 Sinapses/segundo, refund na falha): passa videoUrl (URL pública) + title (+ durationSec se souber, pra cobrar proporcional; sem ela, flat ~2000) e o servidor extrai a SOMBRA (mapa de profundidade) do vídeo e guarda no Acervo (Corpo) como deepshadow reutilizável (driving pro Shot Mimic/Kling Motion).
|
|
215
|
+
Vídeo (sapiens_video) action=create EXIGE 'model': sapiens-video-seedance | kling | wan | kling-motion | lite | fast | quality | omni. Sem model, falha de cara. O 'omni' (Gemini Omni) gera clipe de 10s 720p com áudio nativo a partir de TEXTO (não aceita imagem/vídeo do user, ignora duração/resolução) e EDITA os próprios vídeos: passe editOfImageId com o imageId de um vídeo Omni já gerado e o prompt vira instrução de edição sobre a MESMA cena (troca item/personagem, preserva câmera e ambiente; cada edição debita como geração nova). É o caminho pra variações com continuidade: gera a base uma vez, edita N vezes. Vídeo é o mais caro: confirme com o usuário antes. action=shadows (ADMIN, 200 Sinapses/segundo, refund na falha): passa videoUrl (URL pública) + title (+ durationSec se souber, pra cobrar proporcional; sem ela, flat ~2000) e o servidor extrai a SOMBRA (mapa de profundidade) do vídeo e guarda no Acervo (Corpo) como deepshadow reutilizável (driving pro Shot Mimic/Kling Motion). VÍDEOS PROGRAMÁTICOS (Estúdio de Vídeo, tela /experimentos/films): são OUTRA coisa, a mesa de filmes-de-código da casa, CINCO kinds na tabela videoSpecs: demo (UI clonada + câmera), aula-tour (telas reais + narração), essay (fita-ensaio abstrata: partícula/tinta vira símbolo; narração opcional, 16:9 ou 9:16), tipografia-musical (clipe tipográfico, a música dirige o corte) e dataviz (barras/números no tempo). O RENDER não sai deste MCP: um agente local (Claude Code, skill /film) produz o mp4 no repo da casa. Mas o CICLO na plataforma fecha por aqui (ADMIN, sem custo): film-list/film-get puxam os specs, film-upsert registra/atualiza, film-status cola a URL do render e marca pronto, film-publish põe no Acervo. User comum pedindo 'fita-ensaio'/'demo film': aponte pra tela e pro agente local.
|
|
216
216
|
|
|
217
217
|
Imagem (sapiens_image) action=generate: prompt + model + aspectRatio. Referência via referenceImageUrls (galeria/Acervo/personagens) e/ou sourceImageIds. templateSlug aplica um super-prompt travado da casa. PROMPT na regra da casa: full-bleed, sujeito oversized (70%+ do frame), sem "tarot card"/"intimate scale"/moldura/margem. action=models (sem custo, sem login) lista os modelos ativos com o preço atual: consulte quando não tiver certeza do modelo ou do custo.
|
|
218
218
|
|
package/dist/tools/meta.js
CHANGED
|
@@ -37,18 +37,22 @@ const FORMAT_GUIDE = {
|
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
39
|
carrossel_ig: {
|
|
40
|
-
description: "Carrossel Instagram
|
|
40
|
+
description: "Carrossel Instagram editorial (7-9 slides). O editor da pipeline renderiza por TEMPLATE: cada slide é { templateId, slots } (não { title, body } cru — esse shape genérico abre VAZIO no editor). NÃO monte o payload na mão: use sapiens_pipeline action=generate_carousel_production (sourceId), que cria a production E preenche pelo motor da casa, no shape certo, seguindo o artigo. Depois afine com get_carousel/update_carousel (mesmo shape editorial). O create_production com payload cru é legado pra quem já tem o shape editorial pronto.",
|
|
41
41
|
payloadHint: {
|
|
42
|
+
meta: {
|
|
43
|
+
caption: "string (legenda do post)",
|
|
44
|
+
hashtags: ["string"],
|
|
45
|
+
keyword: "string MAIÚSCULA (gatilho do comment-to-DM, ex: QUERO)",
|
|
46
|
+
},
|
|
47
|
+
theme: { palette: "editorial-ink | editorial-dark | editorial-cream | editorial-bone" },
|
|
42
48
|
slides: [
|
|
43
49
|
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
id: "slide-1",
|
|
51
|
+
templateId: "capa-tipografica-b | corpo-texto-puro-a | corpo-citacao-a | corpo-lista-a | corpo-dado-a | corpo-versus-a | corpo-foto-metade-a | cta-artigo-a (catálogo completo + slots por template vem de get_carousel)",
|
|
52
|
+
slots: { "...": "slots do template escolhido (ex: kicker, title, body)" },
|
|
53
|
+
image: "null, ou objeto { url, stockImageId, focal, zoom } nos templates de foto",
|
|
48
54
|
},
|
|
49
55
|
],
|
|
50
|
-
caption: "string",
|
|
51
|
-
hashtags: ["string"],
|
|
52
56
|
},
|
|
53
57
|
},
|
|
54
58
|
tirinha: {
|
package/dist/tools/persona.js
CHANGED
|
@@ -35,15 +35,17 @@ const MBTI_GROUPS = {
|
|
|
35
35
|
ENFP: "NF", INFP: "NF", ENFJ: "NF", INFJ: "NF",
|
|
36
36
|
ENTP: "NT", INTP: "NT", ENTJ: "NT", INTJ: "NT",
|
|
37
37
|
};
|
|
38
|
+
// Nomes autorais da casa (renome 2026-07-18, sync com o site:
|
|
39
|
+
// apps/sapiens/src/app/experimentos/persona-sapiens/data/personas.ts).
|
|
38
40
|
const MBTI_NAMES = {
|
|
39
|
-
INTJ: "
|
|
40
|
-
ENTJ: "
|
|
41
|
-
INFJ: "
|
|
42
|
-
ENFJ: "
|
|
43
|
-
ISTJ: "
|
|
44
|
-
ESTJ: "
|
|
45
|
-
ISTP: "
|
|
46
|
-
ESTP: "
|
|
41
|
+
INTJ: "Cientista", INTP: "Cypherpunk",
|
|
42
|
+
ENTJ: "Fundador", ENTP: "Inventor",
|
|
43
|
+
INFJ: "Editor", INFP: "Poeta",
|
|
44
|
+
ENFJ: "Maestro", ENFP: "Faísca",
|
|
45
|
+
ISTJ: "Arquivista", ISFJ: "Guardião",
|
|
46
|
+
ESTJ: "Produtor", ESFJ: "Anfitrião",
|
|
47
|
+
ISTP: "Mecânico", ISFP: "Artesão",
|
|
48
|
+
ESTP: "Negociador", ESFP: "Streamer",
|
|
47
49
|
};
|
|
48
50
|
// Escala Likert 1..7 (mesma do quiz no site).
|
|
49
51
|
const LIKERT_SCALE = {
|
|
@@ -126,6 +128,28 @@ const QUIZ_QUESTIONS = [
|
|
|
126
128
|
{ id: "jp-11", axis: "JP", text: "Programas de viagem detalhados me deixam tranquilo, não entediado." },
|
|
127
129
|
{ id: "jp-12", axis: "JP", text: "Eu tendo a deixar várias abas mentais abertas ao mesmo tempo." },
|
|
128
130
|
];
|
|
131
|
+
// ============================================================
|
|
132
|
+
// BLOCO DA CASA: par de jogo (8 perguntas de vínculo, OPCIONAIS).
|
|
133
|
+
// Não mudam o tipo; medem COMO a pessoa quer ser acompanhada (2 eixos:
|
|
134
|
+
// Sparring↔Acolhimento, Direção↔Execução) → 1 de 4 modos de parceiro
|
|
135
|
+
// (Mestre de Jogo, Sparring, Farol, Copiloto), que calibra o Sintético dela.
|
|
136
|
+
// SYNC: espelho de data/vinculoQuestions.ts (site) e VINCULO_CONFIG
|
|
137
|
+
// (convex/personalityProfiles.ts, scoring server-side).
|
|
138
|
+
// ============================================================
|
|
139
|
+
const VINCULO_QUESTIONS = [
|
|
140
|
+
{ id: "vn-sa-1", axis: "SA", text: "Rendo mais quando alguém discorda de mim com força." },
|
|
141
|
+
{ id: "vn-sa-2", axis: "SA", text: "Quando travo, o que me destrava é escuta, não cobrança." },
|
|
142
|
+
{ id: "vn-sa-3", axis: "SA", text: "Prefiro um 'isso tá fraco' na cara a um elogio morno." },
|
|
143
|
+
{ id: "vn-sa-4", axis: "SA", text: "Ideia recém-nascida precisa de espaço seguro antes de aguentar pancada." },
|
|
144
|
+
{ id: "vn-de-1", axis: "DE", text: "Quero alguém que aponte o caminho e me deixe caminhar sozinho." },
|
|
145
|
+
{ id: "vn-de-2", axis: "DE", text: "Plano bom é plano que alguém constrói comigo, mão na massa." },
|
|
146
|
+
{ id: "vn-de-3", axis: "DE", text: "Um bom mapa me serve mais que companhia na trilha." },
|
|
147
|
+
{ id: "vn-de-4", axis: "DE", text: "Ideia boa minha morre por falta de alguém segurando a ponta prática." },
|
|
148
|
+
];
|
|
149
|
+
const VINCULO_AXES_INFO = {
|
|
150
|
+
SA: "Sparring vs Acolhimento — a pessoa quer que a confrontem ou que a sustentem.",
|
|
151
|
+
DE: "Direção vs Execução — quer quem aponta o caminho ou quem senta e faz junto.",
|
|
152
|
+
};
|
|
129
153
|
export const personaSchema = z.object({
|
|
130
154
|
action: z.enum([
|
|
131
155
|
"my_profile",
|
|
@@ -146,6 +170,13 @@ export const personaSchema = z.object({
|
|
|
146
170
|
}))
|
|
147
171
|
.optional()
|
|
148
172
|
.describe("Pra action=submit_quiz: as 48 respostas { questionId, value 1..7 }. Pegue os IDs/perguntas com action=get_quiz e colete tudo antes."),
|
|
173
|
+
vinculoAnswers: z
|
|
174
|
+
.array(z.object({
|
|
175
|
+
questionId: z.string(),
|
|
176
|
+
value: z.number().int().min(1).max(7),
|
|
177
|
+
}))
|
|
178
|
+
.optional()
|
|
179
|
+
.describe("Pra action=submit_quiz: as 8 respostas do bloco da casa (par de jogo, ids vn-*). Opcional, mas recomendado: destrava o modo de parceiro e a calibração do Sintético."),
|
|
149
180
|
nome: z
|
|
150
181
|
.string()
|
|
151
182
|
.optional()
|
|
@@ -155,15 +186,21 @@ export async function persona(args) {
|
|
|
155
186
|
// -------- get_quiz: as 48 perguntas pra aplicar conversando (estático) --------
|
|
156
187
|
if (args.action === "get_quiz") {
|
|
157
188
|
return {
|
|
158
|
-
totalQuestions: QUIZ_QUESTIONS.length,
|
|
189
|
+
totalQuestions: QUIZ_QUESTIONS.length + VINCULO_QUESTIONS.length,
|
|
159
190
|
scale: LIKERT_SCALE,
|
|
160
191
|
axes: AXES_INFO,
|
|
161
192
|
instructions: "Aplique conversando: apresente as perguntas (pode ir em blocos por eixo) e " +
|
|
162
193
|
"peça pra pessoa responder de 1 (Discordo totalmente) a 7 (Concordo totalmente). " +
|
|
163
|
-
"Junte TODAS as 48 respostas e chame action=submit_quiz com answers=[{questionId, value}]. " +
|
|
194
|
+
"Junte TODAS as 48 respostas de `questions` e chame action=submit_quiz com answers=[{questionId, value}]. " +
|
|
195
|
+
"Depois das 48, aplique também o BLOCO DA CASA (`vinculoQuestions`, 8 itens, mesma escala): " +
|
|
196
|
+
"ele descobre o par de jogo da pessoa (como ela quer ser acompanhada) e calibra o Sintético dela. " +
|
|
197
|
+
"Essas 8 vão SEPARADAS, no arg vinculoAnswers. " +
|
|
164
198
|
"Não calcule o resultado você mesmo: o scoring é server-side.",
|
|
165
199
|
questions: QUIZ_QUESTIONS,
|
|
166
|
-
|
|
200
|
+
vinculoQuestions: VINCULO_QUESTIONS,
|
|
201
|
+
vinculoAxes: VINCULO_AXES_INFO,
|
|
202
|
+
howToSubmit: "sapiens_persona action=submit_quiz answers=[{questionId:'ei-1', value:5}, ...] (48 itens) " +
|
|
203
|
+
"vinculoAnswers=[{questionId:'vn-sa-1', value:6}, ...] (8 itens, opcional).",
|
|
167
204
|
};
|
|
168
205
|
}
|
|
169
206
|
// -------- submit_quiz: salva o resultado no perfil do user (de graça) --------
|
|
@@ -177,6 +214,10 @@ export async function persona(args) {
|
|
|
177
214
|
const res = await convexMutation("mcpExtras:mcpSubmitPersonaQuiz", {
|
|
178
215
|
sessionToken,
|
|
179
216
|
answers: answers.map((a) => ({ questionId: a.questionId, value: a.value })),
|
|
217
|
+
// Bloco da casa (par de jogo): opcional; só vai se coletado completo.
|
|
218
|
+
...(args.vinculoAnswers?.length
|
|
219
|
+
? { vinculoAnswers: args.vinculoAnswers.map((a) => ({ questionId: a.questionId, value: a.value })) }
|
|
220
|
+
: {}),
|
|
180
221
|
nome: args.nome,
|
|
181
222
|
});
|
|
182
223
|
return {
|
|
@@ -205,10 +246,10 @@ export async function persona(args) {
|
|
|
205
246
|
group: MBTI_GROUPS[c],
|
|
206
247
|
})),
|
|
207
248
|
groups: {
|
|
208
|
-
NT: "
|
|
209
|
-
NF: "
|
|
210
|
-
SJ: "
|
|
211
|
-
SP: "
|
|
249
|
+
NT: "Direção (traça o rumo, pensa em sistemas)",
|
|
250
|
+
NF: "Roteiro (escreve o sentido, pensa em pessoas)",
|
|
251
|
+
SJ: "Produção (faz rodar, pensa em ordem)",
|
|
252
|
+
SP: "Cena (joga em tempo real, pensa em sensação)",
|
|
212
253
|
},
|
|
213
254
|
};
|
|
214
255
|
}
|
package/dist/tools/pipeline.js
CHANGED
|
@@ -43,6 +43,12 @@ export const pipelineSchema = z.object({
|
|
|
43
43
|
"propose_mega_grafico_plan",
|
|
44
44
|
"run_mega_grafico_full",
|
|
45
45
|
"generate_carousel",
|
|
46
|
+
"generate_carousel_production",
|
|
47
|
+
"list_carousels",
|
|
48
|
+
"get_carousel",
|
|
49
|
+
"update_carousel",
|
|
50
|
+
"carousel_auto_images",
|
|
51
|
+
"carousel_generate_image",
|
|
46
52
|
]),
|
|
47
53
|
// propose_mega_grafico_plan / run_mega_grafico_full
|
|
48
54
|
withHelen: z
|
|
@@ -83,6 +89,31 @@ export const pipelineSchema = z.object({
|
|
|
83
89
|
.boolean()
|
|
84
90
|
.optional()
|
|
85
91
|
.describe("generate_carousel: se TRUE, a voz do Sintético do dono dirige o copy (amplificar). Default false = estilo do dono."),
|
|
92
|
+
// carrossel standalone (get/update/imagens)
|
|
93
|
+
carouselId: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("carousels_standalone:_id — get_carousel/update_carousel/carousel_auto_images/carousel_generate_image. Vem de generate_carousel ou list_carousels."),
|
|
97
|
+
slideId: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("carousel_generate_image: id do slide (ex: 'slide-3'), do payload do get_carousel."),
|
|
101
|
+
force: z
|
|
102
|
+
.boolean()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("carousel_auto_images: TRUE substitui as imagens já aplicadas por escolhas novas. Default false = preenche só o que falta."),
|
|
105
|
+
makeVisual: z
|
|
106
|
+
.boolean()
|
|
107
|
+
.optional()
|
|
108
|
+
.describe("carousel_auto_images: TRUE converte slides de texto pra layouts de foto antes de escolher (ritmo visual pro carrossel texto-pesado). O texto fica, muda a moldura."),
|
|
109
|
+
generateMissing: z
|
|
110
|
+
.boolean()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe("carousel_auto_images: TRUE gera imagem NOVA (nanoBanana, ~450 Sinapses cada, até 4) pros slides que o banco não cobriu. O fallback stock→gerador. Avise o usuário do custo antes."),
|
|
113
|
+
customPrompt: z
|
|
114
|
+
.string()
|
|
115
|
+
.optional()
|
|
116
|
+
.describe("carousel_generate_image: direção extra da cena (opcional; sem ela a IA usa o texto do slide)."),
|
|
86
117
|
// list_articles
|
|
87
118
|
includeDrafts: z.boolean().optional(),
|
|
88
119
|
includeArchived: z.boolean().optional(),
|
|
@@ -94,7 +125,7 @@ export const pipelineSchema = z.object({
|
|
|
94
125
|
sourceId: z
|
|
95
126
|
.string()
|
|
96
127
|
.optional()
|
|
97
|
-
.describe("contentSources:_id — get_source/create_production/remove_source/set_source_done/update_source_notes. Vem de list_sources/add_article_as_source."),
|
|
128
|
+
.describe("contentSources:_id — get_source/create_production/generate_carousel_production/remove_source/set_source_done/update_source_notes. Vem de list_sources/add_article_as_source."),
|
|
98
129
|
productionId: z
|
|
99
130
|
.string()
|
|
100
131
|
.optional()
|
|
@@ -353,5 +384,62 @@ export async function pipeline(args) {
|
|
|
353
384
|
useSintetico: args.useSintetico,
|
|
354
385
|
});
|
|
355
386
|
}
|
|
387
|
+
case "generate_carousel_production": {
|
|
388
|
+
// Gera um carrossel EDITORIAL como PRODUCTION da pipeline, a partir do
|
|
389
|
+
// artigo de um source (sourceId de list_sources/add_article_as_source):
|
|
390
|
+
// cria a production carrossel_ig e a preenche pelo MESMO motor do editor
|
|
391
|
+
// (shape templateId+slots), então o resultado JÁ abre pronto no editor de
|
|
392
|
+
// pipeline, ao contrário do create_production cru. ADMIN-ONLY, cobra
|
|
393
|
+
// Sinapses (reembolsa se falhar). Devolve productionId + url do editor.
|
|
394
|
+
// SÍNCRONA e pesada (Gemini + imagens): vale a REGRA DO TIMEOUT — se voltar
|
|
395
|
+
// Timeout, confira em get_production/list_sources antes de repetir.
|
|
396
|
+
return await convexAction("carouselAutofill:mcpFillCarouselFromArticle", {
|
|
397
|
+
sessionToken,
|
|
398
|
+
sourceId: need(args.sourceId, "sourceId"),
|
|
399
|
+
autoPickImages: args.autoPickImages,
|
|
400
|
+
useSintetico: args.useSintetico,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
case "list_carousels":
|
|
404
|
+
// Carrosséis standalone do dono (id + título + url do editor).
|
|
405
|
+
return await convexQuery("carouselMcp:mcpListCarousels", { sessionToken });
|
|
406
|
+
case "get_carousel":
|
|
407
|
+
// Devolve payload completo + catálogo de templates (id/slots/limites) +
|
|
408
|
+
// paletas válidas: tudo que você precisa pra ESCREVER os slides direto.
|
|
409
|
+
return await convexQuery("carouselMcp:mcpGetCarousel", {
|
|
410
|
+
sessionToken,
|
|
411
|
+
carouselId: need(args.carouselId, "carouselId"),
|
|
412
|
+
});
|
|
413
|
+
case "update_carousel":
|
|
414
|
+
// Payload INTEIRO (mesmo shape do get_carousel). Sanitizado no servidor.
|
|
415
|
+
// Preserve os campos image dos slides que você não mexeu — mandar slide
|
|
416
|
+
// sem image derruba a foto dele.
|
|
417
|
+
return await convexMutation("carouselMcp:mcpUpdateCarousel", {
|
|
418
|
+
sessionToken,
|
|
419
|
+
carouselId: need(args.carouselId, "carouselId"),
|
|
420
|
+
payload: coercePayloadArg(need(args.payload, "payload")),
|
|
421
|
+
...(args.title ? { title: args.title } : {}),
|
|
422
|
+
});
|
|
423
|
+
case "carousel_auto_images":
|
|
424
|
+
// IA escolhe imagens do banco pros slides de foto sem imagem (60 Sinapses).
|
|
425
|
+
// makeVisual dá ritmo a carrossel texto-pesado; generateMissing gera as que
|
|
426
|
+
// o banco não cobriu (custo por imagem). SÍNCRONA e pesada com
|
|
427
|
+
// generateMissing: vale a REGRA DO TIMEOUT.
|
|
428
|
+
return await convexAction("carouselAutofill:mcpCarouselAutoImages", {
|
|
429
|
+
sessionToken,
|
|
430
|
+
carouselId: need(args.carouselId, "carouselId"),
|
|
431
|
+
force: args.force,
|
|
432
|
+
makeVisual: args.makeVisual,
|
|
433
|
+
generateMissing: args.generateMissing,
|
|
434
|
+
});
|
|
435
|
+
case "carousel_generate_image":
|
|
436
|
+
// Gera imagem NOVA pra UM slide (nanoBanana, ~450 Sinapses) e salva no
|
|
437
|
+
// carrossel. SÍNCRONA (~20-60s): vale a REGRA DO TIMEOUT.
|
|
438
|
+
return await convexAction("carouselAutofill:mcpCarouselGenerateImage", {
|
|
439
|
+
sessionToken,
|
|
440
|
+
carouselId: need(args.carouselId, "carouselId"),
|
|
441
|
+
slideId: need(args.slideId, "slideId"),
|
|
442
|
+
...(args.customPrompt ? { customPrompt: args.customPrompt } : {}),
|
|
443
|
+
});
|
|
356
444
|
}
|
|
357
445
|
}
|
package/dist/tools/video.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { convexAction, convexQuery, convexMutation, getSessionToken } from "../convexClient.js";
|
|
2
|
+
import { convexAction, convexQuery, convexMutation, getSessionToken, isRemoteContext, } from "../convexClient.js";
|
|
3
3
|
import { httpUrl } from "../schema.js";
|
|
4
4
|
/**
|
|
5
5
|
* Sapiens Video — gera vídeo (qualquer membro logado; vídeo é caro, cobra as
|
|
@@ -25,14 +25,28 @@ import { httpUrl } from "../schema.js";
|
|
|
25
25
|
* som da cena -> VARIANTE nova com trilha sincronizada (o original
|
|
26
26
|
* fica intacto). Cobra por segundo do clipe; poll via action=status.
|
|
27
27
|
*
|
|
28
|
+
* Vídeos Programáticos (Estúdio de Vídeo, /experimentos/films) — ADMIN, sem custo.
|
|
29
|
+
* A mesa de specs da casa (tabela videoSpecs, 5 kinds: demo | aula-tour | essay |
|
|
30
|
+
* tipografia-musical | dataviz). A PRODUÇÃO do mp4 segue no agente local (skill
|
|
31
|
+
* /film, repo films/); estas actions fecham o CICLO na plataforma sem browser:
|
|
32
|
+
* - film-list: lista os specs (todos os kinds; filtros filmKind/filmStatus).
|
|
33
|
+
* - film-get: um spec inteiro por slug (payload pronto pra produção).
|
|
34
|
+
* - film-upsert: cria/atualiza spec por slug (idempotente; validação server).
|
|
35
|
+
* - film-status: status/URL do render/duração por slug (fecho: pronto+URL).
|
|
36
|
+
* - film-publish: publica/despublica no Acervo (aba Fitas) + portfólio.
|
|
37
|
+
* - film-delete: apaga um spec (limpar rascunho/duplicata).
|
|
38
|
+
*
|
|
28
39
|
* Modelos (action=create):
|
|
29
40
|
* - sapiens-video-seedance Seedance 2.0 — cena com áudio nativo, 4-15s, 480/720/1080p (t2v/i2v)
|
|
30
41
|
* - sapiens-video-kling Kling 3.0 Pro — dá vida a uma imagem, 3-15s, sound opcional (i2v/t2v)
|
|
31
42
|
* - sapiens-video-wan WAN 2.5 — imagem que fala/canta (áudio+lip-sync nativo), 5/10s (i2v)
|
|
32
43
|
* - sapiens-video-kling-motion Kling Motion — transfere o movimento de um vídeo pra uma imagem
|
|
33
|
-
* (PRECISA de pessoa com tronco visível na imagem E no vídeo
|
|
44
|
+
* (PRECISA de pessoa com tronco visível na imagem E no vídeo;
|
|
45
|
+
* vídeo de referência MÁX 10s — o provider recusa acima disso —
|
|
46
|
+
* e cobra pela duração do clipe; corte antes de mandar)
|
|
34
47
|
* - sapiens-video-shot-mimic Shot Mimic — recria o plano do vídeo de referência (câmera,
|
|
35
48
|
* cortes, blocking) como cena nova; personagem via role 'start'
|
|
49
|
+
* (vídeo de referência MÁX 15s — acima o provider corta em 15s)
|
|
36
50
|
* - sapiens-video-lite/fast/quality Veo 3.1 (2000/5000/25000 sinapses)
|
|
37
51
|
* - sapiens-video-omni Gemini Omni — texto -> vídeo 10s 720p com áudio nativo embutido.
|
|
38
52
|
* t2v + EDIÇÃO conversacional: `editOfImageId` aponta um vídeo Omni
|
|
@@ -56,12 +70,56 @@ const VIDEO_MODELS = [
|
|
|
56
70
|
"sapiens-video-quality",
|
|
57
71
|
"sapiens-video-omni",
|
|
58
72
|
];
|
|
73
|
+
const FILM_KINDS = ["demo", "aula-tour", "essay", "tipografia-musical", "dataviz"];
|
|
74
|
+
const FILM_STATUSES = ["rascunho", "na_fila", "renderizando", "pronto"];
|
|
59
75
|
export const videoSchema = z.object({
|
|
60
|
-
action: z.enum([
|
|
76
|
+
action: z.enum([
|
|
77
|
+
"create",
|
|
78
|
+
"generate",
|
|
79
|
+
"status",
|
|
80
|
+
"models",
|
|
81
|
+
"demos",
|
|
82
|
+
"showcase",
|
|
83
|
+
"shadows",
|
|
84
|
+
"shadows-list",
|
|
85
|
+
"sonorize",
|
|
86
|
+
"film-list",
|
|
87
|
+
"film-get",
|
|
88
|
+
"film-upsert",
|
|
89
|
+
"film-status",
|
|
90
|
+
"film-publish",
|
|
91
|
+
"film-delete",
|
|
92
|
+
]),
|
|
93
|
+
// --- Vídeos Programáticos (film-*): mesa de specs da casa, ADMIN, sem custo ---
|
|
94
|
+
filmKind: z
|
|
95
|
+
.enum(FILM_KINDS)
|
|
96
|
+
.optional()
|
|
97
|
+
.describe("film-list: filtra por kind (demo | aula-tour | essay | tipografia-musical | dataviz)."),
|
|
98
|
+
filmStatus: z
|
|
99
|
+
.enum(FILM_STATUSES)
|
|
100
|
+
.optional()
|
|
101
|
+
.describe("film-status: novo status de produção (rascunho | na_fila | renderizando | pronto). " +
|
|
102
|
+
"film-list: filtra por status. O fecho do render é film-status com filmStatus='pronto' + videoUrl + durationSec."),
|
|
103
|
+
spec: z
|
|
104
|
+
.record(z.any())
|
|
105
|
+
.optional()
|
|
106
|
+
.describe("film-upsert: o spec inteiro como objeto JSON (validação fica no servidor, fonte única). " +
|
|
107
|
+
"Shape: { kind, musicMode ('default'|'file'|'track'), musicRef?, aulaSlug? (só aula-tour), " +
|
|
108
|
+
"e o payload do kind: demo | aulaTour | essay | tipoMusical | dataviz }. " +
|
|
109
|
+
"Mesmo shape do 'Copiar spec' da tela /experimentos/films; descubra um exemplo real com film-get."),
|
|
110
|
+
published: z
|
|
111
|
+
.boolean()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("film-publish: true publica no Acervo (aba Fitas) + portfólio (exige pronto + videoUrl), false despublica."),
|
|
114
|
+
durationSecMeasured: z
|
|
115
|
+
.number()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe("film-status: duração MEDIDA do render em segundos (ffprobe), vira a duração do card."),
|
|
61
118
|
// --- action=shadows (deepshadows: extrai o mapa de profundidade de um vídeo) ---
|
|
62
119
|
videoUrl: httpUrl()
|
|
63
120
|
.optional()
|
|
64
|
-
.describe("action=shadows: URL pública (http/https) do vídeo-fonte. O servidor extrai a SOMBRA (depth) e guarda no Acervo (Corpo). ADMIN, 200 Sinapses/segundo (refund na falha)."
|
|
121
|
+
.describe("action=shadows: URL pública (http/https) do vídeo-fonte. O servidor extrai a SOMBRA (depth) e guarda no Acervo (Corpo). ADMIN, 200 Sinapses/segundo (refund na falha). " +
|
|
122
|
+
"film-status: a URL https do render no CDN (Bunny), o que acende o player do card."),
|
|
65
123
|
title: z
|
|
66
124
|
.string()
|
|
67
125
|
.optional()
|
|
@@ -74,7 +132,9 @@ export const videoSchema = z.object({
|
|
|
74
132
|
slug: z
|
|
75
133
|
.string()
|
|
76
134
|
.optional()
|
|
77
|
-
.describe("action=showcase: slug do demo film a curar (descubra via action=demos)."
|
|
135
|
+
.describe("action=showcase: slug do demo film a curar (descubra via action=demos). " +
|
|
136
|
+
"film-get/film-status/film-publish/film-delete: slug do spec (descubra via film-list). " +
|
|
137
|
+
"film-upsert: slug fixo do filme no repo films/ (idempotente: existe = atualiza, não existe = cria com esse slug); omita pra criar com slug gerado."),
|
|
78
138
|
showcase: z
|
|
79
139
|
.boolean()
|
|
80
140
|
.optional()
|
|
@@ -93,8 +153,8 @@ export const videoSchema = z.object({
|
|
|
93
153
|
.optional()
|
|
94
154
|
.describe("action=create: modelo de vídeo. 'sapiens-video-seedance' (cinematográfico+áudio, t2v/i2v), " +
|
|
95
155
|
"'sapiens-video-kling' (anima imagem, i2v/t2v), 'sapiens-video-wan' (imagem que fala, i2v), " +
|
|
96
|
-
"'sapiens-video-kling-motion' (motion transfer, precisa pessoa na imagem E no vídeo de movimento), " +
|
|
97
|
-
"'sapiens-video-shot-mimic' (recria o plano do vídeo de referência com seu personagem: mesma câmera, mesmos cortes; 'driving' = previs/clipe do plano, 'start' = personagem), " +
|
|
156
|
+
"'sapiens-video-kling-motion' (motion transfer, precisa pessoa na imagem E no vídeo de movimento; vídeo de referência MÁX 10s, cobra pela duração do clipe), " +
|
|
157
|
+
"'sapiens-video-shot-mimic' (recria o plano do vídeo de referência com seu personagem: mesma câmera, mesmos cortes; 'driving' = previs/clipe do plano MÁX 15s, 'start' = personagem), " +
|
|
98
158
|
"'sapiens-video-lite/fast/quality' (Veo 3.1), " +
|
|
99
159
|
"'sapiens-video-omni' (Gemini Omni: texto -> vídeo 10s 720p com áudio nativo; t2v + EDIÇÃO conversacional via editOfImageId; não aceita imagem/vídeo do user, ignora duração/resolução)."),
|
|
100
160
|
durationSec: z
|
|
@@ -145,7 +205,7 @@ export const videoSchema = z.object({
|
|
|
145
205
|
.describe("'start' = imagem inicial (i2v), 'end' = frame final, 'driving' = vídeo de movimento (Motion)"),
|
|
146
206
|
}))
|
|
147
207
|
.optional()
|
|
148
|
-
.describe("References em base64 (escape hatch / Motion / Shot Mimic). i2v: role 'start' (imagem). Motion: 'start' (pessoa) + 'driving' (vídeo de movimento, <=5MB). Shot Mimic: 'start' (personagem) + 'driving' (previs ou clipe do plano a imitar, <=5MB). Pra frame inicial/final a partir do seu acervo, prefira start/endImage* abaixo (sem precisar de base64)."),
|
|
208
|
+
.describe("References em base64 (escape hatch / Motion / Shot Mimic). i2v: role 'start' (imagem). Motion: 'start' (pessoa) + 'driving' (vídeo de movimento, <=5MB, MÁX 10s — o Kling Motion recusa referência acima de 10s e cobra pela duração do clipe; corte o trecho antes). Shot Mimic: 'start' (personagem) + 'driving' (previs ou clipe do plano a imitar, <=5MB, MÁX 15s — acima o provider corta em 15s). Pra frame inicial/final a partir do seu acervo, prefira start/endImage* abaixo (sem precisar de base64)."),
|
|
149
209
|
// Frame inicial/final por REFERÊNCIA (resolvido server-side, igual à imagem):
|
|
150
210
|
// id da própria galeria OU url allowlist (galeria/Acervo/personagens). Mais
|
|
151
211
|
// simples que mandar base64. Descubra via sapiens_reference / sapiens_gallery.
|
|
@@ -163,7 +223,91 @@ export const videoSchema = z.object({
|
|
|
163
223
|
endImageUrl: httpUrl()
|
|
164
224
|
.optional()
|
|
165
225
|
.describe("Frame FINAL: url pública de galeria/Acervo/personagem. Vira reference role 'end' (suporte varia por modelo)."),
|
|
226
|
+
// Frame inicial/final por ARQUIVO LOCAL (paridade com o upload do gerador do
|
|
227
|
+
// site). Só no MCP instalado (stdio): o processo lê o arquivo do disco e sobe
|
|
228
|
+
// como reference role 'start'/'end', sem o base64 passar pelo contexto do
|
|
229
|
+
// modelo. No remoto é recusado (o path seria do servidor, não do usuário).
|
|
230
|
+
startImagePath: z
|
|
231
|
+
.string()
|
|
232
|
+
.optional()
|
|
233
|
+
.describe("Frame INICIAL (i2v) a partir de um ARQUIVO LOCAL do seu PC — só no MCP instalado (stdio), não na conexão remota. " +
|
|
234
|
+
"Passe o caminho absoluto (ex: 'C:\\\\Users\\\\voce\\\\HERO\\\\1.png'); o processo lê o arquivo e sobe como frame inicial, igual a subir a imagem no gerador do site. PNG/JPEG/WebP, até 8MB. " +
|
|
235
|
+
"É 1 imagem inicial por vídeo (o modelo do site): pra vários vídeos, rode create uma vez por imagem. Mutuamente exclusivo com startImageId/startImageUrl."),
|
|
236
|
+
endImagePath: z
|
|
237
|
+
.string()
|
|
238
|
+
.optional()
|
|
239
|
+
.describe("Frame FINAL a partir de um ARQUIVO LOCAL do seu PC — só no MCP instalado (stdio). Caminho absoluto; PNG/JPEG/WebP até 8MB. Vira reference role 'end' (suporte varia por modelo). 1 imagem por vídeo. Mutuamente exclusivo com endImageId/endImageUrl."),
|
|
166
240
|
});
|
|
241
|
+
// Teto do arquivo local que vira frame de vídeo. Frame inicial/final não precisa
|
|
242
|
+
// ser pesado; 8MB cobre um PNG/JPEG grande com folga e evita estourar o payload
|
|
243
|
+
// da action (base64 infla ~33%).
|
|
244
|
+
const MAX_LOCAL_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
245
|
+
// Detecta o mime por MAGIC BYTES (não confia na extensão). Frame de vídeo aceita
|
|
246
|
+
// só PNG/JPEG/WebP; qualquer outra coisa é recusada com erro claro.
|
|
247
|
+
export function detectImageMime(buf) {
|
|
248
|
+
if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
249
|
+
return "image/png";
|
|
250
|
+
}
|
|
251
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
252
|
+
return "image/jpeg";
|
|
253
|
+
}
|
|
254
|
+
if (buf.length >= 12 &&
|
|
255
|
+
buf.toString("ascii", 0, 4) === "RIFF" &&
|
|
256
|
+
buf.toString("ascii", 8, 12) === "WEBP") {
|
|
257
|
+
return "image/webp";
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
// Lê um arquivo de imagem do disco e devolve uma reference base64 pro role dado.
|
|
262
|
+
// Guarda de segurança: só no stdio (o processo roda na máquina do dono do token).
|
|
263
|
+
// No remoto recusa ANTES de tocar o disco — ler um path arbitrário lá seria
|
|
264
|
+
// leitura de arquivo do servidor multi-tenant.
|
|
265
|
+
async function readLocalImageAsReference(filePath, role) {
|
|
266
|
+
if (isRemoteContext()) {
|
|
267
|
+
throw new Error(`${role}ImagePath (arquivo local) só funciona no MCP instalado na sua máquina (stdio). ` +
|
|
268
|
+
`Na conexão remota, suba a imagem no site e passe ${role}ImageId (id da galeria) ou ${role}ImageUrl (host Sapiens).`);
|
|
269
|
+
}
|
|
270
|
+
const [{ default: fs }, { default: path }] = await Promise.all([
|
|
271
|
+
import("node:fs/promises"),
|
|
272
|
+
import("node:path"),
|
|
273
|
+
]);
|
|
274
|
+
const abs = path.resolve(filePath);
|
|
275
|
+
let buf;
|
|
276
|
+
try {
|
|
277
|
+
buf = await fs.readFile(abs);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
throw new Error(`Não achei/li o arquivo em ${role}ImagePath: "${filePath}". Use o caminho absoluto do arquivo (ex: C:\\Users\\voce\\HERO\\1.png).`);
|
|
281
|
+
}
|
|
282
|
+
if (buf.length > MAX_LOCAL_IMAGE_BYTES) {
|
|
283
|
+
const mb = (buf.length / (1024 * 1024)).toFixed(1);
|
|
284
|
+
throw new Error(`Imagem de ${role} muito grande (${mb}MB; teto ${Math.round(MAX_LOCAL_IMAGE_BYTES / (1024 * 1024))}MB). Exporte menor e tente de novo.`);
|
|
285
|
+
}
|
|
286
|
+
const mimeType = detectImageMime(buf);
|
|
287
|
+
if (!mimeType) {
|
|
288
|
+
throw new Error(`O arquivo de ${role} não parece PNG/JPEG/WebP ("${filePath}"). O frame inicial/final aceita só imagem nesses formatos.`);
|
|
289
|
+
}
|
|
290
|
+
return { mimeType, data: buf.toString("base64"), role };
|
|
291
|
+
}
|
|
292
|
+
// Monta as references de frame inicial/final a partir de arquivos locais,
|
|
293
|
+
// barrando o conflito com id/url (o site usa UMA via por frame: ou o arquivo,
|
|
294
|
+
// ou a imagem já hospedada). Devolve [] quando nenhum path foi passado.
|
|
295
|
+
export async function localFrameReferences(args) {
|
|
296
|
+
const refs = [];
|
|
297
|
+
if (args.startImagePath) {
|
|
298
|
+
if (args.startImageId || args.startImageUrl) {
|
|
299
|
+
throw new Error("Frame inicial: escolha UMA via — startImagePath (arquivo local) OU startImageId/startImageUrl (galeria/Acervo), não as duas.");
|
|
300
|
+
}
|
|
301
|
+
refs.push(await readLocalImageAsReference(args.startImagePath, "start"));
|
|
302
|
+
}
|
|
303
|
+
if (args.endImagePath) {
|
|
304
|
+
if (args.endImageId || args.endImageUrl) {
|
|
305
|
+
throw new Error("Frame final: escolha UMA via — endImagePath (arquivo local) OU endImageId/endImageUrl (galeria/Acervo), não as duas.");
|
|
306
|
+
}
|
|
307
|
+
refs.push(await readLocalImageAsReference(args.endImagePath, "end"));
|
|
308
|
+
}
|
|
309
|
+
return refs;
|
|
310
|
+
}
|
|
167
311
|
export async function video(args) {
|
|
168
312
|
// models: catálogo VIVO dos modelos de vídeo (ativos + preço-piso/config +
|
|
169
313
|
// override admin + disponibilidade). Público, sem custo e sem login — antes de
|
|
@@ -196,6 +340,69 @@ export async function video(args) {
|
|
|
196
340
|
if (args.action === "demos") {
|
|
197
341
|
return await convexQuery("videoSpecs:mcpListMyDemoFilms", { sessionToken });
|
|
198
342
|
}
|
|
343
|
+
// --- Vídeos Programáticos (mesa de specs, ADMIN, sem custo) ---
|
|
344
|
+
if (args.action === "film-list") {
|
|
345
|
+
return await convexQuery("videoSpecs:mcpListMyVideoSpecs", {
|
|
346
|
+
sessionToken,
|
|
347
|
+
kind: args.filmKind,
|
|
348
|
+
status: args.filmStatus,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (args.action === "film-get") {
|
|
352
|
+
if (!args.slug)
|
|
353
|
+
throw new Error("film-get exige slug (descubra via film-list).");
|
|
354
|
+
return await convexQuery("videoSpecs:mcpGetVideoSpec", { sessionToken, slug: args.slug });
|
|
355
|
+
}
|
|
356
|
+
if (args.action === "film-upsert") {
|
|
357
|
+
if (!args.spec || typeof args.spec !== "object") {
|
|
358
|
+
throw new Error("film-upsert exige spec (objeto JSON com kind + musicMode + payload do kind: demo | aulaTour | essay | tipoMusical | dataviz).");
|
|
359
|
+
}
|
|
360
|
+
const { kind, musicMode, musicRef, aulaSlug, demo, aulaTour, essay, tipoMusical, dataviz } = args.spec;
|
|
361
|
+
return await convexMutation("videoSpecs:mcpUpsertVideoSpec", {
|
|
362
|
+
sessionToken,
|
|
363
|
+
slug: args.slug,
|
|
364
|
+
kind,
|
|
365
|
+
musicMode: musicMode ?? "default",
|
|
366
|
+
musicRef,
|
|
367
|
+
aulaSlug,
|
|
368
|
+
demo,
|
|
369
|
+
aulaTour,
|
|
370
|
+
essay,
|
|
371
|
+
tipoMusical,
|
|
372
|
+
dataviz,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (args.action === "film-status") {
|
|
376
|
+
if (!args.slug)
|
|
377
|
+
throw new Error("film-status exige slug (descubra via film-list).");
|
|
378
|
+
if (args.filmStatus === undefined && args.videoUrl === undefined && args.durationSecMeasured === undefined) {
|
|
379
|
+
throw new Error("film-status: passe filmStatus, videoUrl e/ou durationSecMeasured (o fecho do render é os três).");
|
|
380
|
+
}
|
|
381
|
+
return await convexMutation("videoSpecs:mcpSetVideoStatus", {
|
|
382
|
+
sessionToken,
|
|
383
|
+
slug: args.slug,
|
|
384
|
+
status: args.filmStatus,
|
|
385
|
+
videoUrl: args.videoUrl,
|
|
386
|
+
durationSec: args.durationSecMeasured,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (args.action === "film-publish") {
|
|
390
|
+
if (!args.slug)
|
|
391
|
+
throw new Error("film-publish exige slug (descubra via film-list).");
|
|
392
|
+
if (typeof args.published !== "boolean") {
|
|
393
|
+
throw new Error("film-publish exige published (true publica no Acervo, false despublica).");
|
|
394
|
+
}
|
|
395
|
+
return await convexMutation("videoSpecs:mcpSetVideoPublished", {
|
|
396
|
+
sessionToken,
|
|
397
|
+
slug: args.slug,
|
|
398
|
+
published: args.published,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
if (args.action === "film-delete") {
|
|
402
|
+
if (!args.slug)
|
|
403
|
+
throw new Error("film-delete exige slug (descubra via film-list).");
|
|
404
|
+
return await convexMutation("videoSpecs:mcpDeleteVideoSpec", { sessionToken, slug: args.slug });
|
|
405
|
+
}
|
|
199
406
|
if (args.action === "showcase") {
|
|
200
407
|
if (!args.slug) {
|
|
201
408
|
throw new Error("action=showcase exige slug (descubra via action=demos).");
|
|
@@ -237,6 +444,13 @@ export async function video(args) {
|
|
|
237
444
|
if (!args.model) {
|
|
238
445
|
throw new Error("action=create exige model (ex: sapiens-video-seedance, sapiens-video-kling, sapiens-video-wan, sapiens-video-kling-motion).");
|
|
239
446
|
}
|
|
447
|
+
// Frame inicial/final por arquivo local (paridade com o upload do site):
|
|
448
|
+
// lê do disco e injeta em references role start/end. O backend
|
|
449
|
+
// (buildVideoReferences) já mescla references + start/end e valida o teto.
|
|
450
|
+
const localRefs = await localFrameReferences(args);
|
|
451
|
+
const references = localRefs.length
|
|
452
|
+
? [...(args.references ?? []), ...localRefs]
|
|
453
|
+
: args.references;
|
|
240
454
|
return await convexAction("mcpExtrasActions:mcpVideoCreateAndRender", {
|
|
241
455
|
sessionToken,
|
|
242
456
|
model: args.model,
|
|
@@ -245,7 +459,7 @@ export async function video(args) {
|
|
|
245
459
|
durationSec: args.durationSec,
|
|
246
460
|
resolution: args.resolution,
|
|
247
461
|
audio: args.audio,
|
|
248
|
-
references
|
|
462
|
+
references,
|
|
249
463
|
startImageId: args.startImageId,
|
|
250
464
|
startImageUrl: args.startImageUrl,
|
|
251
465
|
endImageId: args.endImageId,
|
|
@@ -277,12 +491,16 @@ export async function video(args) {
|
|
|
277
491
|
if (!args.imageId) {
|
|
278
492
|
throw new Error("action=generate exige imageId (vídeo já criado no site).");
|
|
279
493
|
}
|
|
494
|
+
const localRefs = await localFrameReferences(args);
|
|
495
|
+
const references = localRefs.length
|
|
496
|
+
? [...(args.references ?? []), ...localRefs]
|
|
497
|
+
: args.references;
|
|
280
498
|
return await convexAction("mcpExtrasActions:mcpVideoGenerate", {
|
|
281
499
|
sessionToken,
|
|
282
500
|
imageId: args.imageId,
|
|
283
501
|
prompt: args.prompt,
|
|
284
502
|
aspectRatio: args.aspectRatio,
|
|
285
|
-
references
|
|
503
|
+
references,
|
|
286
504
|
startImageId: args.startImageId,
|
|
287
505
|
startImageUrl: args.startImageUrl,
|
|
288
506
|
endImageId: args.endImageId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sapiens-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.36.0",
|
|
4
4
|
"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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|