sagadeck 1.2.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/LICENSE +21 -0
- package/README.md +305 -0
- package/SKILL.md +148 -0
- package/bin/sagadeck.js +511 -0
- package/docs/ANDAMENTO.md +1255 -0
- package/docs/EXPERIENCIAS.md +39 -0
- package/docs/PLANO-EXPERIENCIAS.md +39 -0
- package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
- package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
- package/docs/REFERENCIA.md +1131 -0
- package/package.json +59 -0
- package/src/adaptive-layouts.js +18 -0
- package/src/ai/art-direction.js +8 -0
- package/src/ai/commands.js +130 -0
- package/src/ai/context.js +245 -0
- package/src/ai/deck-ai.js +1135 -0
- package/src/ai/directions.js +74 -0
- package/src/ai/document-materials.js +49 -0
- package/src/ai/document-visuals.js +215 -0
- package/src/ai/evaluation.js +31 -0
- package/src/ai/ground.js +55 -0
- package/src/ai/llm.js +292 -0
- package/src/ai/quality.js +51 -0
- package/src/ai/relay.js +45 -0
- package/src/ai/staged-generation.js +24 -0
- package/src/ai/transform.js +858 -0
- package/src/ai/usage.js +17 -0
- package/src/ai/variety.js +80 -0
- package/src/ai/video-frames.js +24 -0
- package/src/ai/video-generation.js +126 -0
- package/src/ai/web-operation.js +31 -0
- package/src/algo-catalog.js +153 -0
- package/src/algo-trace.js +139 -0
- package/src/api-client.js +514 -0
- package/src/api-collections.js +14 -0
- package/src/api-demo.js +222 -0
- package/src/aspect.js +40 -0
- package/src/build.js +323 -0
- package/src/carousel.js +56 -0
- package/src/chrome.js +65 -0
- package/src/code-highlight.js +68 -0
- package/src/code-lab-browser.js +22 -0
- package/src/code-lab.js +3 -0
- package/src/code-language.js +52 -0
- package/src/csv.js +74 -0
- package/src/decision-lab.js +20 -0
- package/src/deck-file.js +119 -0
- package/src/diagram/napkin-examples.js +13 -0
- package/src/diagram/napkin.js +539 -0
- package/src/docx.js +128 -0
- package/src/dynamics/git-sim.js +200 -0
- package/src/dynamics/layouts.js +112 -0
- package/src/elements.js +554 -0
- package/src/experiences.js +124 -0
- package/src/exploration.js +31 -0
- package/src/export/browser.js +53 -0
- package/src/export/estudo.js +67 -0
- package/src/export/pptx.js +475 -0
- package/src/export/roteiro.js +40 -0
- package/src/export/shots.js +182 -0
- package/src/figures/charts.js +383 -0
- package/src/figures/diagrams.js +117 -0
- package/src/figures/icons.js +166 -0
- package/src/figures/material-pictos.js +20 -0
- package/src/figures/pictos.js +255 -0
- package/src/figures/points.js +106 -0
- package/src/figures/qr.js +29 -0
- package/src/figures/ufmap.js +48 -0
- package/src/fiscal/autofix.js +377 -0
- package/src/fiscal/normalize.js +115 -0
- package/src/graph-lab.js +24 -0
- package/src/hologram.js +30 -0
- package/src/identity.js +86 -0
- package/src/import/crop.js +59 -0
- package/src/import/index.js +132 -0
- package/src/import/legacy-word.js +42 -0
- package/src/import/merge.js +45 -0
- package/src/import/office.js +123 -0
- package/src/import/omml.js +69 -0
- package/src/import/pdf-render.js +56 -0
- package/src/import/pptx.js +701 -0
- package/src/import/xml.js +42 -0
- package/src/infographic.js +401 -0
- package/src/layouts.js +723 -0
- package/src/lessons.js +171 -0
- package/src/library.js +399 -0
- package/src/markup.js +116 -0
- package/src/master.js +214 -0
- package/src/mcp/server.js +371 -0
- package/src/meeting.js +42 -0
- package/src/motion.js +56 -0
- package/src/package.js +116 -0
- package/src/playground.js +19 -0
- package/src/portal-scene.js +20 -0
- package/src/preferences.js +61 -0
- package/src/protect.js +51 -0
- package/src/purpose.js +24 -0
- package/src/pytrace.js +1092 -0
- package/src/research/duckduckgo.js +73 -0
- package/src/research/research.js +202 -0
- package/src/research/svg-image.js +17 -0
- package/src/research/visuals.js +52 -0
- package/src/runtime/api-collection-ui.js +5 -0
- package/src/runtime/api-collections.js +28 -0
- package/src/runtime/api-core.js +530 -0
- package/src/runtime/api-ui.js +966 -0
- package/src/runtime/base.css +1774 -0
- package/src/runtime/calc.js +139 -0
- package/src/runtime/code-lab.js +371 -0
- package/src/runtime/decision-lab.js +49 -0
- package/src/runtime/diagram.js +341 -0
- package/src/runtime/fit.js +268 -0
- package/src/runtime/fonts/caveat.css +2 -0
- package/src/runtime/fonts/comic-neue.css +3 -0
- package/src/runtime/fonts/fredoka.css +2 -0
- package/src/runtime/fonts/index.json +10 -0
- package/src/runtime/fonts/inter.css +2 -0
- package/src/runtime/fonts/patrick-hand.css +2 -0
- package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
- package/src/runtime/fonts/press-start-2p.css +2 -0
- package/src/runtime/fonts/vt323.css +2 -0
- package/src/runtime/formula.js +148 -0
- package/src/runtime/graph-lab.js +72 -0
- package/src/runtime/meeting.css +7 -0
- package/src/runtime/meeting.js +10 -0
- package/src/runtime/motion.js +15 -0
- package/src/runtime/playground.js +11 -0
- package/src/runtime/points.js +19 -0
- package/src/runtime/portal-scene.css +6 -0
- package/src/runtime/portal-scene.js +22 -0
- package/src/runtime/runtime.js +827 -0
- package/src/runtime/science.js +89 -0
- package/src/runtime/skins/aluminio.css +31 -0
- package/src/runtime/skins/arcade.css +25 -0
- package/src/runtime/skins/aurora.css +17 -0
- package/src/runtime/skins/bauhaus.css +19 -0
- package/src/runtime/skins/editorial.css +19 -0
- package/src/runtime/skins/jornal.css +17 -0
- package/src/runtime/skins/manual-noite.css +14 -0
- package/src/runtime/skins/manual.css +14 -0
- package/src/runtime/skins/noite.css +21 -0
- package/src/runtime/skins/oceano.css +17 -0
- package/src/runtime/skins/pop.css +23 -0
- package/src/runtime/skins/prata.css +18 -0
- package/src/runtime/skins/prisma.css +28 -0
- package/src/runtime/skins/rabisco.css +18 -0
- package/src/runtime/skins/relevo.css +33 -0
- package/src/runtime/skins/sinal.css +16 -0
- package/src/runtime/skins/terminal.css +23 -0
- package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
- package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
- package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
- package/src/runtime/vendor/katex.css +1 -0
- package/src/runtime/vendor/mermaid.min.js +3636 -0
- package/src/runtime/vendor/plotly.min.js +3620 -0
- package/src/runtime/video-player.js +35 -0
- package/src/science.js +162 -0
- package/src/studio/ai-response.js +36 -0
- package/src/studio/api-routes.js +152 -0
- package/src/studio/assets/contexto/leia-me.md +12 -0
- package/src/studio/assets/contexto/vendas.csv +7 -0
- package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
- package/src/studio/assets/imagens/colecao-cor.png +0 -0
- package/src/studio/assets/imagens/colecao-equipe.png +0 -0
- package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
- package/src/studio/code-routes.js +32 -0
- package/src/studio/command-approval.js +48 -0
- package/src/studio/demo-decks.js +98 -0
- package/src/studio/direction-routes.js +29 -0
- package/src/studio/errors.js +65 -0
- package/src/studio/exploration-demo.js +17 -0
- package/src/studio/exporting.js +105 -0
- package/src/studio/generation-report.js +10 -0
- package/src/studio/instance.js +48 -0
- package/src/studio/layout-samples.js +152 -0
- package/src/studio/project.js +371 -0
- package/src/studio/public/api-collections.js +28 -0
- package/src/studio/public/app.js +5265 -0
- package/src/studio/public/art-preview.js +10 -0
- package/src/studio/public/calc-fields.js +11 -0
- package/src/studio/public/chat-progress.js +47 -0
- package/src/studio/public/developer-fields.js +22 -0
- package/src/studio/public/explorer.js +443 -0
- package/src/studio/public/history.js +58 -0
- package/src/studio/public/index.html +867 -0
- package/src/studio/public/inspector.js +270 -0
- package/src/studio/public/lab-fields.js +5 -0
- package/src/studio/public/library.css +275 -0
- package/src/studio/public/library.html +55 -0
- package/src/studio/public/library.js +471 -0
- package/src/studio/public/merge-decks.js +155 -0
- package/src/studio/public/review-ui.js +100 -0
- package/src/studio/public/screenshot-editor.js +140 -0
- package/src/studio/public/share-ui.js +72 -0
- package/src/studio/public/slide-clipboard.js +23 -0
- package/src/studio/public/slide-form.js +1263 -0
- package/src/studio/public/studio-awareness.js +154 -0
- package/src/studio/public/studio-next.css +303 -0
- package/src/studio/public/style.css +1262 -0
- package/src/studio/public/ui-icons.js +2 -0
- package/src/studio/public/viewers.js +372 -0
- package/src/studio/public/visual-editor.js +326 -0
- package/src/studio/public/workspace.js +18 -0
- package/src/studio/public-files.js +1 -0
- package/src/studio/request-helpers.js +41 -0
- package/src/studio/server.js +1583 -0
- package/src/studio/share-routes.js +146 -0
- package/src/studio/slide-copy-routes.js +26 -0
- package/src/studio/slide-copy.js +30 -0
- package/src/studio/snapshot.js +160 -0
- package/src/studio/style-routes.js +103 -0
- package/src/studio/template-collections.js +97 -0
- package/src/studio/versions.js +95 -0
- package/src/studio/video-download.js +29 -0
- package/src/studio/visual-keys.js +52 -0
- package/src/studio/workspaces.js +39 -0
- package/src/table.js +93 -0
- package/src/templates/scaffold.js +372 -0
- package/src/themes.js +604 -0
- package/src/trace-view.js +450 -0
- package/src/uid.js +19 -0
- package/src/video-player.js +40 -0
- package/src/visual-edits.js +99 -0
- package/src/ws.js +134 -0
- package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
- package/templates/cenario/imagens/cidade.svg +30 -0
- package/templates/cenario/imagens/pessoa.svg +15 -0
- package/templates/cenario-e-ciencia.yaml +47 -0
- package/templates/ensaio-api.yaml +160 -0
- package/templates/exemplo-alegre.yaml +75 -0
- package/templates/exemplo-keynote.yaml +90 -0
- package/templates/exemplo-napkin.yaml +139 -0
- package/templates/exemplo.yaml +228 -0
package/src/ai/usage.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Medição por tarefa, isolada de outras pessoas/requisições concorrentes.
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
const scope = new AsyncLocalStorage();
|
|
4
|
+
export const currentUsage = () => scope.getStore();
|
|
5
|
+
export async function measureLLM(run) {
|
|
6
|
+
const usage = { calls: 0, failedCalls: 0, promptTokens: 0, completionTokens: 0, missingUsage: 0, reportedCost: 0, missingCost: 0 };
|
|
7
|
+
const result = await scope.run(usage, run);
|
|
8
|
+
return { result, usage };
|
|
9
|
+
}
|
|
10
|
+
export function recordUsage(metrics, result) {
|
|
11
|
+
const u = result?.usage;
|
|
12
|
+
if (Number.isFinite(u?.prompt_tokens) && Number.isFinite(u?.completion_tokens)) {
|
|
13
|
+
metrics.promptTokens += u.prompt_tokens; metrics.completionTokens += u.completion_tokens;
|
|
14
|
+
} else metrics.missingUsage++;
|
|
15
|
+
if (Number.isFinite(u?.cost)) metrics.reportedCost += u.cost;
|
|
16
|
+
else metrics.missingCost++;
|
|
17
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Variedade de um deck: mede o que deixa uma apresentação repetitiva (e todos os decks com a mesma cara).
|
|
2
|
+
// É métrica, não decisão: quem decide como variar é a IA (generateDeck revisa; o Studio oferece no Ritmo).
|
|
3
|
+
|
|
4
|
+
const LISTY = new Set(["cards", "list", "steps", "stats", "bento", "agenda", "references"]);
|
|
5
|
+
const IMPACT = new Set(["headline", "number", "statement", "full", "quote", "question", "poll", "image", "section"]);
|
|
6
|
+
const FRAME = new Set(["cover", "end"]);
|
|
7
|
+
|
|
8
|
+
export function varietyReport(spec) {
|
|
9
|
+
const slides = (spec?.slides || []).map((s) => ({ layout: s.layout || "blocks", tone: s.tone || "light" }));
|
|
10
|
+
const body = slides.filter((s) => !FRAME.has(s.layout));
|
|
11
|
+
const n = body.length;
|
|
12
|
+
const problems = [];
|
|
13
|
+
const distinct = new Set(body.map((s) => s.layout)).size;
|
|
14
|
+
|
|
15
|
+
// 3+ slides seguidos com o mesmo layout
|
|
16
|
+
const runs = [];
|
|
17
|
+
for (let i = 0; i < slides.length;) {
|
|
18
|
+
let j = i;
|
|
19
|
+
while (j + 1 < slides.length && slides[j + 1].layout === slides[i].layout) j++;
|
|
20
|
+
if (j - i + 1 >= 3 && !FRAME.has(slides[i].layout)) runs.push({ layout: slides[i].layout, from: i + 1, to: j + 1 });
|
|
21
|
+
i = j + 1;
|
|
22
|
+
}
|
|
23
|
+
runs.forEach((r) => problems.push(`${r.to - r.from + 1} slides seguidos no mesmo layout (${r.layout}, slides ${r.from}–${r.to})`));
|
|
24
|
+
|
|
25
|
+
if (n >= 6) {
|
|
26
|
+
const want = Math.min(8, Math.ceil(n * 0.5));
|
|
27
|
+
if (distinct < want) problems.push(`só ${distinct} layouts diferentes em ${n} slides de conteúdo (bom: ${want} ou mais)`);
|
|
28
|
+
const listy = body.filter((s) => LISTY.has(s.layout)).length;
|
|
29
|
+
if (listy / n > 0.45) problems.push(`${listy} de ${n} slides são listas/cartões — falta respiro (manchete, número grande, imagem, pergunta)`);
|
|
30
|
+
if (!body.some((s) => IMPACT.has(s.layout))) problems.push("nenhum slide de impacto (manchete, número grande, frase, página inteira, pergunta)");
|
|
31
|
+
if (new Set(slides.map((s) => s.tone)).size === 1) problems.push(`todos os slides no mesmo tom (${slides[0].tone}) — alterne com escuro/destaque nos momentos-chave`);
|
|
32
|
+
}
|
|
33
|
+
const counts = {};
|
|
34
|
+
body.forEach((s) => (counts[s.layout] = (counts[s.layout] || 0) + 1));
|
|
35
|
+
return { slides: slides.length, distinct, runs, counts, problems, ok: problems.length === 0 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Direções criativas: cada deck gerado sorteia um jeito de contar (e de se parecer), para dois decks sobre
|
|
39
|
+
// assuntos parecidos não saírem iguais.
|
|
40
|
+
export const CREATIVE_DIRECTIONS = [
|
|
41
|
+
"Keynote minimalista: uma frase de abertura sem ilustração, grandes espaços vazios, composição central, um número por ideia. Considere prata; destaque em cor apenas na virada.",
|
|
42
|
+
"Investigação editorial: abra com uma cena concreta, revele pistas por enquadramentos e comparações, entregue a conclusão no final. Considere editorial; serifas, linhas finas, papel claro e um único contraste escuro.",
|
|
43
|
+
"Workshop geométrico: abra com uma escolha da plateia, alterne desafios curtos, diagramas e timer. Considere bauhaus ou pop; formas primárias, assimetria e revelação por clique.",
|
|
44
|
+
"Estudo de caso documental: contexto → problema → tentativa → resultado → lição. Considere jornal; manchetes, evidências visuais, cronologia e legendas curtas. Só use dados do briefing ou dados verificados.",
|
|
45
|
+
"Contraponto: abra com duas possibilidades em comparação, questione uma crença e mostre uma evidência decisiva. Considere sinal; contraste firme, pouco ornamento e uma frase final que retoma a abertura.",
|
|
46
|
+
"Atlas visual: conte a história com páginas inteiras, diagramas e detalhes ampliados; texto como legenda. Considere oceano; alterne escala, posição e composição, sem usar um mosaico em toda página.",
|
|
47
|
+
"Sala de decisão: peça a decisão no começo, mostre evidências, opções, riscos e o próximo marco. Considere noite; ouro discreto, tipografia leve, gráficos limpos e transições suaves.",
|
|
48
|
+
"Palco elétrico: abra com uma pergunta provocadora em tamanho gigante, crie expectativa, pause e revele. Considere aurora; tela escura, uma cor luminosa, números monumentais e participação real da plateia.",
|
|
49
|
+
"Caderno de descoberta: uma pergunta inicial, um mapa de hipóteses, uma tentativa e uma descoberta. Considere rabisco; desenhos, setas e anotações visuais, com espaço para a audiência pensar.",
|
|
50
|
+
"Laboratório vivo: prever → observar → explicar → experimentar. Considere terminal; use codewalk para ler código por etapas, spotlight para screenshots e uma pergunta antes da resposta. Saídas simuladas devem ser identificadas.",
|
|
51
|
+
"Lançamento de produto: primeiro o benefício, depois uma demonstração visual, um detalhe ampliado, prova e convite. Considere prata ou oceano; mostre o produto em escala, evitando uma sequência de cartões de funcionalidades.",
|
|
52
|
+
"Manifesto tipográfico: alterne frases muito curtas, uma pausa visual, contrastes de escala e uma conclusão coletiva. Considere editorial ou bauhaus; capa assimétrica sem ícone decorativo e cada ato com composição própria.",
|
|
53
|
+
"Cinema hacker de bom gosto: capa escura e cinematográfica com poeira de luz à deriva, transições morph entre cenas e saída nas viradas, cada slide estático com um detalhe vivo discreto que ecoa a capa sem repeti-la, e uma demonstração visual escolhida pelo problema de negócio. Points é 2D; para areia 3D girando, planeje vídeo e aguarde autorização quando exigida. Respeite o fundo solicitado; Considere noite ou prisma; paleta luzquente; brancos quentes e âmbar de luz de prédio, nunca neon chapado; fundo escuro quente ou branco quente, cor de joia só no detalhe.",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function pickDirection(seed = Math.random()) {
|
|
57
|
+
return CREATIVE_DIRECTIONS[Math.floor(seed * CREATIVE_DIRECTIONS.length) % CREATIVE_DIRECTIONS.length];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Um baralho embaralhado distribui todas as direções antes de repetir. O estado tem tamanho fixo,
|
|
61
|
+
// dura só nesta sessão do servidor e não armazena o conteúdo das apresentações.
|
|
62
|
+
export function createDirectionPicker(random = Math.random) {
|
|
63
|
+
let remaining = [];
|
|
64
|
+
let previous;
|
|
65
|
+
return () => {
|
|
66
|
+
if (!remaining.length) {
|
|
67
|
+
remaining = [...CREATIVE_DIRECTIONS];
|
|
68
|
+
for (let i = remaining.length - 1; i > 0; i--) {
|
|
69
|
+
const j = Math.min(i, Math.max(0, Math.floor(random() * (i + 1)) || 0));
|
|
70
|
+
[remaining[i], remaining[j]] = [remaining[j], remaining[i]];
|
|
71
|
+
}
|
|
72
|
+
// Também evita a repetição na fronteira entre dois baralhos.
|
|
73
|
+
if (remaining.at(-1) === previous) [remaining[0], remaining[remaining.length - 1]] = [remaining.at(-1), remaining[0]];
|
|
74
|
+
}
|
|
75
|
+
previous = remaining.pop();
|
|
76
|
+
return previous;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const nextDirection = createDirectionPicker();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Frame nativo da apresentação: referência visual local para gerar um clipe.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {loadSpec} from '../build.js';
|
|
5
|
+
import {slideSnapshots} from '../studio/snapshot.js';
|
|
6
|
+
|
|
7
|
+
export async function videoFrame(request,{cwd,snapshot=slideSnapshots}={}) {
|
|
8
|
+
if(!cwd)throw Error('Abra uma apresentação salva para capturar um frame.');
|
|
9
|
+
const root=path.resolve(cwd);
|
|
10
|
+
const within=name=>{const file=path.resolve(root,name),rel=path.relative(root,file);if(!rel||rel.startsWith('..')||path.isAbsolute(rel))throw Error('O frame e o deck devem ficar na pasta da apresentação.');return file;};
|
|
11
|
+
const files=fs.readdirSync(root).filter(name=>/\.ya?ml$/i.test(name));
|
|
12
|
+
const name=request.deck||files.find(name=>path.basename(name,path.extname(name))===path.basename(root))||(files.length===1?files[0]:null);
|
|
13
|
+
if(!name)throw Error('Informe deck com o nome do YAML desta apresentação.');
|
|
14
|
+
const spec=loadSpec(within(name));
|
|
15
|
+
const index=Number(request.slide)-1;
|
|
16
|
+
if(!Number.isInteger(index)||!spec.slides[index])throw Error('slide deve ser o número de um slide existente, começando em 1.');
|
|
17
|
+
const output=within(request.out||`imagens/frame-slide-${index+1}.jpg`);
|
|
18
|
+
if(!/\.jpe?g$/i.test(output))throw Error('O frame nativo deve ser salvo como JPG.');
|
|
19
|
+
if(fs.existsSync(output))throw Error('O frame de saída já existe; escolha outro nome.');
|
|
20
|
+
const frames=await snapshot(spec,index,{mode:'final',width:1920,maxFrames:1});
|
|
21
|
+
if(!frames[0]?.dataUrl?.startsWith('data:image/jpeg;base64,'))throw Error('Não foi possível capturar o slide.');
|
|
22
|
+
fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,Buffer.from(frames[0].dataUrl.split(',')[1],'base64'),{flag:'wx'});
|
|
23
|
+
return {action:'frame',costIncurred:false,slide:index+1,file:path.relative(root,output).split(path.sep).join('/'),width:1920};
|
|
24
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mediaPrompt } from './art-direction.js';
|
|
2
|
+
// Operações curtas e retomáveis: o chat não fica preso esperando o render do provedor.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
const BASE='https://openrouter.ai/api/v1';
|
|
8
|
+
export function parseVideoArgs(args){
|
|
9
|
+
const result={};for(let i=0;i<args.length;i++){
|
|
10
|
+
const m=args[i].match(/^--([^=]+)(?:=(.*))?$/);if(!m)throw new Error(`Argumento inesperado: ${args[i]}`);
|
|
11
|
+
result[m[1]]=m[2]??(args[i+1]&&!args[i+1].startsWith('--')?args[++i]:true);
|
|
12
|
+
}return result;
|
|
13
|
+
}
|
|
14
|
+
export function videoApiURL(value){
|
|
15
|
+
const url=new URL(String(value).startsWith('/api/v1/')?value:String(value).replace(/^\//,''),BASE+'/');
|
|
16
|
+
if(url.origin!=='https://openrouter.ai'||!url.pathname.startsWith('/api/v1/videos'))throw new Error('Polling precisa usar a API de vídeo do OpenRouter.');
|
|
17
|
+
return url.href;
|
|
18
|
+
}
|
|
19
|
+
// quanto custa ANTES de gerar (para mostrar na aprovação): catálogo é de graça; null se não der para estimar
|
|
20
|
+
export async function estimateVideoCost({model,duration,resolution},{key=process.env.SAGADECK_VIDEO_KEY||process.env.OPENROUTER_API_KEY,fetcher=fetch}={}){
|
|
21
|
+
try{
|
|
22
|
+
if(!key)return null;
|
|
23
|
+
const r=await fetcher(`${BASE}/videos/models`,{headers:{Authorization:`Bearer ${key}`},signal:AbortSignal.timeout(15000)});
|
|
24
|
+
if(!r.ok)return null;
|
|
25
|
+
const meta=(await r.json()).data?.find((m)=>m.id===model||m.canonical_slug===model);
|
|
26
|
+
const skus=meta?.pricing_skus||meta?.pricing||null;
|
|
27
|
+
const perSec=Number(skus?.[/1080|1k|2k|4k/i.test(String(resolution||''))?'per-video-second-1080p':'per-video-second']);
|
|
28
|
+
if(!Number.isFinite(perSec)||perSec<0)return null;
|
|
29
|
+
const total=perSec*Number(duration||0);
|
|
30
|
+
return Number.isFinite(total)&&total>0?`~$${total.toFixed(2)}`:null;
|
|
31
|
+
}catch{return null;}
|
|
32
|
+
}
|
|
33
|
+
function within(dir,name){
|
|
34
|
+
const target=path.resolve(dir,name),rel=path.relative(path.resolve(dir),target);
|
|
35
|
+
if(!rel||rel.startsWith('..')||path.isAbsolute(rel))throw new Error('Salve o vídeo dentro da pasta da apresentação.');
|
|
36
|
+
return target;
|
|
37
|
+
}
|
|
38
|
+
export async function videoOperation(request,{cwd,key=process.env.SAGADECK_VIDEO_KEY||process.env.OPENROUTER_API_KEY,fetcher=fetch}={}){
|
|
39
|
+
if(request.action==='frame'){const {videoFrame}=await import('./video-frames.js');return videoFrame(request,{cwd});}
|
|
40
|
+
if(!key)throw new Error('Configure SAGADECK_VIDEO_KEY ou OPENROUTER_API_KEY para a API de vídeo. O modelo de texto não gera vídeo.');
|
|
41
|
+
const api=async(value,body)=>{
|
|
42
|
+
const r=await fetcher(videoApiURL(value),{method:body?'POST':'GET',headers:{Authorization:`Bearer ${key}`,'Content-Type':'application/json'},...(body?{body:JSON.stringify(body)}:{}),signal:AbortSignal.timeout(30000)});
|
|
43
|
+
if(!r.ok)throw new Error(`API de vídeo retornou HTTP ${r.status}.`);return r.json();
|
|
44
|
+
};
|
|
45
|
+
const action=request.action||'plan';
|
|
46
|
+
if(['plan','submit'].includes(action)){
|
|
47
|
+
const model=request.model||'google/veo-3.1-lite',catalog=await api('/videos/models');
|
|
48
|
+
const meta=catalog.data?.find(m=>m.id===model||m.canonical_slug===model);if(!meta)throw new Error(`Modelo de vídeo indisponível: ${model}`);
|
|
49
|
+
const body={model,prompt:String(request.prompt||''),duration:Number(request.duration||4),resolution:request.resolution||'720p',aspect_ratio:request.aspect||'16:9',generate_audio:false};
|
|
50
|
+
if(!body.prompt.trim())throw new Error('Descreva o clipe antes de gerar.');
|
|
51
|
+
body.prompt=mediaPrompt(body.prompt); // plan mostra exatamente o prompt que submit enviará.
|
|
52
|
+
if(!Number.isInteger(body.duration)||body.duration<1)throw new Error('Duração precisa ser um número inteiro positivo.');
|
|
53
|
+
for(const [field,list] of [['duration',meta.supported_durations],['resolution',meta.supported_resolutions],['aspect_ratio',meta.supported_aspect_ratios]])if(list?.length&&!list.map(String).includes(String(body[field])))throw new Error(`${field} não suportado por ${model}.`);
|
|
54
|
+
if(request.seed!=null){body.seed=Number(request.seed);if(!Number.isInteger(body.seed)||body.seed<0)throw new Error('seed precisa ser inteiro não negativo.');}
|
|
55
|
+
const frames=[];
|
|
56
|
+
for(const [field,type] of [['firstFrame','first_frame'],['lastFrame','last_frame']])if(request[field]){
|
|
57
|
+
if(!cwd)throw new Error('Abra a apresentação para usar frames locais.');
|
|
58
|
+
const file=within(cwd,request[field]),ext=path.extname(file).toLowerCase();
|
|
59
|
+
if(!['.png','.jpg','.jpeg','.webp'].includes(ext))throw new Error('Frame precisa ser PNG, JPEG ou WebP.');
|
|
60
|
+
if(fs.statSync(file).size>10*1024*1024)throw new Error('Frame excede 10 MB.');
|
|
61
|
+
frames.push({type:'image_url',frame_type:type,image_url:{url:`data:image/${ext==='.jpg'?'jpeg':ext.slice(1)};base64,${fs.readFileSync(file).toString('base64')}`}});
|
|
62
|
+
}
|
|
63
|
+
if(frames.length)body.frame_images=frames;
|
|
64
|
+
const summary={...body,...(frames.length?{frame_images:frames.map(f=>({frame_type:f.frame_type,local:true}))}:{})};
|
|
65
|
+
if(action==='plan')return{action,costIncurred:false,request:summary,pricing:meta.pricing_skus||meta.pricing||null};
|
|
66
|
+
if(!cwd)throw new Error('Abra uma apresentação salva antes de gerar vídeo.');
|
|
67
|
+
const output=within(cwd,request.out||`videos/clip-${crypto.randomUUID()}.mp4`);if(fs.existsSync(output))throw new Error('O arquivo de saída já existe; escolha outro nome.');
|
|
68
|
+
const job=await api('/videos',body);
|
|
69
|
+
if(!/^[\w-]+$/.test(job.id||''))throw new Error('A API não devolveu um identificador de job válido.');
|
|
70
|
+
const folder=path.join(cwd,'.sagadeck','videos');fs.mkdirSync(folder,{recursive:true});
|
|
71
|
+
const record={id:job.id,polling_url:videoApiURL(job.polling_url||`/videos/${job.id}`),output:path.relative(cwd,output).split(path.sep).join("/"),request:summary,createdAt:new Date().toISOString()};
|
|
72
|
+
fs.writeFileSync(path.join(folder,job.id+'.json'),JSON.stringify(record,null,2),{flag:'wx'});
|
|
73
|
+
return{id:job.id,status:job.status,output:record.output,nextAction:'status'};
|
|
74
|
+
}
|
|
75
|
+
if(!['status','download'].includes(action))throw new Error('Ação de vídeo: plan, submit, status ou download.');
|
|
76
|
+
if(!/^[\w-]+$/.test(request.id||''))throw new Error('Informe o id da geração.');
|
|
77
|
+
const file=within(cwd,`.sagadeck/videos/${request.id}.json`),record=JSON.parse(fs.readFileSync(file,'utf8'));
|
|
78
|
+
const job=await api(record.polling_url);
|
|
79
|
+
if(action==='status'||job.status!=='completed')return{id:record.id,status:job.status,error:job.error||null,cost:job.usage?.cost??null};
|
|
80
|
+
const output=within(cwd,record.output);if(fs.existsSync(output))return{id:record.id,status:'downloaded',video:record.output,cached:true};
|
|
81
|
+
const url=new URL(job.unsigned_urls?.[0]||`${BASE}/videos/${record.id}/content?index=0`);
|
|
82
|
+
if(url.protocol!=='https:')throw new Error('Download do vídeo exige HTTPS.');
|
|
83
|
+
const r=await fetcher(url.href,{headers:url.origin==='https://openrouter.ai'?{Authorization:`Bearer ${key}`}:{},signal:AbortSignal.timeout(30000)});
|
|
84
|
+
if(!r.ok)throw new Error(`Download retornou HTTP ${r.status}.`);
|
|
85
|
+
const mime=r.headers.get('content-type')||'';if(!/^video\/|application\/octet-stream/i.test(mime))throw new Error('O provedor não retornou um arquivo de vídeo.');
|
|
86
|
+
const chunks=[];let bytes=0;for await(const chunk of r.body){bytes+=chunk.length;if(bytes>100*1024*1024)throw new Error('Vídeo excede 100 MB.');chunks.push(chunk);}
|
|
87
|
+
const data=Buffer.concat(chunks);if(data.length<12||data.toString('ascii',4,8)!=='ftyp')throw new Error('O resultado não é um MP4 válido.');
|
|
88
|
+
fs.mkdirSync(path.dirname(output),{recursive:true});fs.writeFileSync(output,data,{flag:'wx'});
|
|
89
|
+
return{id:record.id,status:'downloaded',video:record.output,bytes,cost:job.usage?.cost??null};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// vigia os jobs do deck: confere andamento (grátis), baixa sozinho o que terminou.
|
|
93
|
+
// para o ticker do Studio avisar "clipe pronto" sem a pessoa ficar olhando.
|
|
94
|
+
const lastCheck = new Map(); // polling_url -> { at, status }
|
|
95
|
+
export async function pollVideoJobs(cwd,{key=process.env.SAGADECK_VIDEO_KEY||process.env.OPENROUTER_API_KEY,fetcher=fetch,cooldownMs=60000}={}){
|
|
96
|
+
const dir=path.join(path.resolve(cwd),'.sagadeck','videos');
|
|
97
|
+
let files=[];
|
|
98
|
+
try{files=fs.readdirSync(dir).filter((f)=>f.endsWith('.json'));}catch{return[];}
|
|
99
|
+
const out=[];
|
|
100
|
+
for(const f of files){
|
|
101
|
+
let record;try{record=JSON.parse(fs.readFileSync(path.join(dir,f),'utf8'));}catch{continue;}
|
|
102
|
+
if(!record?.id||!record?.polling_url)continue;
|
|
103
|
+
const done=record.downloadedAt||(record.output&&fs.existsSync(path.join(path.resolve(cwd),record.output))&&record.status==='completed');
|
|
104
|
+
const cached=lastCheck.get(record.polling_url);
|
|
105
|
+
let status=record.status||'unknown';
|
|
106
|
+
if(cached&&Date.now()-cached.at<cooldownMs) status=cached.status;
|
|
107
|
+
else if(!done&&key){
|
|
108
|
+
try{
|
|
109
|
+
const r=await fetcher(record.polling_url,{headers:{Authorization:`Bearer ${key}`},signal:AbortSignal.timeout(15000)});
|
|
110
|
+
if(r.ok){const job=await r.json();status=job.status||status;lastCheck.set(record.polling_url,{at:Date.now(),status});}
|
|
111
|
+
}catch{/* sem rede: mantém o último */}
|
|
112
|
+
}
|
|
113
|
+
let video=record.output&&fs.existsSync(path.join(path.resolve(cwd),record.output))?record.output:null;
|
|
114
|
+
if(status==='completed'&&!video&&key){
|
|
115
|
+
try{
|
|
116
|
+
const dl=await videoOperation({action:'download',id:record.id},{cwd,key,fetcher});
|
|
117
|
+
video=dl.video;status='downloaded';
|
|
118
|
+
record.downloadedAt=new Date().toISOString();record.status='completed';
|
|
119
|
+
try{fs.writeFileSync(path.join(dir,f),JSON.stringify(record,null,2));}catch{}
|
|
120
|
+
}catch{/* tenta de novo no próximo ciclo */}
|
|
121
|
+
}
|
|
122
|
+
if(video)status='downloaded'; // arquivo em disco manda: pronto para usar, em qualquer chamada
|
|
123
|
+
out.push({id:record.id,status,video:video||null});
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defaultWeb } from '../research/research.js';
|
|
2
|
+
|
|
3
|
+
export function webOnlyRunner({signal,onProgress=()=>{},web=defaultWeb}={}) {
|
|
4
|
+
const run = async command => {
|
|
5
|
+
if (command.language !== 'web') return {denied:true,stderr:'Este servidor oferece pesquisa web; execução de código local está indisponível.'};
|
|
6
|
+
onProgress({phase:'command',text:'Pesquisando na web…'});
|
|
7
|
+
try {return {exitCode:0,stdout:JSON.stringify(await webOperation(JSON.parse(command.code),{web,signal})),stderr:''};}
|
|
8
|
+
catch(e) {if(signal?.aborted)throw e;return {exitCode:1,stdout:'',stderr:e.message};}
|
|
9
|
+
};
|
|
10
|
+
run.description = 'PESQUISA WEB disponível: responda com bloco YAML run: {language: web, why: motivo, code: JSON}. JSON aceita {"action":"search","query":"busca"} ou {"action":"read","url":"URL observada"}. Leia a fonte antes de afirmar fatos. Anexos são exclusivos sem autorização explícita para complementar; blogs e memes servem para humor/opiniões. Não afirme falta de acesso sem tentar. Não peça javascript/python/shell/video: esses comandos estão indisponíveis neste servidor.';
|
|
11
|
+
return run;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Ferramenta de leitura explícita: disponível ao chat, sem depender de o LLM ter navegação própria.
|
|
15
|
+
export async function webOperation(request,{web=defaultWeb,signal}={}) {
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
if (process.env.SAGADECK_WEB === '0') throw Error('Pesquisa web desligada neste servidor.');
|
|
18
|
+
if (request.action === 'search') {
|
|
19
|
+
const query = String(request.query || '').trim();
|
|
20
|
+
if (!query || query.length > 1000) throw Error('Informe uma busca de até 1000 caracteres.');
|
|
21
|
+
const results = await web.search(query);
|
|
22
|
+
signal?.throwIfAborted();
|
|
23
|
+
return {query,results:results.slice(0,10)};
|
|
24
|
+
}
|
|
25
|
+
if (request.action === 'read') {
|
|
26
|
+
const doc = await web.fetch(String(request.url || ''));
|
|
27
|
+
signal?.throwIfAborted();
|
|
28
|
+
return {...doc,text:String(doc.text || '').slice(0,20000)};
|
|
29
|
+
}
|
|
30
|
+
throw Error('web.action: use search ou read.');
|
|
31
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Catálogo de algoritmos prontos para o layout algo, por área (como as toolboxes do MATLAB). Cada um é um programa no
|
|
2
|
+
// Python simples de src/pytrace.js; o slide só passa os dados. Algoritmo novo entra aqui como mais um programa:
|
|
3
|
+
// program: o código · call(s): a chamada com os dados do slide · view: o que desenhar e onde ficam os ponteiros
|
|
4
|
+
import { pyLiteral } from "./pytrace.js";
|
|
5
|
+
|
|
6
|
+
const str = (v, d) => pyLiteral(String(v ?? d));
|
|
7
|
+
const graphOf = (s, d) => pyLiteral(s.graph && typeof s.graph === "object" ? s.graph : d);
|
|
8
|
+
const firstKey = (s, d) => (s.graph && typeof s.graph === "object" ? Object.keys(s.graph)[0] : Object.keys(d)[0]);
|
|
9
|
+
|
|
10
|
+
const GRAFO = { A: ["B", "C"], B: ["A", "D", "E"], C: ["A", "F"], D: ["B"], E: ["B", "F"], F: ["C", "E"] };
|
|
11
|
+
const GRAFO_PESOS = { A: [["B", 4], ["C", 1]], B: [["A", 4], ["C", 2], ["D", 5]], C: [["A", 1], ["B", 2], ["D", 8]], D: [["B", 5], ["C", 8]] };
|
|
12
|
+
|
|
13
|
+
export const CATALOG = {
|
|
14
|
+
naive: {
|
|
15
|
+
area: "texto", name: "Busca ingênua",
|
|
16
|
+
program: `def busca(texto, padrao):
|
|
17
|
+
n, m = len(texto), len(padrao)
|
|
18
|
+
achados = []
|
|
19
|
+
for i in range(n - m + 1): # encaixa o padrão na posição {i}
|
|
20
|
+
j = 0
|
|
21
|
+
while j < m and texto[i + j] == padrao[j]: # texto[{i + j}] com padrao[{j}]: {'igual, avança' if j < m and texto[i + j] == padrao[j] else 'para aqui'}
|
|
22
|
+
j += 1
|
|
23
|
+
if j == m:
|
|
24
|
+
achados.append(i) # achou na posição {i}
|
|
25
|
+
return achados`,
|
|
26
|
+
call: (s) => `busca(${str(s.text, "abracadabra")}, ${str(s.pattern, "abra")})`,
|
|
27
|
+
view: [{ var: "texto", pointers: ["i + j"] }, { var: "padrao", under: "texto", offset: "i", pointers: ["j"] }, { var: "achados" }],
|
|
28
|
+
},
|
|
29
|
+
kmp: {
|
|
30
|
+
area: "texto", name: "KMP (Knuth-Morris-Pratt)",
|
|
31
|
+
program: `def prefixos(padrao):
|
|
32
|
+
m = len(padrao)
|
|
33
|
+
pi = [0] * m
|
|
34
|
+
k = 0
|
|
35
|
+
for q in range(1, m):
|
|
36
|
+
while k > 0 and padrao[k] != padrao[q]:
|
|
37
|
+
k = pi[k - 1] # não casou: k volta para {k}
|
|
38
|
+
if padrao[k] == padrao[q]:
|
|
39
|
+
k += 1 # casou: {k} letras de prefixo que também são sufixo
|
|
40
|
+
pi[q] = k # pi[{q}] = {k}
|
|
41
|
+
return pi
|
|
42
|
+
|
|
43
|
+
def kmp(texto, padrao):
|
|
44
|
+
n, m = len(texto), len(padrao)
|
|
45
|
+
pi = prefixos(padrao) # a tabela diz quanto o padrão anda sem voltar no texto
|
|
46
|
+
achados = []
|
|
47
|
+
q = 0
|
|
48
|
+
for i in range(n):
|
|
49
|
+
ini = i - q # janela começa em {ini}
|
|
50
|
+
while q > 0 and padrao[q] != texto[i]:
|
|
51
|
+
q = pi[q - 1]
|
|
52
|
+
ini = i - q # não casou: o padrão pula para {ini}, e o texto não volta
|
|
53
|
+
if padrao[q] == texto[i]:
|
|
54
|
+
q += 1 # casou: {q} de {m} letras
|
|
55
|
+
if q == m:
|
|
56
|
+
achados.append(i - m + 1) # achou na posição {i - m + 1}
|
|
57
|
+
q = pi[q - 1]
|
|
58
|
+
return achados`,
|
|
59
|
+
call: (s) => `kmp(${str(s.text, "abababcabababab")}, ${str(s.pattern, "ababab")})`,
|
|
60
|
+
view: [{ var: "texto", pointers: ["i"] }, { var: "padrao", under: "texto", offset: "ini", pointers: ["q", "k"] }, { var: "pi", as: "cells" }, { var: "achados" }],
|
|
61
|
+
},
|
|
62
|
+
quicksearch: {
|
|
63
|
+
area: "texto", name: "Quick Search (Sunday)",
|
|
64
|
+
program: `def quick_search(texto, padrao):
|
|
65
|
+
n, m = len(texto), len(padrao)
|
|
66
|
+
salto = {}
|
|
67
|
+
for j in range(m):
|
|
68
|
+
salto[padrao[j]] = m - j # letra '{padrao[j]}': pula {m - j}
|
|
69
|
+
achados = []
|
|
70
|
+
i = 0
|
|
71
|
+
while i <= n - m: # {'encaixa o padrão na posição ' + str(i) if i <= n - m else 'a janela passou do fim do texto'}
|
|
72
|
+
j = 0
|
|
73
|
+
while j < m and texto[i + j] == padrao[j]: # texto[{i + j}] com padrao[{j}]: {'igual, avança' if j < m and texto[i + j] == padrao[j] else 'para aqui'}
|
|
74
|
+
j += 1
|
|
75
|
+
if j == m:
|
|
76
|
+
achados.append(i) # achou na posição {i}
|
|
77
|
+
if i + m >= n:
|
|
78
|
+
break
|
|
79
|
+
c = texto[i + m] # olha a letra logo depois da janela: '{c}'
|
|
80
|
+
i += salto.get(c, m + 1) # '{c}' manda a janela para {i}
|
|
81
|
+
return achados`,
|
|
82
|
+
call: (s) => `quick_search(${str(s.text, "gcatcgcagagagtatacagtacg")}, ${str(s.pattern, "gcagagag")})`,
|
|
83
|
+
view: [{ var: "texto", pointers: ["i + j", "i + m"] }, { var: "padrao", under: "texto", offset: "i", pointers: ["j"] }, { var: "salto", as: "table" }, { var: "achados" }],
|
|
84
|
+
},
|
|
85
|
+
bfs: {
|
|
86
|
+
area: "grafos", name: "Busca em largura (BFS)",
|
|
87
|
+
program: `from collections import deque
|
|
88
|
+
|
|
89
|
+
def bfs(grafo, inicio):
|
|
90
|
+
visitado = {inicio}
|
|
91
|
+
fila = deque([inicio])
|
|
92
|
+
ordem = []
|
|
93
|
+
while fila:
|
|
94
|
+
v = fila.popleft() # sai da fila: {v}
|
|
95
|
+
ordem.append(v)
|
|
96
|
+
for w in grafo[v]: # vizinho de {v}: {w}
|
|
97
|
+
if w not in visitado:
|
|
98
|
+
visitado.add(w)
|
|
99
|
+
fila.append(w) # {w} ainda não foi visto: entra na fila
|
|
100
|
+
return ordem`,
|
|
101
|
+
call: (s) => `bfs(${graphOf(s, GRAFO)}, ${pyLiteral(s.start ?? firstKey(s, GRAFO))})`,
|
|
102
|
+
view: [{ var: "grafo", as: "graph" }, { var: "fila" }, { var: "ordem" }],
|
|
103
|
+
},
|
|
104
|
+
dijkstra: {
|
|
105
|
+
area: "grafos", name: "Dijkstra (menor caminho)",
|
|
106
|
+
program: `import heapq
|
|
107
|
+
|
|
108
|
+
def dijkstra(grafo, origem):
|
|
109
|
+
dist = {v: float('inf') for v in grafo}
|
|
110
|
+
dist[origem] = 0
|
|
111
|
+
fila = [(0, origem)]
|
|
112
|
+
feito = set()
|
|
113
|
+
while fila:
|
|
114
|
+
d, v = heapq.heappop(fila) # o mais perto ainda aberto: {v} (distância {d})
|
|
115
|
+
if v in feito:
|
|
116
|
+
continue
|
|
117
|
+
feito.add(v)
|
|
118
|
+
for w, peso in grafo[v]:
|
|
119
|
+
if d + peso < dist[w]:
|
|
120
|
+
dist[w] = d + peso # caminho melhor até {w}: {dist[w]}
|
|
121
|
+
heapq.heappush(fila, (dist[w], w))
|
|
122
|
+
return dist`,
|
|
123
|
+
call: (s) => `dijkstra(${graphOf(s, GRAFO_PESOS)}, ${pyLiteral(s.start ?? firstKey(s, GRAFO_PESOS))})`,
|
|
124
|
+
view: [{ var: "grafo", as: "graph" }, { var: "dist", as: "table" }, { var: "fila" }],
|
|
125
|
+
},
|
|
126
|
+
bst: {
|
|
127
|
+
area: "árvores", name: "Árvore binária de busca",
|
|
128
|
+
program: `class No:
|
|
129
|
+
def __init__(self, valor):
|
|
130
|
+
self.valor = valor
|
|
131
|
+
self.esq = None
|
|
132
|
+
self.dir = None
|
|
133
|
+
|
|
134
|
+
def insere(raiz, valor):
|
|
135
|
+
if raiz is None:
|
|
136
|
+
return No(valor) # lugar vazio: {valor} vira folha
|
|
137
|
+
if valor < raiz.valor: # {valor} < {raiz.valor}? {'sim: desce à esquerda' if valor < raiz.valor else 'não: desce à direita'}
|
|
138
|
+
raiz.esq = insere(raiz.esq, valor)
|
|
139
|
+
else:
|
|
140
|
+
raiz.dir = insere(raiz.dir, valor)
|
|
141
|
+
return raiz
|
|
142
|
+
|
|
143
|
+
def monta(valores):
|
|
144
|
+
raiz = None
|
|
145
|
+
for x in valores: # insere {x}
|
|
146
|
+
raiz = insere(raiz, x)
|
|
147
|
+
return raiz`,
|
|
148
|
+
call: (s) => `monta(${pyLiteral(Array.isArray(s.array) ? s.array : [50, 30, 70, 20, 40, 60, 80])})`,
|
|
149
|
+
view: [{ var: "raiz", from: "monta", as: "tree" }, { var: "valores" }],
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const CATALOG_AREAS = [...new Set(Object.values(CATALOG).map((c) => c.area))];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Passo a passo de algoritmos (layout algo): o motor roda o algoritmo de verdade sobre o vetor do slide e grava cada
|
|
2
|
+
// passo (comparar, trocar, escrever, pivô, faixa, achou). Quem escreve o slide só diz qual algoritmo e o vetor.
|
|
3
|
+
// algorithm: bubble | insertion | selection | merge | quick | linear | binary array: [5, 1, 4] target: 4
|
|
4
|
+
// Cada passo: { arr, compare: [i, j], swap: [i, j], write: i, pivot: i, range: [lo, hi], sorted: [índices],
|
|
5
|
+
// found: i, line: n (linha do pseudocódigo), text, comparisons, swaps }
|
|
6
|
+
|
|
7
|
+
export const ALGO_CODE = {
|
|
8
|
+
bubble: ["para i de 0 até n-2:", " para j de 0 até n-2-i:", " se v[j] > v[j+1]:", " troca v[j] e v[j+1]", " (o maior chegou ao fim)"],
|
|
9
|
+
insertion: ["para i de 1 até n-1:", " chave = v[i]; j = i - 1", " enquanto j >= 0 e v[j] > chave:", " v[j+1] = v[j]; j = j - 1", " v[j+1] = chave"],
|
|
10
|
+
selection: ["para i de 0 até n-2:", " menor = i", " para j de i+1 até n-1:", " se v[j] < v[menor]: menor = j", " troca v[i] e v[menor]"],
|
|
11
|
+
merge: ["ordena(v, ini, fim):", " se fim - ini < 1: volta", " meio = (ini + fim) / 2", " ordena(v, ini, meio); ordena(v, meio+1, fim)", " intercala as duas metades"],
|
|
12
|
+
quick: ["ordena(v, ini, fim):", " pivô = v[fim]; i = ini", " para j de ini até fim-1:", " se v[j] < pivô: troca v[i] e v[j]; i++", " troca v[i] e v[fim] (pivô no lugar)", " ordena os dois lados"],
|
|
13
|
+
linear: ["para i de 0 até n-1:", " se v[i] == alvo:", " achou na posição i", "não achou"],
|
|
14
|
+
binary: ["ini = 0; fim = n-1", "enquanto ini <= fim:", " meio = (ini + fim) / 2", " se v[meio] == alvo: achou", " se v[meio] < alvo: ini = meio + 1", " senão: fim = meio - 1", "não achou"],
|
|
15
|
+
};
|
|
16
|
+
export const ALGO_NAMES = { bubble: "Bubble sort", insertion: "Insertion sort", selection: "Selection sort", merge: "Merge sort", quick: "Quicksort", linear: "Busca linear", binary: "Busca binária" };
|
|
17
|
+
|
|
18
|
+
export function traceAlgorithm(algorithm, input, { target } = {}) {
|
|
19
|
+
const a = [...input].map(Number);
|
|
20
|
+
if (!ALGO_CODE[algorithm]) throw new Error(`algoritmo "${algorithm}" não existe (${Object.keys(ALGO_CODE).join(" | ")})`);
|
|
21
|
+
if (!a.length || a.some((x) => !Number.isFinite(x))) throw new Error("array: uma lista de números, ex.: [5, 1, 4, 2]");
|
|
22
|
+
if (a.length > 16) throw new Error("array: até 16 números (acima disso não dá para ver o passo a passo)");
|
|
23
|
+
const steps = [];
|
|
24
|
+
let comparisons = 0, swaps = 0;
|
|
25
|
+
const sorted = new Set();
|
|
26
|
+
const push = (o) => steps.push({ arr: [...a], sorted: [...sorted], comparisons, swaps, ...o });
|
|
27
|
+
const n = a.length;
|
|
28
|
+
push({ line: 0, text: `Começo: ${n} números fora de ordem.` });
|
|
29
|
+
if (algorithm === "bubble") {
|
|
30
|
+
for (let i = 0; i < n - 1; i++) {
|
|
31
|
+
let trocou = false;
|
|
32
|
+
for (let j = 0; j < n - 1 - i; j++) {
|
|
33
|
+
comparisons++;
|
|
34
|
+
push({ compare: [j, j + 1], line: 2, text: `Compara ${a[j]} com ${a[j + 1]}.` });
|
|
35
|
+
if (a[j] > a[j + 1]) { [a[j], a[j + 1]] = [a[j + 1], a[j]]; swaps++; trocou = true; push({ swap: [j, j + 1], line: 3, text: `${a[j + 1]} > ${a[j]}: troca.` }); }
|
|
36
|
+
}
|
|
37
|
+
sorted.add(n - 1 - i);
|
|
38
|
+
push({ line: 4, text: `${a[n - 1 - i]} chegou ao lugar dele.` });
|
|
39
|
+
if (!trocou) { for (let k = 0; k < n; k++) sorted.add(k); push({ line: 4, text: "Nenhuma troca nesta volta: já está ordenado." }); break; }
|
|
40
|
+
}
|
|
41
|
+
sorted.add(0);
|
|
42
|
+
} else if (algorithm === "insertion") {
|
|
43
|
+
sorted.add(0);
|
|
44
|
+
for (let i = 1; i < n; i++) {
|
|
45
|
+
const key = a[i];
|
|
46
|
+
let j = i - 1;
|
|
47
|
+
push({ pivot: i, line: 1, text: `Pega ${key} e procura o lugar dele à esquerda.` });
|
|
48
|
+
while (j >= 0) {
|
|
49
|
+
comparisons++;
|
|
50
|
+
push({ compare: [j, j + 1], pivot: j + 1, line: 2, text: `${a[j]} > ${key}?` });
|
|
51
|
+
if (a[j] <= key) break;
|
|
52
|
+
a[j + 1] = a[j]; a[j] = key; swaps++;
|
|
53
|
+
push({ swap: [j, j + 1], pivot: j, line: 3, text: `Sim: ${a[j + 1]} anda uma casa para a direita.` });
|
|
54
|
+
j--;
|
|
55
|
+
}
|
|
56
|
+
for (let k = 0; k <= i; k++) sorted.add(k);
|
|
57
|
+
push({ write: j + 1, line: 4, text: `${key} entra na posição ${j + 1}.` });
|
|
58
|
+
}
|
|
59
|
+
} else if (algorithm === "selection") {
|
|
60
|
+
for (let i = 0; i < n - 1; i++) {
|
|
61
|
+
let min = i;
|
|
62
|
+
push({ pivot: min, line: 1, text: `Procura o menor a partir da posição ${i}.` });
|
|
63
|
+
for (let j = i + 1; j < n; j++) {
|
|
64
|
+
comparisons++;
|
|
65
|
+
push({ compare: [j, min], pivot: min, line: 3, text: `${a[j]} < ${a[min]}?` });
|
|
66
|
+
if (a[j] < a[min]) { min = j; push({ pivot: min, line: 3, text: `Novo menor: ${a[min]}.` }); }
|
|
67
|
+
}
|
|
68
|
+
if (min !== i) { [a[i], a[min]] = [a[min], a[i]]; swaps++; push({ swap: [i, min], line: 4, text: `Troca: ${a[i]} vai para a posição ${i}.` }); }
|
|
69
|
+
sorted.add(i);
|
|
70
|
+
push({ line: 4, text: `${a[i]} está no lugar.` });
|
|
71
|
+
}
|
|
72
|
+
sorted.add(n - 1);
|
|
73
|
+
} else if (algorithm === "merge") {
|
|
74
|
+
const sort = (lo, hi) => {
|
|
75
|
+
if (hi - lo < 1) return;
|
|
76
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
77
|
+
push({ range: [lo, hi], line: 2, text: `Divide [${lo}..${hi}] ao meio.` });
|
|
78
|
+
sort(lo, mid); sort(mid + 1, hi);
|
|
79
|
+
const left = a.slice(lo, mid + 1), right = a.slice(mid + 1, hi + 1);
|
|
80
|
+
let i = 0, j = 0, k = lo;
|
|
81
|
+
while (i < left.length && j < right.length) {
|
|
82
|
+
comparisons++;
|
|
83
|
+
push({ range: [lo, hi], compare: [lo + i, mid + 1 + j], line: 4, text: `Intercala: ${left[i]} ou ${right[j]}?` });
|
|
84
|
+
a[k] = left[i] <= right[j] ? left[i++] : right[j++]; swaps++;
|
|
85
|
+
push({ range: [lo, hi], write: k, line: 4, text: `Escreve ${a[k]} na posição ${k}.` }); k++;
|
|
86
|
+
}
|
|
87
|
+
while (i < left.length) { a[k] = left[i++]; swaps++; push({ range: [lo, hi], write: k, line: 4, text: `Sobrou ${a[k]}: vai para a posição ${k}.` }); k++; }
|
|
88
|
+
while (j < right.length) { a[k] = right[j++]; swaps++; push({ range: [lo, hi], write: k, line: 4, text: `Sobrou ${a[k]}: vai para a posição ${k}.` }); k++; }
|
|
89
|
+
if (lo === 0 && hi === n - 1) for (let x = 0; x < n; x++) sorted.add(x);
|
|
90
|
+
push({ range: [lo, hi], line: 4, text: `[${lo}..${hi}] ordenado.` });
|
|
91
|
+
};
|
|
92
|
+
sort(0, n - 1);
|
|
93
|
+
} else if (algorithm === "quick") {
|
|
94
|
+
const sort = (lo, hi) => {
|
|
95
|
+
if (lo > hi) return;
|
|
96
|
+
if (lo === hi) { sorted.add(lo); return; }
|
|
97
|
+
const p = a[hi];
|
|
98
|
+
let i = lo;
|
|
99
|
+
push({ range: [lo, hi], pivot: hi, line: 1, text: `Pivô: ${p}.` });
|
|
100
|
+
for (let j = lo; j < hi; j++) {
|
|
101
|
+
comparisons++;
|
|
102
|
+
push({ range: [lo, hi], pivot: hi, compare: [j, hi], line: 3, text: `${a[j]} < ${p}?` });
|
|
103
|
+
if (a[j] < p) {
|
|
104
|
+
if (i !== j) { [a[i], a[j]] = [a[j], a[i]]; swaps++; push({ range: [lo, hi], pivot: hi, swap: [i, j], line: 3, text: `Sim: ${a[i]} vai para o lado dos menores.` }); }
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (i !== hi) { [a[i], a[hi]] = [a[hi], a[i]]; swaps++; }
|
|
109
|
+
sorted.add(i);
|
|
110
|
+
push({ range: [lo, hi], swap: i !== hi ? [i, hi] : undefined, line: 4, text: `O pivô ${p} vai para a posição ${i}: menores à esquerda, maiores à direita.` });
|
|
111
|
+
sort(lo, i - 1); sort(i + 1, hi);
|
|
112
|
+
};
|
|
113
|
+
sort(0, n - 1);
|
|
114
|
+
for (let x = 0; x < n; x++) sorted.add(x);
|
|
115
|
+
} else if (algorithm === "linear") {
|
|
116
|
+
let found = -1;
|
|
117
|
+
for (let i = 0; i < n; i++) {
|
|
118
|
+
comparisons++;
|
|
119
|
+
push({ compare: [i], line: 1, text: `${a[i]} é ${target}?` });
|
|
120
|
+
if (a[i] === Number(target)) { found = i; push({ found: i, line: 2, text: `Achou ${target} na posição ${i}, depois de ${comparisons} comparações.` }); break; }
|
|
121
|
+
}
|
|
122
|
+
if (found < 0) push({ line: 3, text: `Não achou ${target}: olhou os ${n} números.` });
|
|
123
|
+
} else if (algorithm === "binary") {
|
|
124
|
+
if (a.some((x, i) => i && a[i - 1] > x)) { a.sort((x, y) => x - y); steps[0] = { ...steps[0], arr: [...a], text: "A busca binária precisa do vetor ordenado: ordenei antes." }; }
|
|
125
|
+
let lo = 0, hi = n - 1, found = -1;
|
|
126
|
+
push({ range: [lo, hi], line: 0, text: `Procura ${target} entre as posições ${lo} e ${hi}.` });
|
|
127
|
+
while (lo <= hi) {
|
|
128
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
129
|
+
comparisons++;
|
|
130
|
+
push({ range: [lo, hi], compare: [mid], line: 2, text: `Meio: posição ${mid} (${a[mid]}).` });
|
|
131
|
+
if (a[mid] === Number(target)) { found = mid; push({ range: [lo, hi], found: mid, line: 3, text: `Achou ${target} em ${comparisons} comparações (a busca linear levaria até ${n}).` }); break; }
|
|
132
|
+
if (a[mid] < Number(target)) { lo = mid + 1; push({ range: [lo, hi], line: 4, text: `${a[mid]} < ${target}: descarta a metade da esquerda.` }); }
|
|
133
|
+
else { hi = mid - 1; push({ range: [lo, hi], line: 5, text: `${a[mid]} > ${target}: descarta a metade da direita.` }); }
|
|
134
|
+
}
|
|
135
|
+
if (found < 0) push({ line: 6, text: `Não achou ${target}.` });
|
|
136
|
+
}
|
|
137
|
+
if (["bubble", "insertion", "selection", "merge", "quick"].includes(algorithm)) push({ sorted: a.map((_, i) => i), line: -1, text: `Pronto: ${comparisons} comparações e ${swaps} ${algorithm === "merge" ? "escritas" : "trocas"}.` });
|
|
138
|
+
return steps;
|
|
139
|
+
}
|