@praeko/studio-core 0.0.0-stage → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ Segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e [SemVer](https://semver.org/lang/pt-BR/).
4
+
5
+ ## [0.1.0] - 2026-11-07
6
+
7
+ Primeira versão publicada, extraída do projeto de divulgação onde nasceu. Publica `dist/` compilado (ESM + `.d.ts`); `react` e `remotion` são peers.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcone Santos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/LICENSES.md ADDED
@@ -0,0 +1,38 @@
1
+ # Licenças do Praeko Studio / Licenses
2
+
3
+ Conferido em 07/11/2026 (verified on 2026-11-07). Este arquivo não é aconselhamento jurídico; vale o texto de cada fornecedor.
4
+
5
+ ## O código do Praeko Studio
6
+ Os pacotes `@praeko/studio`, `@praeko/studio-core` e `@praeko/studio-kit` são **MIT** (ver `LICENSE`, Copyright (c) 2026 Marcone Santos), como `@praeko/sdk` e `@praeko/cli`. Os efeitos sonoros (`pop`, `ding`, `thump`, `whoosh`) e o `avatar.png` do modelo de projeto foram criados para o Studio (sons sintetizados por `scripts/gerar-sfx.mjs`, sem amostras de terceiros) e seguem a mesma licença MIT. O pacote não traz fontes, músicas, imagens de banco nem arte de terceiros.
7
+
8
+ ## Aviso que você precisa ver (Remotion NÃO é MIT)
9
+ > **Remotion.** O Remotion é *source-available*, não open source. É **grátis** para pessoas físicas, organizações sem fins lucrativos, avaliação e empresas com **até 3 funcionários** (uso comercial incluído). Empresas com **4 ou mais pessoas** precisam da **Company License** (remotion.pro/license). Quem instala e roda o Remotion é **você**: a licença é sua, não do Praeko. O Remotion também vende planos para quem **automatiza renders por programa ou agente** (Creators, Automators); o FAQ deles não diz expressamente se um agente local que roda `remotion render` entra: **pergunte ao Remotion** antes de usar em escala. O Praeko Studio não copia nem modifica código do Remotion: usa-o como dependência (peer) instalada por você.
10
+ >
11
+ > **Remotion** is source-available, not open source. Free for individuals, non-profits, evaluation and companies of up to 3 employees (commercial use included); companies of 4+ need the Company License (remotion.pro/license). You install and run Remotion, so the license is yours. They also sell plans for programmatic/agent-driven rendering; their FAQ does not say whether a local agent running `remotion render` counts: ask Remotion before using at scale. Praeko Studio does not copy or modify Remotion code; it uses it as a dependency you install.
12
+
13
+ Este aviso aparece no `README`, na instalação (`praeko studio init`), em `praeko studio doctor` e em `praeko studio licencas` (`--aceitar` registra a leitura em `~/.config/praeko/studio/licencas.json`).
14
+
15
+ **Capas** são renderizadas com `remotion still`, mesmo com `--motor hyperframes`: hoje não existe projeto 100% livre de Remotion. Uma capa sem Remotion está no roteiro de trabalho (`docs/studio-produto.md`, item 9) e não foi prometida.
16
+
17
+ **Não prometemos "grátis para sempre":** as condições do Remotion podem mudar (a própria página avisa que a licença muda no Remotion 5.0). Reveja a licença ao subir de versão.
18
+
19
+ ## Dependências de terceiros
20
+ | Componente | Onde | Licença | Fonte |
21
+ |---|---|---|---|
22
+ | `remotion`, `@remotion/cli` (4.0.539) | instalados no **projeto** do usuário (peer de core/kit) | Remotion License (ver acima), não MIT | remotion.pro/license; github.com/remotion-dev/remotion (LICENSE.md) |
23
+ | `react`, `react-dom` (19.2.3) | projeto do usuário (peer) | MIT | package.json de cada pacote |
24
+ | `@praeko/studio-core`, `-kit` | projeto do usuário | MIT | LICENSE |
25
+ | `yaml` (2.9.1) | embutido no bundle de `@praeko/studio` | ISC | package.json |
26
+ | `@elevenlabs/elevenlabs-js` (2.x) | dependência de `@praeko/studio` | MIT | package.json |
27
+ | `hyperframes` (0.9.13, motor opcional) | baixado por `npx` na primeira vez que você usa `--motor hyperframes` | Apache-2.0 | github.com/heygen-com/hyperframes |
28
+ | `typescript`, `esbuild`, `@types/*` | só desenvolvimento/build | Apache-2.0 / MIT | package.json |
29
+ | ffmpeg / ffprobe | você instala | LGPL ou GPL, conforme a compilação | ffmpeg.org/legal.html |
30
+ | Chrome Headless Shell / Chromium | o Remotion baixa no primeiro render | licenças do Chromium/Google | remotion.dev/docs/miscellaneous/chrome-headless-shell |
31
+
32
+ ## Serviços e modelos (você contrata)
33
+ - **ElevenLabs**: seus termos e seu plano (clonagem de voz só com direito sobre a voz). A chave é sua; o Studio nunca a imprime.
34
+ - **VoiceStudio local** (opcional): o modelo OmniVoice é **CC-BY-NC (uso não comercial)**. Não é padrão em nenhuma configuração do Studio e **não deve ser usado em conteúdo monetizado**. Confira a licença de qualquer modelo local que usar.
35
+ - **YouTube, Instagram, TikTok**: termos e regras de conteúdo sintético de cada rede (veja `praeko studio docs publicar`).
36
+
37
+ ## Fontes e mídia
38
+ Nenhuma fonte é distribuída. O modelo de projeto usa fontes do sistema (`Arial Black`, `Menlo`) que o `doctor` confere; troque-as no `marca.json` por fontes abertas (OFL) se quiser que o vídeo saia igual em todos os sistemas. Música, imagens e clipes que você adicionar são responsabilidade sua (direitos e licenças).
package/README.md CHANGED
@@ -1,3 +1,20 @@
1
- # Temporary Holding Version
1
+ # @praeko/studio-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Motor do Praeko Studio (peças Remotion): episódio por voz e palavras com tempo, legendas palavra a palavra, golpes de edição, fechamento e tema da marca. Biblioteca para projetos criados com [`@praeko/studio`](https://www.npmjs.com/package/@praeko/studio) (`praeko studio init` já instala e configura).
4
+
5
+ > **Licenças.** Este pacote é MIT e usa o **Remotion** como dependência (peer) que **você** instala. O Remotion **não é MIT**: grátis para pessoas, organizações sem fins lucrativos e empresas de até 3 funcionários; empresas com 4 ou mais pessoas precisam da Company License ([remotion.pro/license](https://www.remotion.pro/license)). Detalhes em [LICENSES.md](./LICENSES.md).
6
+ >
7
+ > **Licenses.** This package is MIT and uses **Remotion** as a peer dependency that **you** install. Remotion is **not MIT**: free for individuals, non-profits and companies of up to 3 employees; companies of 4+ need the Company License.
8
+
9
+ ## Instalar
10
+ ```sh
11
+ # npm
12
+ npm install @praeko/studio-core remotion react
13
+ # yarn
14
+ yarn add @praeko/studio-core remotion react
15
+ # pnpm
16
+ pnpm add @praeko/studio-core remotion react
17
+ # bun
18
+ bun add @praeko/studio-core remotion react
19
+ ```
20
+ Peers: `remotion` ^4.0.539 (todos os pacotes `@remotion/*` na mesma versão) e `react` ^19. Node 22 ou mais novo. Uso: `praeko studio docs episodio-yaml`.
@@ -0,0 +1,5 @@
1
+ export * from "./motor";
2
+ export * from "./palavra";
3
+ export * from "./tema";
4
+ export * from "./util";
5
+ export * from "./longo";
package/dist/index.js ADDED
@@ -0,0 +1,460 @@
1
+ // src/motor.tsx
2
+ import { AbsoluteFill, Audio, Img, Sequence, interpolate as interpolate2, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
3
+
4
+ // src/tema.tsx
5
+ import { createContext, useContext } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ var TEMA_PADRAO = {
8
+ fundo: "#14110f",
9
+ fundoCapa: "#0e0c14",
10
+ destaque: "#f4b942",
11
+ texto: "#f3ece0",
12
+ suave: "#a89c8a",
13
+ ok: "#7ee787",
14
+ erro: "#ff6b5e",
15
+ info: "#7fd7ff",
16
+ extra: "#a98bff",
17
+ caixa: "#2a2118",
18
+ codigo: "#1b1610",
19
+ fonte: "'Arial Black', 'Helvetica Neue', system-ui, sans-serif",
20
+ mono: "Menlo, 'Courier New', monospace",
21
+ handle: "@seucanal",
22
+ avatar: "capa/avatar.png"
23
+ };
24
+ var Ctx = createContext(TEMA_PADRAO);
25
+ var TemaProvider = ({ tema, children }) => /* @__PURE__ */ jsx(Ctx.Provider, { value: tema, children });
26
+ var useTema = () => useContext(Ctx);
27
+ var temaDaMarca = (m) => ({
28
+ ...TEMA_PADRAO,
29
+ ...m.cores || {},
30
+ ...m.fontes?.titulo ? { fonte: m.fontes.titulo } : {},
31
+ ...m.fontes?.mono ? { mono: m.fontes.mono } : {},
32
+ ...m.handle ? { handle: m.handle } : {},
33
+ ...m.avatar ? { avatar: m.avatar } : {}
34
+ });
35
+ var rgba = (hex, a) => {
36
+ const n = parseInt(hex.slice(1), 16);
37
+ return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
38
+ };
39
+
40
+ // src/util.ts
41
+ import { Easing, interpolate } from "remotion";
42
+ var FPS = 30;
43
+ var clamp = { extrapolateLeft: "clamp", extrapolateRight: "clamp" };
44
+ var EO = Easing.bezier(0.16, 1, 0.3, 1);
45
+ var surge = (t, q, d = 0.35) => interpolate(t, [q, q + d], [0, 1], { ...clamp, easing: EO });
46
+ var ramp = (t, a, b) => interpolate(t, [a, b], [0, 1], { ...clamp, easing: Easing.out(Easing.cubic) });
47
+ var pop = (t, q) => interpolate(t, [q, q + 0.18, q + 0.4], [0.6, 1.14, 1], { ...clamp, easing: Easing.out(Easing.cubic) });
48
+ var fmt = (n) => String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
49
+ var norm = (s) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]/g, "");
50
+ var mkAt = (P, S) => {
51
+ const W = P.map((p) => norm(p.t));
52
+ return (i) => (frase, nth = 0) => {
53
+ const f2 = frase.split(/\s+/).map(norm).filter(Boolean);
54
+ let c = 0;
55
+ for (let k = 0; k <= W.length - f2.length; k++) {
56
+ if (f2.every((w, j) => W[k + j] === w)) {
57
+ if (c === nth) return P[k].a - S[i];
58
+ c++;
59
+ }
60
+ }
61
+ throw new Error("frase n\xE3o achada: " + frase);
62
+ };
63
+ };
64
+
65
+ // src/motor.tsx
66
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
67
+ var f = (s) => Math.round(s * FPS);
68
+ var DUR_SOM = { pop: 0.16, ding: 0.9, thump: 0.4, whoosh: 0.55 };
69
+ var VOL_SOM = { pop: 0.22, ding: 0.26, thump: 0.33, whoosh: 0.28 };
70
+ var Rotulo = ({ texto }) => {
71
+ const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
72
+ const frame = useCurrentFrame();
73
+ const { fps } = useVideoConfig();
74
+ const pop2 = spring({ frame: frame - 3, fps, config: { damping: 11, stiffness: 200 } });
75
+ return /* @__PURE__ */ jsx2(
76
+ "div",
77
+ {
78
+ style: {
79
+ position: "absolute",
80
+ top: 70,
81
+ left: 0,
82
+ right: 0,
83
+ display: "flex",
84
+ justifyContent: "center",
85
+ transform: `scale(${0.6 + 0.4 * pop2}) rotate(${(1 - pop2) * -4}deg)`,
86
+ opacity: Math.min(1, pop2 * 1.5)
87
+ },
88
+ children: /* @__PURE__ */ jsx2(
89
+ "div",
90
+ {
91
+ style: {
92
+ background: LAMP,
93
+ color: BG,
94
+ fontFamily: FONT,
95
+ fontSize: 62,
96
+ fontWeight: 900,
97
+ letterSpacing: 1,
98
+ padding: "14px 36px",
99
+ borderRadius: 22,
100
+ boxShadow: "0 10px 30px rgba(0,0,0,.55)",
101
+ whiteSpace: "nowrap"
102
+ },
103
+ children: texto
104
+ }
105
+ )
106
+ }
107
+ );
108
+ };
109
+ var Fundo = () => {
110
+ const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
111
+ const frame = useCurrentFrame();
112
+ const pulso = 0.16 + 0.05 * Math.sin(frame / 22);
113
+ return /* @__PURE__ */ jsx2(
114
+ AbsoluteFill,
115
+ {
116
+ style: {
117
+ background: `radial-gradient(ellipse at 50% 20%, ${rgba(LAMP, pulso)} 0%, ${rgba(LAMP, 0)} 55%), radial-gradient(ellipse at 50% 100%, rgba(80,60,120,.22) 0%, rgba(0,0,0,0) 60%), ${BG}`
118
+ },
119
+ children: Array.from({ length: 26 }).map((_, i) => {
120
+ const x = i * 137 % 1080;
121
+ const vel = 0.35 + i % 5 * 0.18;
122
+ const y = (i * 251 % 1920 - frame * vel + 1920 * 4) % 1920;
123
+ const r = 3 + i % 4 * 2;
124
+ return /* @__PURE__ */ jsx2(
125
+ "div",
126
+ {
127
+ style: {
128
+ position: "absolute",
129
+ left: x,
130
+ top: y,
131
+ width: r,
132
+ height: r,
133
+ borderRadius: r,
134
+ background: LAMP,
135
+ opacity: 0.1 + i % 4 * 0.05
136
+ }
137
+ },
138
+ i
139
+ );
140
+ })
141
+ }
142
+ );
143
+ };
144
+ var agrupar = (ps) => {
145
+ const g = [];
146
+ let cur = [];
147
+ const fecha = () => {
148
+ if (cur.length) g.push({ palavras: cur, a: cur[0].a, b: cur[cur.length - 1].b });
149
+ cur = [];
150
+ };
151
+ ps.forEach((p, i) => {
152
+ cur.push(p);
153
+ const gap = i < ps.length - 1 ? ps[i + 1].a - p.b : 1;
154
+ const letras = cur.reduce((n, x) => n + x.t.length, 0);
155
+ const prox = ps[i + 1]?.t.length ?? 0;
156
+ if (/[.,?!:]$/.test(p.t) || cur.length >= 3 || letras + prox > 15 || gap > 0.3) fecha();
157
+ });
158
+ fecha();
159
+ return g;
160
+ };
161
+ var FundoTerminal = ({ linhas }) => {
162
+ const { destaque: LAMP, ok: OK } = useTema();
163
+ const frame = useCurrentFrame();
164
+ const n = Math.max(1, linhas.length);
165
+ const passo = 64, ciclo = 34 * passo + 0;
166
+ return /* @__PURE__ */ jsxs(AbsoluteFill, { style: { overflow: "hidden" }, children: [
167
+ /* @__PURE__ */ jsx2(Fundo, {}),
168
+ /* @__PURE__ */ jsx2("div", { style: { position: "absolute", inset: 0, opacity: 0.09, filter: "blur(1.2px)" }, children: Array.from({ length: 34 }).map((_, i) => {
169
+ const y = ((i * passo - frame * 0.8) % ciclo + ciclo) % ciclo - 160;
170
+ const col = (i + Math.floor(i / n)) % 2;
171
+ return /* @__PURE__ */ jsx2(
172
+ "div",
173
+ {
174
+ style: {
175
+ position: "absolute",
176
+ left: 30 + col * 420,
177
+ top: y,
178
+ fontFamily: "Menlo, 'Courier New', monospace",
179
+ fontSize: 28,
180
+ color: i % 4 === 0 ? LAMP : OK,
181
+ whiteSpace: "nowrap"
182
+ },
183
+ children: linhas[i % n]
184
+ },
185
+ i
186
+ );
187
+ }) })
188
+ ] });
189
+ };
190
+ var LegendaEp = ({ palavras, bottom, px = 90 }) => {
191
+ const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
192
+ const frame = useCurrentFrame();
193
+ const t = frame / FPS;
194
+ const grupos2 = agrupar(palavras);
195
+ const idx = grupos2.findIndex((g2, i) => {
196
+ const prox = grupos2[i + 1]?.a ?? g2.b + 0.4;
197
+ return t >= g2.a - 0.05 && t < Math.max(g2.b + 0.15, Math.min(prox, g2.b + 0.5));
198
+ });
199
+ if (idx < 0) return null;
200
+ const g = grupos2[idx];
201
+ const dentro = frame - f(g.a);
202
+ const entra = interpolate2(dentro, [-2, 4], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
203
+ return /* @__PURE__ */ jsx2(
204
+ "div",
205
+ {
206
+ style: {
207
+ position: "absolute",
208
+ left: px < 90 ? 70 : 40,
209
+ right: px < 90 ? 70 : 40,
210
+ bottom,
211
+ display: "flex",
212
+ justifyContent: "center",
213
+ gap: px < 90 ? 64 : 44,
214
+ flexWrap: "wrap",
215
+ opacity: entra,
216
+ transform: `translateY(${(1 - entra) * 24}px)`
217
+ },
218
+ children: g.palavras.map((p, i) => {
219
+ const ativa = t >= p.a && t < p.b + 0.05;
220
+ const ja = t >= p.a;
221
+ return /* @__PURE__ */ jsx2(
222
+ "span",
223
+ {
224
+ style: {
225
+ fontFamily: FONT,
226
+ fontWeight: 900,
227
+ fontSize: px,
228
+ lineHeight: 1.05,
229
+ textTransform: "uppercase",
230
+ color: ativa ? LAMP : ja ? INK : rgba(INK, 0.55),
231
+ transform: `scale(${ativa ? 1.12 : 1})`,
232
+ textShadow: "0 6px 0 #000, 0 0 24px rgba(0,0,0,.85), 4px 0 0 #000, -4px 0 0 #000"
233
+ },
234
+ children: p.t
235
+ },
236
+ i
237
+ );
238
+ })
239
+ }
240
+ );
241
+ };
242
+ var FechamentoEp = ({ cta }) => {
243
+ const tema = useTema();
244
+ const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
245
+ const frame = useCurrentFrame();
246
+ const { fps } = useVideoConfig();
247
+ const logo = spring({ frame, fps, config: { damping: 10, stiffness: 150 } });
248
+ const nome = spring({ frame: frame - 8, fps, config: { damping: 200 } });
249
+ const pill = spring({ frame: frame - 20, fps, config: { damping: 12, stiffness: 160 } });
250
+ return /* @__PURE__ */ jsxs(
251
+ AbsoluteFill,
252
+ {
253
+ style: {
254
+ background: `radial-gradient(circle at 50% 40%, ${CAIXA} 0%, ${BG} 72%)`,
255
+ justifyContent: "center",
256
+ alignItems: "center",
257
+ fontFamily: FONT,
258
+ textAlign: "center"
259
+ },
260
+ children: [
261
+ /* @__PURE__ */ jsx2(
262
+ "div",
263
+ {
264
+ style: {
265
+ width: 420,
266
+ height: 420,
267
+ borderRadius: 210,
268
+ overflow: "hidden",
269
+ border: `12px solid ${LAMP}`,
270
+ boxShadow: `0 0 80px ${rgba(LAMP, 0.45)}`,
271
+ transform: `scale(${0.6 + 0.4 * logo})`,
272
+ opacity: Math.min(1, logo * 1.5)
273
+ },
274
+ children: /* @__PURE__ */ jsx2(Img, { src: staticFile(tema.avatar), style: { width: 420, height: 420, imageRendering: "pixelated" } })
275
+ }
276
+ ),
277
+ /* @__PURE__ */ jsx2(
278
+ "div",
279
+ {
280
+ style: {
281
+ marginTop: 46,
282
+ fontSize: 92,
283
+ fontWeight: 900,
284
+ color: LAMP,
285
+ opacity: Math.max(0, nome),
286
+ transform: `translateY(${(1 - Math.max(0, nome)) * 30}px)`
287
+ },
288
+ children: tema.handle
289
+ }
290
+ ),
291
+ /* @__PURE__ */ jsx2(
292
+ "div",
293
+ {
294
+ style: {
295
+ marginTop: 50,
296
+ background: LAMP,
297
+ color: BG,
298
+ fontSize: cta.length > 18 ? 54 : 64,
299
+ fontWeight: 900,
300
+ padding: "20px 44px",
301
+ borderRadius: 28,
302
+ opacity: Math.max(0, pill),
303
+ transform: `scale(${0.6 + 0.4 * Math.max(0, pill)})`
304
+ },
305
+ children: cta
306
+ }
307
+ )
308
+ ]
309
+ }
310
+ );
311
+ };
312
+ var Barra = ({ durS }) => {
313
+ const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
314
+ const frame = useCurrentFrame();
315
+ const w = Math.min(100, frame / f(durS) * 100);
316
+ return /* @__PURE__ */ jsx2("div", { style: { position: "absolute", top: 0, left: 0, right: 0, height: 10, background: "rgba(0,0,0,.4)" }, children: /* @__PURE__ */ jsx2("div", { style: { width: `${w}%`, height: "100%", background: LAMP } }) });
317
+ };
318
+ var Golpes = ({ golpes, children }) => {
319
+ const t = useCurrentFrame() / FPS;
320
+ let sx = 0, sy = 0, zoom = 1, flash = 0;
321
+ for (const g of golpes) {
322
+ const dt = t - g.at;
323
+ if (dt < 0 || dt > 0.45) continue;
324
+ const k = (1 - dt / 0.45) ** 2;
325
+ if (g.tipo === "shake" || g.tipo === "tudo") {
326
+ sx += Math.sin(dt * 95) * 16 * k;
327
+ sy += Math.cos(dt * 120) * 9 * k;
328
+ }
329
+ if (g.tipo === "zoom" || g.tipo === "tudo") zoom = Math.max(zoom, 1 + 0.055 * k);
330
+ if (g.tipo === "flash" || g.tipo === "tudo") flash = Math.max(flash, 0.33 * k);
331
+ }
332
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
333
+ /* @__PURE__ */ jsx2(AbsoluteFill, { style: { transform: `translate(${sx}px, ${sy}px) scale(${zoom})` }, children }),
334
+ flash > 0.01 ? /* @__PURE__ */ jsx2(AbsoluteFill, { style: { background: `rgba(255,244,214,${flash})`, pointerEvents: "none" } }) : null
335
+ ] });
336
+ };
337
+ var Episodio = ({ d }) => {
338
+ const { fundo: BG } = useTema();
339
+ return /* @__PURE__ */ jsxs(AbsoluteFill, { style: { backgroundColor: BG }, children: [
340
+ /* @__PURE__ */ jsx2(Audio, { src: staticFile(d.voz) }),
341
+ /* @__PURE__ */ jsxs(Golpes, { golpes: d.golpes ?? [], children: [
342
+ /* @__PURE__ */ jsx2(FundoTerminal, { linhas: d.fundoLinhas ?? ["\u2713 ok"] }),
343
+ /* @__PURE__ */ jsx2(Sequence, { from: 0, durationInFrames: f(DUR_SOM.whoosh) + 2, children: /* @__PURE__ */ jsx2(Audio, { src: staticFile("sfx/whoosh.wav"), volume: VOL_SOM.whoosh }) }),
344
+ d.cenas.flatMap((c, i) => {
345
+ const sons = [{ at: 0, f: "pop" }, ...c.sons ?? []];
346
+ return sons.map((s, k) => /* @__PURE__ */ jsx2(Sequence, { from: f(c.a + s.at), durationInFrames: f(DUR_SOM[s.f]) + 2, children: /* @__PURE__ */ jsx2(Audio, { src: staticFile(`sfx/${s.f}.wav`), volume: s.v ?? VOL_SOM[s.f] }) }, `s${i}-${k}`));
347
+ }),
348
+ d.cenas.map((c, i) => /* @__PURE__ */ jsx2(Sequence, { from: f(c.a), durationInFrames: f(c.b - c.a), premountFor: 10, children: /* @__PURE__ */ jsx2(c.C, { dur: c.b - c.a }) }, "c" + i)),
349
+ d.cenas.map(
350
+ (c, i) => c.rotulo ? /* @__PURE__ */ jsx2(Sequence, { from: f(c.a), durationInFrames: f(c.b - c.a), children: /* @__PURE__ */ jsx2(Rotulo, { texto: c.rotulo }) }, "r" + i) : null
351
+ )
352
+ ] }),
353
+ /* @__PURE__ */ jsx2(Sequence, { from: f(d.fechamento.a), durationInFrames: f(d.durS - d.fechamento.a), children: /* @__PURE__ */ jsx2(FechamentoEp, { cta: d.fechamento.cta }) }),
354
+ /* @__PURE__ */ jsx2(Sequence, { from: 0, durationInFrames: f(d.durS), children: /* @__PURE__ */ jsx2(LegendaEp, { palavras: d.palavras, bottom: 190, px: d.legendaPx }) }),
355
+ /* @__PURE__ */ jsx2(Sequence, { from: f(d.fechamento.a), durationInFrames: 2, children: /* @__PURE__ */ jsx2(Audio, { src: staticFile("sfx/ding.wav"), volume: 0.22 }) }),
356
+ /* @__PURE__ */ jsx2(Barra, { durS: d.durS })
357
+ ] });
358
+ };
359
+
360
+ // src/longo.tsx
361
+ import { AbsoluteFill as AbsoluteFill2, Audio as Audio2, Img as Img2, interpolate as interpolate3, spring as spring2, staticFile as staticFile2, useCurrentFrame as useCurrentFrame2, useVideoConfig as useVideoConfig2 } from "remotion";
362
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
363
+ var clamp2 = { extrapolateLeft: "clamp", extrapolateRight: "clamp" };
364
+ var janela = (t, a, b, d = 0.4) => interpolate3(t, [a, a + d, b - d, b], [0, 1, 1, 0], clamp2);
365
+ var mkAtG = (P) => {
366
+ const W = P.map((p) => norm(p.t));
367
+ return (frase, nth = 0, from = 0) => {
368
+ const f2 = frase.split(/\s+/).map(norm).filter(Boolean);
369
+ let c = 0;
370
+ for (let i = 0; i <= W.length - f2.length; i++) {
371
+ if (P[i].a < from) continue;
372
+ if (f2.every((w, k) => W[i + k] === w)) {
373
+ if (c === nth) return P[i].a;
374
+ c++;
375
+ }
376
+ }
377
+ throw new Error("frase n\xE3o achada: " + frase);
378
+ };
379
+ };
380
+ var Cena = ({ t, a, b, n, titulo, children }) => {
381
+ const T = useTema();
382
+ if (t < a - 0.1 || t > b + 0.1) return null;
383
+ return /* @__PURE__ */ jsxs2(AbsoluteFill2, { style: { opacity: janela(t, a, b) }, children: [
384
+ /* @__PURE__ */ jsxs2("div", { style: { position: "absolute", left: 100, top: 46, display: "flex", alignItems: "center", gap: 18, fontFamily: T.fonte, fontSize: 30, letterSpacing: 4, color: T.destaque, whiteSpace: "nowrap" }, children: [
385
+ /* @__PURE__ */ jsx3("span", { style: { background: T.destaque, color: T.fundo, borderRadius: 12, padding: "4px 16px" }, children: n }),
386
+ titulo
387
+ ] }),
388
+ children
389
+ ] });
390
+ };
391
+ var grupos = (P) => {
392
+ const g = [];
393
+ let cur = [];
394
+ P.forEach((p) => {
395
+ cur.push(p);
396
+ if (/[.!?:]$/.test(p.t) || cur.length >= 8 || /[,;]$/.test(p.t) && cur.length >= 5) {
397
+ g.push(cur);
398
+ cur = [];
399
+ }
400
+ });
401
+ if (cur.length) g.push(cur);
402
+ return g;
403
+ };
404
+ var Legenda = ({ t, G }) => {
405
+ const T = useTema();
406
+ let grupo = null;
407
+ for (const g of G) if (t >= g[0].a - 0.05 && t <= g[g.length - 1].b + 0.4) grupo = g;
408
+ if (!grupo) return null;
409
+ return /* @__PURE__ */ jsx3("div", { style: { position: "absolute", left: 160, right: 160, bottom: 56, textAlign: "center", fontFamily: T.fonte, fontSize: 46, lineHeight: 1.2, textShadow: "0 3px 12px rgba(0,0,0,.9)" }, children: grupo.map((p, i) => /* @__PURE__ */ jsxs2("span", { style: { color: t >= p.a && t < p.b + 0.05 ? T.destaque : T.texto }, children: [
410
+ p.t,
411
+ i < grupo.length - 1 ? " " : ""
412
+ ] }, i)) });
413
+ };
414
+ var FechoTela = ({ f: f2, t }) => {
415
+ const T = useTema();
416
+ const { fps } = useVideoConfig2();
417
+ const k = (atraso) => spring2({ frame: (t - f2.a - atraso) * fps, fps, config: { damping: 14, stiffness: 140 } });
418
+ const op = interpolate3(t, [f2.a, f2.a + 0.5], [0, 1], clamp2);
419
+ return /* @__PURE__ */ jsxs2(AbsoluteFill2, { style: { opacity: op, background: `radial-gradient(circle at 50% 40%, ${T.caixa} 0%, ${T.fundo} 75%)`, alignItems: "center", justifyContent: "center", fontFamily: T.fonte, textAlign: "center", paddingBottom: 90 }, children: [
420
+ /* @__PURE__ */ jsx3("div", { style: { width: 230, height: 230, borderRadius: 44, overflow: "hidden", border: `8px solid ${f2.imagem ? "#3a3566" : T.destaque}`, background: f2.imagem ? "#1c1a33" : void 0, boxShadow: `0 0 70px ${rgba(T.destaque, 0.3)}`, transform: `scale(${0.6 + 0.4 * k(0)})`, opacity: Math.min(1, k(0) * 1.5) }, children: /* @__PURE__ */ jsx3(Img2, { src: staticFile2(f2.imagem ?? T.avatar), style: { width: 230, height: 230, imageRendering: "pixelated", objectFit: "contain" } }) }),
421
+ f2.texto ? /* @__PURE__ */ jsx3("div", { style: { marginTop: 44, fontSize: 104, fontWeight: 900, color: T.destaque, opacity: Math.max(0, k(0.15)), transform: `translateY(${(1 - Math.max(0, k(0.15))) * 24}px)` }, children: f2.texto }) : null,
422
+ /* @__PURE__ */ jsx3("div", { style: { marginTop: 28, fontSize: 54, fontWeight: 900, color: T.texto, opacity: Math.max(0, k(0.3)) }, children: T.handle }),
423
+ f2.link ? /* @__PURE__ */ jsx3("div", { style: { marginTop: 18, fontSize: 38, fontFamily: T.mono, color: T.suave, opacity: Math.max(0, k(0.45)) }, children: f2.link }) : null
424
+ ] });
425
+ };
426
+ var Longo = ({ d }) => {
427
+ const T = useTema();
428
+ const t = useCurrentFrame2() / FPS;
429
+ const G = grupos(d.palavras);
430
+ return /* @__PURE__ */ jsxs2(AbsoluteFill2, { style: { background: T.fundo, color: T.texto, fontFamily: T.fonte }, children: [
431
+ /* @__PURE__ */ jsx3(AbsoluteFill2, { style: { background: `radial-gradient(1200px 700px at 70% 20%, ${rgba(T.destaque, 0.07)}, transparent 70%)` } }),
432
+ /* @__PURE__ */ jsx3(Audio2, { src: staticFile2(d.voz) }),
433
+ d.cenas.map((c) => /* @__PURE__ */ jsx3(Cena, { t, a: c.a, b: c.b, n: c.n, titulo: c.titulo, children: /* @__PURE__ */ jsx3(c.C, { t }) }, c.n)),
434
+ d.fecho && t >= d.fecho.a ? /* @__PURE__ */ jsx3(FechoTela, { f: d.fecho, t }) : null,
435
+ /* @__PURE__ */ jsx3(Legenda, { t, G }),
436
+ /* @__PURE__ */ jsx3("div", { style: { position: "absolute", left: 0, top: 0, height: 8, width: `${t / d.durS * 100}%`, background: T.destaque } })
437
+ ] });
438
+ };
439
+ export {
440
+ Episodio,
441
+ FPS,
442
+ Fundo,
443
+ Longo,
444
+ Rotulo,
445
+ TEMA_PADRAO,
446
+ TemaProvider,
447
+ agrupar,
448
+ clamp,
449
+ fmt,
450
+ janela,
451
+ mkAt,
452
+ mkAtG,
453
+ norm,
454
+ pop,
455
+ ramp,
456
+ rgba,
457
+ surge,
458
+ temaDaMarca,
459
+ useTema
460
+ };
@@ -0,0 +1,39 @@
1
+ import { Palavra } from "./palavra";
2
+ /**
3
+ * MOTOR DE LONGO (16:9, vários minutos). Um longo é uma lista de cenas em tempo GLOBAL: cada cena recebe `t` (s desde o início do vídeo)
4
+ * e se liga à voz com `mkAtG(PALAVRAS)(frase, nth, from)`. O motor cuida do fundo, da voz, do cabeçalho da cena (número + título),
5
+ * da legenda por grupos e da barra de progresso. As cenas são TSX do projeto, montadas com as peças de `@praeko/studio-kit/longo`.
6
+ */
7
+ export type CenaLongo = {
8
+ n: string;
9
+ titulo: string;
10
+ a: number;
11
+ b: number;
12
+ C: React.FC<{
13
+ t: number;
14
+ }>;
15
+ };
16
+ /** Tela final opcional: avatar, texto (ex.: a assinatura), handle da marca e link. Entra em `a` (s) e fica até o fim. */
17
+ export type FechoLongo = {
18
+ a: number;
19
+ texto?: string;
20
+ link?: string;
21
+ imagem?: string;
22
+ };
23
+ export type LongoData = {
24
+ id: string;
25
+ durS: number;
26
+ voz: string;
27
+ palavras: Palavra[];
28
+ cenas: CenaLongo[];
29
+ largura?: number;
30
+ altura?: number;
31
+ fecho?: FechoLongo;
32
+ };
33
+ /** Entra e sai com fade de `d` s nas pontas da janela [a, b]. */
34
+ export declare const janela: (t: number, a: number, b: number, d?: number) => number;
35
+ /** Âncora global: `at("frase", nth, from)` = instante (s) em que a frase começa; `from` ignora ocorrências antes de `from` s. Falha alto se não achar. */
36
+ export declare const mkAtG: (P: Palavra[]) => (frase: string, nth?: number, from?: number) => number;
37
+ export declare const Longo: React.FC<{
38
+ d: LongoData;
39
+ }>;
@@ -0,0 +1,56 @@
1
+ import { Palavra } from "./palavra";
2
+ /**
3
+ * NÚCLEO DO PRAEKO STUDIO.
4
+ * Um episódio é só dados (EpData): voz, palavras com tempo, cenas (cartão + rótulo + sons) e fechamento.
5
+ * O resto (fundo, legenda, barra de progresso, fechamento, efeitos) é compartilhado e usa o tema da marca.
6
+ */
7
+ export type Som = {
8
+ at: number;
9
+ f: "pop" | "ding" | "thump" | "whoosh";
10
+ v?: number;
11
+ };
12
+ export type Golpe = {
13
+ at: number;
14
+ tipo: "shake" | "zoom" | "flash" | "tudo";
15
+ };
16
+ export type EpCena = {
17
+ a: number;
18
+ b: number;
19
+ C: React.FC<{
20
+ dur: number;
21
+ }>;
22
+ rotulo?: string;
23
+ /** sons relativos ao início da cena (s) */
24
+ sons?: Som[];
25
+ };
26
+ export type EpData = {
27
+ id: string;
28
+ durS: number;
29
+ voz: string;
30
+ palavras: Palavra[];
31
+ cenas: EpCena[];
32
+ fechamento: {
33
+ a: number;
34
+ cta: string;
35
+ };
36
+ /** linhas de "terminal" desfocadas no fundo */
37
+ fundoLinhas?: string[];
38
+ /** tamanho da legenda (padrão 90) */
39
+ legendaPx?: number;
40
+ /** golpes de edição: tremor, clarão e zoom rápidos em instantes (s) da narração */
41
+ golpes?: Golpe[];
42
+ };
43
+ export declare const Rotulo: React.FC<{
44
+ texto: string;
45
+ }>;
46
+ /** Fundo limpo: escuro, brilho âmbar que pulsa e pontinhos que sobem devagar. */
47
+ export declare const Fundo: React.FC;
48
+ export type Grupo = {
49
+ palavras: Palavra[];
50
+ a: number;
51
+ b: number;
52
+ };
53
+ export declare const agrupar: (ps: Palavra[]) => Grupo[];
54
+ export declare const Episodio: React.FC<{
55
+ d: EpData;
56
+ }>;
@@ -0,0 +1,5 @@
1
+ export type Palavra = {
2
+ t: string;
3
+ a: number;
4
+ b: number;
5
+ };
package/dist/tema.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /** Tokens de marca. Tudo que é cor, fonte, nome e avatar vem daqui; o motor e o kit não conhecem nenhuma marca. */
2
+ export type Tema = {
3
+ fundo: string;
4
+ fundoCapa: string;
5
+ destaque: string;
6
+ texto: string;
7
+ suave: string;
8
+ ok: string;
9
+ erro: string;
10
+ info: string;
11
+ extra: string;
12
+ caixa: string;
13
+ caixaLongo?: string;
14
+ codigo: string;
15
+ fonte: string;
16
+ mono: string;
17
+ handle: string;
18
+ avatar: string;
19
+ };
20
+ export declare const TEMA_PADRAO: Tema;
21
+ export declare const TemaProvider: React.FC<{
22
+ tema: Tema;
23
+ children: React.ReactNode;
24
+ }>;
25
+ export declare const useTema: () => Tema;
26
+ /** Lê o `marca.json` de um projeto (campos que faltarem usam o tema padrão). */
27
+ export declare const temaDaMarca: (m: {
28
+ cores?: Partial<Tema>;
29
+ fontes?: {
30
+ titulo?: string;
31
+ mono?: string;
32
+ };
33
+ handle?: string;
34
+ avatar?: string;
35
+ }) => Tema;
36
+ /** "#f4b942" + 0.16 → "rgba(244,185,66,0.16)". */
37
+ export declare const rgba: (hex: string, a: number) => string;
package/dist/util.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { Palavra } from "./palavra";
2
+ export declare const FPS = 30;
3
+ export declare const clamp: {
4
+ readonly extrapolateLeft: "clamp";
5
+ readonly extrapolateRight: "clamp";
6
+ };
7
+ /** 0 → 1 a partir do instante `q` (s), com saída suave. */
8
+ export declare const surge: (t: number, q: number, d?: number) => number;
9
+ export declare const ramp: (t: number, a: number, b: number) => number;
10
+ export declare const pop: (t: number, q: number) => number;
11
+ export declare const fmt: (n: number) => string;
12
+ export declare const norm: (s: string) => string;
13
+ /**
14
+ * Âncoras por frase: `mkAt(palavras, inicios)(i)(frase, n)` devolve o instante LOCAL (s, desde o início da cena i) em que
15
+ * a frase começa na voz. Falha alto se a frase não existe (o Scribe às vezes escreve número em dígitos).
16
+ */
17
+ export declare const mkAt: (P: Palavra[], S: number[]) => (i: number) => (frase: string, nth?: number) => number;
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@praeko/studio-core",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Motor do Praeko Studio: episódio por voz e palavras com tempo, legendas palavra a palavra, golpes de edição, fechamento e tema da marca (peças Remotion). Biblioteca para projetos criados com `praeko studio init`.",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "praeko",
9
+ "video",
10
+ "shorts",
11
+ "remotion",
12
+ "captions",
13
+ "engine"
14
+ ],
15
+ "homepage": "https://praeko.app/docs",
16
+ "bugs": {
17
+ "url": "https://praeko.app/docs"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/Praeko/praeko-js.git",
22
+ "directory": "studio/packages/core"
23
+ },
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "CHANGELOG.md",
37
+ "LICENSE",
38
+ "LICENSES.md"
39
+ ],
40
+ "sideEffects": false,
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "node ../../../scripts/build-studio.mjs core",
46
+ "prepack": "npm run build",
47
+ "typecheck": "tsc --noEmit -p tsconfig.json"
48
+ },
49
+ "peerDependencies": {
50
+ "react": "^19.0.0",
51
+ "remotion": "^4.0.539"
52
+ },
53
+ "engines": {
54
+ "node": ">=22"
55
+ },
56
+ "devDependencies": {
57
+ "@types/react": "19.2.7",
58
+ "react": "19.2.3",
59
+ "react-dom": "19.2.3",
60
+ "remotion": "4.0.539"
61
+ }
62
+ }