@praeko/studio-core 0.1.0 → 0.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/CHANGELOG.md CHANGED
@@ -1,7 +1,16 @@
1
1
  # Changelog
2
2
 
3
- Segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e [SemVer](https://semver.org/lang/pt-BR/).
3
+ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
4
+
5
+ ## [0.2.0] - unreleased
6
+
7
+ Breaking: the whole public API is English. There are no deprecated aliases; move to the new names. Published types are generated by `tsc`, and their relative specifiers end in `.js`.
8
+
9
+ - Types and data: `Palavra` -> `Word`, `Tema` -> `Theme`, `TEMA_PADRAO` -> `DEFAULT_THEME`, `EpData` -> `EpisodeData` (`voz` -> `voice`, `palavras` -> `words`, `cenas` -> `scenes`, `fechamento` -> `outro`, `fundoLinhas` -> `backgroundLines`, `legendaPx` -> `captionPx`, `golpes` -> `hits`), `EpCena` -> `EpisodeScene` (`rotulo` -> `label`, `sons` -> `sounds`), `Som` -> `Sound` (`f` -> `sound`), `Golpe` -> `Hit` (`tipo` -> `type`, `tudo` -> `all`), `Grupo` -> `Group`, `LongoData` -> `LongData`, `CenaLongo` -> `LongScene`, `FechoLongo` -> `LongEndcard`.
10
+ - Functions and components: `Episodio` -> `Episode`, `Longo` -> `Long`, `Rotulo` -> `Label`, `Fundo` -> `Background`, `agrupar` -> `groupWords`, `TemaProvider` -> `ThemeProvider` (prop `tema` -> `theme`), `useTema` -> `useTheme`, `temaDaMarca` -> `themeFromBrand`, `surge` -> `appear`, `mkAt` -> `makeAt`, `mkAtG` -> `makeAtGlobal`, `janela` -> `fadeWindow`.
11
+ - Theme fields: `fundo` -> `background`, `fundoCapa` -> `coverBackground`, `destaque` -> `accent`, `texto` -> `text`, `suave` -> `muted`, `erro` -> `error`, `caixa` -> `box`, `caixaLongo` -> `longBox`, `codigo` -> `code`, `fonte` -> `font`. New `mascotPath` (default `mascots/{name}.png`). `themeFromBrand` reads the English `brand.json` shape (`colors`, `fonts.title`, `fonts.mono`).
12
+ - Error messages are English (`phrase not found: ...`).
4
13
 
5
14
  ## [0.1.0] - 2026-11-07
6
15
 
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.
16
+ First published version, extracted from the project where it was born. Publishes compiled `dist/` (ESM + `.d.ts`); `react` and `remotion` are peers.
package/LICENSES.md CHANGED
@@ -1,38 +1,36 @@
1
- # Licenças do Praeko Studio / Licenses
1
+ # Praeko Studio licenses
2
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.
3
+ Verified on 2026-11-07. This file is not legal advice; each vendor's own text prevails.
4
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.
5
+ ## Praeko Studio's code
6
+ The packages `@praeko/studio`, `@praeko/studio-core` and `@praeko/studio-kit` are **MIT** (see `LICENSE`, Copyright (c) 2026 Marcone Santos), like `@praeko/sdk` and `@praeko/cli`. The sound effects (`pop`, `ding`, `thump`, `whoosh`) and the `avatar.png` of the project template were created for Studio (sounds synthesized by `scripts/generate-sfx.mjs`, with no third-party samples) and follow the same MIT license. The package ships no fonts, music, stock images or third-party art.
7
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.
8
+ ## Notice you need to see (Remotion is NOT MIT)
9
+ > **Remotion.** Remotion is *source-available*, not open source. It is **free** for individuals, non-profits, evaluation and companies with **up to 3 employees** (commercial use included). Companies with **4 or more people** need the **Company License** (remotion.pro/license). You install and run Remotion, so the license is **yours**, not Praeko's. Remotion also sells plans for those who **automate renders by program or agent** (Creators, Automators); their FAQ does not expressly 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 (peer) that you install.
12
10
 
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`).
11
+ This notice appears in the `README`, at install time (`praeko studio init`), in `praeko studio doctor` and in `praeko studio licenses` (`--accept` records that you read it in `~/.config/praeko/studio/licenses.json`).
14
12
 
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.
13
+ **Engines.** Remotion is the default engine for videos **and** covers. With `--engine hyperframes` (or `"engine": "hyperframes"` in `brand.json`) the video **and** the cover are rendered by HyperFrames and nothing from Remotion is used, so a project that only uses HyperFrames does not need a Remotion license. Scenes or covers written as TSX (`component:`) only exist in Remotion.
16
14
 
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.
15
+ **We do not promise "free forever":** Remotion's conditions may change (their own page warns that the license changes in Remotion 5.0). Review the license when you upgrade.
18
16
 
19
- ## Dependências de terceiros
20
- | Componente | Onde | Licença | Fonte |
17
+ ## Third-party dependencies
18
+ | Component | Where | License | Source |
21
19
  |---|---|---|---|
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).
20
+ | `remotion`, `@remotion/cli` (4.0.539) | installed in the user's **project** (peer of core/kit) | Remotion License (see above), not MIT | remotion.pro/license; github.com/remotion-dev/remotion (LICENSE.md) |
21
+ | `react`, `react-dom` (19.2.3) | user's project (peer) | MIT | each package's package.json |
22
+ | `@praeko/studio-core`, `-kit` | user's project | MIT | LICENSE |
23
+ | `yaml` (2.9.1) | embedded in the `@praeko/studio` bundle | ISC | package.json |
24
+ | `@elevenlabs/elevenlabs-js` (2.x) | dependency of `@praeko/studio` | MIT | package.json |
25
+ | `hyperframes` (0.9.13, optional engine) | downloaded by `npx` the first time you use `--engine hyperframes` | Apache-2.0 | github.com/heygen-com/hyperframes |
26
+ | `typescript`, `esbuild`, `@types/*` | development/build only | Apache-2.0 / MIT | package.json |
27
+ | ffmpeg / ffprobe | you install | LGPL or GPL, depending on the build | ffmpeg.org/legal.html |
28
+ | Chrome Headless Shell / Chromium | Remotion downloads it on the first render | Chromium/Google licenses | remotion.dev/docs/miscellaneous/chrome-headless-shell |
29
+
30
+ ## Services and models (you sign up)
31
+ - **ElevenLabs**: its terms and your plan (voice cloning only with rights over the voice). The key is yours; Studio never prints it.
32
+ - **Local VoiceStudio** (optional): the OmniVoice model is **CC-BY-NC (non-commercial use)**. It is not the default in any Studio configuration and **must not be used in monetized content**. Check the license of any local model you use.
33
+ - **YouTube, Instagram, TikTok**: each network's terms and synthetic-content rules (see `praeko studio docs publish`).
34
+
35
+ ## Fonts and media
36
+ No font is distributed. The project template uses system fonts (`Arial Black`, `Menlo`) that `doctor` checks; swap them in `brand.json` for open fonts (OFL) if you want the video to look the same on every system. Music, images and clips you add are your responsibility (rights and licenses).
package/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  # @praeko/studio-core
2
2
 
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).
3
+ The Praeko Studio engine (Remotion parts): voice-driven episodes with timed words, word-by-word captions, editing hits, outro and brand theme. A library for projects created with [`@praeko/studio`](https://www.npmjs.com/package/@praeko/studio) (`praeko studio init` installs and configures it).
4
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.
5
+ > **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 or more people need the Company License ([remotion.pro/license](https://www.remotion.pro/license)). Details in [LICENSES.md](./LICENSES.md).
8
6
 
9
- ## Instalar
7
+ ## Install
10
8
  ```sh
11
9
  # npm
12
10
  npm install @praeko/studio-core remotion react
@@ -17,4 +15,4 @@ pnpm add @praeko/studio-core remotion react
17
15
  # bun
18
16
  bun add @praeko/studio-core remotion react
19
17
  ```
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`.
18
+ Peers: `remotion` ^4.0.539 (all `@remotion/*` packages on the same version) and `react` ^19. Node 22 or newer. Usage: `praeko studio docs episode-yaml`.
@@ -0,0 +1,56 @@
1
+ import type { Word } from "./word.js";
2
+ /**
3
+ * PRAEKO STUDIO ENGINE.
4
+ * An episode is just data (EpisodeData): voice, timed words, scenes (card + label + sounds) and outro.
5
+ * The rest (background, captions, progress bar, outro, effects) is shared and uses the brand theme.
6
+ */
7
+ export type Sound = {
8
+ at: number;
9
+ sound: "pop" | "ding" | "thump" | "whoosh";
10
+ v?: number;
11
+ };
12
+ export type Hit = {
13
+ at: number;
14
+ type: "shake" | "zoom" | "flash" | "all";
15
+ };
16
+ export type EpisodeScene = {
17
+ a: number;
18
+ b: number;
19
+ C: React.FC<{
20
+ dur: number;
21
+ }>;
22
+ label?: string;
23
+ /** sounds relative to the start of the scene (s) */
24
+ sounds?: Sound[];
25
+ };
26
+ export type EpisodeData = {
27
+ id: string;
28
+ durS: number;
29
+ voice: string;
30
+ words: Word[];
31
+ scenes: EpisodeScene[];
32
+ outro: {
33
+ a: number;
34
+ cta: string;
35
+ };
36
+ /** blurred "terminal" lines in the background */
37
+ backgroundLines?: string[];
38
+ /** caption size (default 90) */
39
+ captionPx?: number;
40
+ /** editing hits: quick shake, flash and zoom at instants (s) of the narration */
41
+ hits?: Hit[];
42
+ };
43
+ export declare const Label: React.FC<{
44
+ text: string;
45
+ }>;
46
+ /** Clean background: dark, a pulsing amber glow and dots drifting up slowly. */
47
+ export declare const Background: React.FC;
48
+ export type Group = {
49
+ words: Word[];
50
+ a: number;
51
+ b: number;
52
+ };
53
+ export declare const groupWords: (ps: Word[]) => Group[];
54
+ export declare const Episode: React.FC<{
55
+ d: EpisodeData;
56
+ }>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- export * from "./motor";
2
- export * from "./palavra";
3
- export * from "./tema";
4
- export * from "./util";
5
- export * from "./longo";
1
+ export * from "./engine.js";
2
+ export * from "./word.js";
3
+ export * from "./theme.js";
4
+ export * from "./theme-context.js";
5
+ export * from "./util.js";
6
+ export * from "./long.js";
package/dist/index.js CHANGED
@@ -1,56 +1,61 @@
1
- // src/motor.tsx
1
+ // src/engine.tsx
2
2
  import { AbsoluteFill, Audio, Img, Sequence, interpolate as interpolate2, spring, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
3
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",
4
+ // src/theme.ts
5
+ var BASE = {
6
+ background: "#14110f",
7
+ coverBackground: "#0e0c14",
8
+ accent: "#f4b942",
9
+ text: "#f3ece0",
10
+ muted: "#a89c8a",
13
11
  ok: "#7ee787",
14
- erro: "#ff6b5e",
12
+ error: "#ff6b5e",
15
13
  info: "#7fd7ff",
16
14
  extra: "#a98bff",
17
- caixa: "#2a2118",
18
- codigo: "#1b1610",
19
- fonte: "'Arial Black', 'Helvetica Neue', system-ui, sans-serif",
15
+ box: "#2a2118",
16
+ code: "#1b1610",
17
+ font: "'Arial Black', 'Helvetica Neue', system-ui, sans-serif",
20
18
  mono: "Menlo, 'Courier New', monospace",
21
- handle: "@seucanal",
22
- avatar: "capa/avatar.png"
19
+ handle: "@yourchannel",
20
+ avatar: "cover/avatar.png",
21
+ mascotPath: "mascots/{name}.png"
23
22
  };
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 } : {},
23
+ var DEFAULT_THEME = BASE;
24
+ var themeFromBrand = (m) => ({
25
+ ...DEFAULT_THEME,
26
+ ...m.colors ?? {},
27
+ ...m.fonts?.title ? { font: m.fonts.title } : {},
28
+ ...m.fonts?.mono ? { mono: m.fonts.mono } : {},
32
29
  ...m.handle ? { handle: m.handle } : {},
33
- ...m.avatar ? { avatar: m.avatar } : {}
30
+ ...m.avatar ? { avatar: m.avatar } : {},
31
+ ...m.mascotPath ? { mascotPath: m.mascotPath } : {}
34
32
  });
35
33
  var rgba = (hex, a) => {
36
34
  const n = parseInt(hex.slice(1), 16);
37
35
  return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
38
36
  };
39
37
 
38
+ // src/theme-context.tsx
39
+ import { createContext, useContext } from "react";
40
+ import { jsx } from "react/jsx-runtime";
41
+ var Ctx = createContext(DEFAULT_THEME);
42
+ var ThemeProvider = ({ theme, children }) => /* @__PURE__ */ jsx(Ctx.Provider, { value: theme, children });
43
+ var useTheme = () => useContext(Ctx);
44
+
40
45
  // src/util.ts
41
46
  import { Easing, interpolate } from "remotion";
42
47
  var FPS = 30;
43
48
  var clamp = { extrapolateLeft: "clamp", extrapolateRight: "clamp" };
44
49
  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 });
50
+ var appear = (t, q, d = 0.35) => interpolate(t, [q, q + d], [0, 1], { ...clamp, easing: EO });
46
51
  var ramp = (t, a, b) => interpolate(t, [a, b], [0, 1], { ...clamp, easing: Easing.out(Easing.cubic) });
47
52
  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
53
  var fmt = (n) => String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
49
54
  var norm = (s) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]/g, "");
50
- var mkAt = (P, S) => {
55
+ var makeAt = (P, S) => {
51
56
  const W = P.map((p) => norm(p.t));
52
- return (i) => (frase, nth = 0) => {
53
- const f2 = frase.split(/\s+/).map(norm).filter(Boolean);
57
+ return (i) => (phrase, nth = 0) => {
58
+ const f2 = phrase.split(/\s+/).map(norm).filter(Boolean);
54
59
  let c = 0;
55
60
  for (let k = 0; k <= W.length - f2.length; k++) {
56
61
  if (f2.every((w, j) => W[k + j] === w)) {
@@ -58,17 +63,17 @@ var mkAt = (P, S) => {
58
63
  c++;
59
64
  }
60
65
  }
61
- throw new Error("frase n\xE3o achada: " + frase);
66
+ throw new Error("phrase not found: " + phrase);
62
67
  };
63
68
  };
64
69
 
65
- // src/motor.tsx
70
+ // src/engine.tsx
66
71
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
67
72
  var f = (s) => Math.round(s * FPS);
68
73
  var DUR_SOM = { pop: 0.16, ding: 0.9, thump: 0.4, whoosh: 0.55 };
69
74
  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();
75
+ var Label = ({ text }) => {
76
+ const { accent: LAMP, background: BG, text: INK, font: FONT, ok: OK, box: BOX } = useTheme();
72
77
  const frame = useCurrentFrame();
73
78
  const { fps } = useVideoConfig();
74
79
  const pop2 = spring({ frame: frame - 3, fps, config: { damping: 11, stiffness: 200 } });
@@ -100,21 +105,21 @@ var Rotulo = ({ texto }) => {
100
105
  boxShadow: "0 10px 30px rgba(0,0,0,.55)",
101
106
  whiteSpace: "nowrap"
102
107
  },
103
- children: texto
108
+ children: text
104
109
  }
105
110
  )
106
111
  }
107
112
  );
108
113
  };
109
- var Fundo = () => {
110
- const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
114
+ var Background = () => {
115
+ const { accent: LAMP, background: BG, text: INK, font: FONT, ok: OK, box: BOX } = useTheme();
111
116
  const frame = useCurrentFrame();
112
- const pulso = 0.16 + 0.05 * Math.sin(frame / 22);
117
+ const pulse = 0.16 + 0.05 * Math.sin(frame / 22);
113
118
  return /* @__PURE__ */ jsx2(
114
119
  AbsoluteFill,
115
120
  {
116
121
  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}`
122
+ background: `radial-gradient(ellipse at 50% 20%, ${rgba(LAMP, pulse)} 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
123
  },
119
124
  children: Array.from({ length: 26 }).map((_, i) => {
120
125
  const x = i * 137 % 1080;
@@ -141,32 +146,32 @@ var Fundo = () => {
141
146
  }
142
147
  );
143
148
  };
144
- var agrupar = (ps) => {
149
+ var groupWords = (ps) => {
145
150
  const g = [];
146
151
  let cur = [];
147
- const fecha = () => {
148
- if (cur.length) g.push({ palavras: cur, a: cur[0].a, b: cur[cur.length - 1].b });
152
+ const close = () => {
153
+ if (cur.length) g.push({ words: cur, a: cur[0].a, b: cur[cur.length - 1].b });
149
154
  cur = [];
150
155
  };
151
156
  ps.forEach((p, i) => {
152
157
  cur.push(p);
153
158
  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();
159
+ const letters = cur.reduce((n, x) => n + x.t.length, 0);
160
+ const next = ps[i + 1]?.t.length ?? 0;
161
+ if (/[.,?!:]$/.test(p.t) || cur.length >= 3 || letters + next > 15 || gap > 0.3) close();
157
162
  });
158
- fecha();
163
+ close();
159
164
  return g;
160
165
  };
161
- var FundoTerminal = ({ linhas }) => {
162
- const { destaque: LAMP, ok: OK } = useTema();
166
+ var TerminalBackground = ({ lines }) => {
167
+ const { accent: LAMP, ok: OK } = useTheme();
163
168
  const frame = useCurrentFrame();
164
- const n = Math.max(1, linhas.length);
165
- const passo = 64, ciclo = 34 * passo + 0;
169
+ const n = Math.max(1, lines.length);
170
+ const step = 64, cycle = 34 * step + 0;
166
171
  return /* @__PURE__ */ jsxs(AbsoluteFill, { style: { overflow: "hidden" }, children: [
167
- /* @__PURE__ */ jsx2(Fundo, {}),
172
+ /* @__PURE__ */ jsx2(Background, {}),
168
173
  /* @__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;
174
+ const y = ((i * step - frame * 0.8) % cycle + cycle) % cycle - 160;
170
175
  const col = (i + Math.floor(i / n)) % 2;
171
176
  return /* @__PURE__ */ jsx2(
172
177
  "div",
@@ -180,26 +185,26 @@ var FundoTerminal = ({ linhas }) => {
180
185
  color: i % 4 === 0 ? LAMP : OK,
181
186
  whiteSpace: "nowrap"
182
187
  },
183
- children: linhas[i % n]
188
+ children: lines[i % n]
184
189
  },
185
190
  i
186
191
  );
187
192
  }) })
188
193
  ] });
189
194
  };
190
- var LegendaEp = ({ palavras, bottom, px = 90 }) => {
191
- const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
195
+ var CaptionEp = ({ words, bottom, px = 90 }) => {
196
+ const { accent: LAMP, background: BG, text: INK, font: FONT, ok: OK, box: BOX } = useTheme();
192
197
  const frame = useCurrentFrame();
193
198
  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));
199
+ const groups = groupWords(words);
200
+ const idx = groups.findIndex((g2, i) => {
201
+ const next = groups[i + 1]?.a ?? g2.b + 0.4;
202
+ return t >= g2.a - 0.05 && t < Math.max(g2.b + 0.15, Math.min(next, g2.b + 0.5));
198
203
  });
199
204
  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" });
205
+ const g = groups[idx];
206
+ const inside = frame - f(g.a);
207
+ const enter = interpolate2(inside, [-2, 4], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
203
208
  return /* @__PURE__ */ jsx2(
204
209
  "div",
205
210
  {
@@ -212,12 +217,12 @@ var LegendaEp = ({ palavras, bottom, px = 90 }) => {
212
217
  justifyContent: "center",
213
218
  gap: px < 90 ? 64 : 44,
214
219
  flexWrap: "wrap",
215
- opacity: entra,
216
- transform: `translateY(${(1 - entra) * 24}px)`
220
+ opacity: enter,
221
+ transform: `translateY(${(1 - enter) * 24}px)`
217
222
  },
218
- children: g.palavras.map((p, i) => {
219
- const ativa = t >= p.a && t < p.b + 0.05;
220
- const ja = t >= p.a;
223
+ children: g.words.map((p, i) => {
224
+ const active = t >= p.a && t < p.b + 0.05;
225
+ const spoken = t >= p.a;
221
226
  return /* @__PURE__ */ jsx2(
222
227
  "span",
223
228
  {
@@ -227,8 +232,8 @@ var LegendaEp = ({ palavras, bottom, px = 90 }) => {
227
232
  fontSize: px,
228
233
  lineHeight: 1.05,
229
234
  textTransform: "uppercase",
230
- color: ativa ? LAMP : ja ? INK : rgba(INK, 0.55),
231
- transform: `scale(${ativa ? 1.12 : 1})`,
235
+ color: active ? LAMP : spoken ? INK : rgba(INK, 0.55),
236
+ transform: `scale(${active ? 1.12 : 1})`,
232
237
  textShadow: "0 6px 0 #000, 0 0 24px rgba(0,0,0,.85), 4px 0 0 #000, -4px 0 0 #000"
233
238
  },
234
239
  children: p.t
@@ -239,19 +244,19 @@ var LegendaEp = ({ palavras, bottom, px = 90 }) => {
239
244
  }
240
245
  );
241
246
  };
242
- var FechamentoEp = ({ cta }) => {
243
- const tema = useTema();
244
- const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
247
+ var OutroEp = ({ cta }) => {
248
+ const theme = useTheme();
249
+ const { accent: LAMP, background: BG, text: INK, font: FONT, ok: OK, box: BOX } = useTheme();
245
250
  const frame = useCurrentFrame();
246
251
  const { fps } = useVideoConfig();
247
252
  const logo = spring({ frame, fps, config: { damping: 10, stiffness: 150 } });
248
- const nome = spring({ frame: frame - 8, fps, config: { damping: 200 } });
253
+ const handleSpring = spring({ frame: frame - 8, fps, config: { damping: 200 } });
249
254
  const pill = spring({ frame: frame - 20, fps, config: { damping: 12, stiffness: 160 } });
250
255
  return /* @__PURE__ */ jsxs(
251
256
  AbsoluteFill,
252
257
  {
253
258
  style: {
254
- background: `radial-gradient(circle at 50% 40%, ${CAIXA} 0%, ${BG} 72%)`,
259
+ background: `radial-gradient(circle at 50% 40%, ${BOX} 0%, ${BG} 72%)`,
255
260
  justifyContent: "center",
256
261
  alignItems: "center",
257
262
  fontFamily: FONT,
@@ -271,7 +276,7 @@ var FechamentoEp = ({ cta }) => {
271
276
  transform: `scale(${0.6 + 0.4 * logo})`,
272
277
  opacity: Math.min(1, logo * 1.5)
273
278
  },
274
- children: /* @__PURE__ */ jsx2(Img, { src: staticFile(tema.avatar), style: { width: 420, height: 420, imageRendering: "pixelated" } })
279
+ children: /* @__PURE__ */ jsx2(Img, { src: staticFile(theme.avatar), style: { width: 420, height: 420, imageRendering: "pixelated" } })
275
280
  }
276
281
  ),
277
282
  /* @__PURE__ */ jsx2(
@@ -282,10 +287,10 @@ var FechamentoEp = ({ cta }) => {
282
287
  fontSize: 92,
283
288
  fontWeight: 900,
284
289
  color: LAMP,
285
- opacity: Math.max(0, nome),
286
- transform: `translateY(${(1 - Math.max(0, nome)) * 30}px)`
290
+ opacity: Math.max(0, handleSpring),
291
+ transform: `translateY(${(1 - Math.max(0, handleSpring)) * 30}px)`
287
292
  },
288
- children: tema.handle
293
+ children: theme.handle
289
294
  }
290
295
  ),
291
296
  /* @__PURE__ */ jsx2(
@@ -309,63 +314,63 @@ var FechamentoEp = ({ cta }) => {
309
314
  }
310
315
  );
311
316
  };
312
- var Barra = ({ durS }) => {
313
- const { destaque: LAMP, fundo: BG, texto: INK, fonte: FONT, ok: OK, caixa: CAIXA } = useTema();
317
+ var ProgressBar = ({ durS }) => {
318
+ const { accent: LAMP, background: BG, text: INK, font: FONT, ok: OK, box: BOX } = useTheme();
314
319
  const frame = useCurrentFrame();
315
320
  const w = Math.min(100, frame / f(durS) * 100);
316
321
  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
322
  };
318
- var Golpes = ({ golpes, children }) => {
323
+ var Hits = ({ hits, children }) => {
319
324
  const t = useCurrentFrame() / FPS;
320
325
  let sx = 0, sy = 0, zoom = 1, flash = 0;
321
- for (const g of golpes) {
326
+ for (const g of hits) {
322
327
  const dt = t - g.at;
323
328
  if (dt < 0 || dt > 0.45) continue;
324
329
  const k = (1 - dt / 0.45) ** 2;
325
- if (g.tipo === "shake" || g.tipo === "tudo") {
330
+ if (g.type === "shake" || g.type === "all") {
326
331
  sx += Math.sin(dt * 95) * 16 * k;
327
332
  sy += Math.cos(dt * 120) * 9 * k;
328
333
  }
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);
334
+ if (g.type === "zoom" || g.type === "all") zoom = Math.max(zoom, 1 + 0.055 * k);
335
+ if (g.type === "flash" || g.type === "all") flash = Math.max(flash, 0.33 * k);
331
336
  }
332
337
  return /* @__PURE__ */ jsxs(Fragment, { children: [
333
338
  /* @__PURE__ */ jsx2(AbsoluteFill, { style: { transform: `translate(${sx}px, ${sy}px) scale(${zoom})` }, children }),
334
339
  flash > 0.01 ? /* @__PURE__ */ jsx2(AbsoluteFill, { style: { background: `rgba(255,244,214,${flash})`, pointerEvents: "none" } }) : null
335
340
  ] });
336
341
  };
337
- var Episodio = ({ d }) => {
338
- const { fundo: BG } = useTema();
342
+ var Episode = ({ d }) => {
343
+ const { background: BG } = useTheme();
339
344
  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"] }),
345
+ /* @__PURE__ */ jsx2(Audio, { src: staticFile(d.voice) }),
346
+ /* @__PURE__ */ jsxs(Hits, { hits: d.hits ?? [], children: [
347
+ /* @__PURE__ */ jsx2(TerminalBackground, { lines: d.backgroundLines ?? ["\u2713 ok"] }),
343
348
  /* @__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}`));
349
+ d.scenes.flatMap((c, i) => {
350
+ const sounds = [{ at: 0, sound: "pop" }, ...c.sounds ?? []];
351
+ return sounds.map((s, k) => /* @__PURE__ */ jsx2(Sequence, { from: f(c.a + s.at), durationInFrames: f(DUR_SOM[s.sound]) + 2, children: /* @__PURE__ */ jsx2(Audio, { src: staticFile(`sfx/${s.sound}.wav`), volume: s.v ?? VOL_SOM[s.sound] }) }, `s${i}-${k}`));
347
352
  }),
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
353
+ d.scenes.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)),
354
+ d.scenes.map(
355
+ (c, i) => c.label ? /* @__PURE__ */ jsx2(Sequence, { from: f(c.a), durationInFrames: f(c.b - c.a), children: /* @__PURE__ */ jsx2(Label, { text: c.label }) }, "r" + i) : null
351
356
  )
352
357
  ] }),
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 })
358
+ /* @__PURE__ */ jsx2(Sequence, { from: f(d.outro.a), durationInFrames: f(d.durS - d.outro.a), children: /* @__PURE__ */ jsx2(OutroEp, { cta: d.outro.cta }) }),
359
+ /* @__PURE__ */ jsx2(Sequence, { from: 0, durationInFrames: f(d.durS), children: /* @__PURE__ */ jsx2(CaptionEp, { words: d.words, bottom: 190, px: d.captionPx }) }),
360
+ /* @__PURE__ */ jsx2(Sequence, { from: f(d.outro.a), durationInFrames: 2, children: /* @__PURE__ */ jsx2(Audio, { src: staticFile("sfx/ding.wav"), volume: 0.22 }) }),
361
+ /* @__PURE__ */ jsx2(ProgressBar, { durS: d.durS })
357
362
  ] });
358
363
  };
359
364
 
360
- // src/longo.tsx
365
+ // src/long.tsx
361
366
  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
367
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
363
368
  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) => {
369
+ var fadeWindow = (t, a, b, d = 0.4) => interpolate3(t, [a, a + d, b - d, b], [0, 1, 1, 0], clamp2);
370
+ var makeAtGlobal = (P) => {
366
371
  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);
372
+ return (phrase, nth = 0, from = 0) => {
373
+ const f2 = phrase.split(/\s+/).map(norm).filter(Boolean);
369
374
  let c = 0;
370
375
  for (let i = 0; i <= W.length - f2.length; i++) {
371
376
  if (P[i].a < from) continue;
@@ -374,21 +379,21 @@ var mkAtG = (P) => {
374
379
  c++;
375
380
  }
376
381
  }
377
- throw new Error("frase n\xE3o achada: " + frase);
382
+ throw new Error("phrase not found: " + phrase);
378
383
  };
379
384
  };
380
- var Cena = ({ t, a, b, n, titulo, children }) => {
381
- const T = useTema();
385
+ var SceneFrame = ({ t, a, b, n, title, children }) => {
386
+ const T = useTheme();
382
387
  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
388
+ return /* @__PURE__ */ jsxs2(AbsoluteFill2, { style: { opacity: fadeWindow(t, a, b) }, children: [
389
+ /* @__PURE__ */ jsxs2("div", { style: { position: "absolute", left: 100, top: 46, display: "flex", alignItems: "center", gap: 18, fontFamily: T.font, fontSize: 30, letterSpacing: 4, color: T.accent, whiteSpace: "nowrap" }, children: [
390
+ /* @__PURE__ */ jsx3("span", { style: { background: T.accent, color: T.background, borderRadius: 12, padding: "4px 16px" }, children: n }),
391
+ title
387
392
  ] }),
388
393
  children
389
394
  ] });
390
395
  };
391
- var grupos = (P) => {
396
+ var splitGroups = (P) => {
392
397
  const g = [];
393
398
  let cur = [];
394
399
  P.forEach((p) => {
@@ -401,60 +406,60 @@ var grupos = (P) => {
401
406
  if (cur.length) g.push(cur);
402
407
  return g;
403
408
  };
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: [
409
+ var Captions = ({ t, G }) => {
410
+ const T = useTheme();
411
+ let group = null;
412
+ for (const g of G) if (t >= g[0].a - 0.05 && t <= g[g.length - 1].b + 0.4) group = g;
413
+ if (!group) return null;
414
+ return /* @__PURE__ */ jsx3("div", { style: { position: "absolute", left: 160, right: 160, bottom: 56, textAlign: "center", fontFamily: T.font, fontSize: 46, lineHeight: 1.2, textShadow: "0 3px 12px rgba(0,0,0,.9)" }, children: group.map((p, i) => /* @__PURE__ */ jsxs2("span", { style: { color: t >= p.a && t < p.b + 0.05 ? T.accent : T.text }, children: [
410
415
  p.t,
411
- i < grupo.length - 1 ? " " : ""
416
+ i < group.length - 1 ? " " : ""
412
417
  ] }, i)) });
413
418
  };
414
- var FechoTela = ({ f: f2, t }) => {
415
- const T = useTema();
419
+ var EndcardScreen = ({ f: f2, t }) => {
420
+ const T = useTheme();
416
421
  const { fps } = useVideoConfig2();
417
- const k = (atraso) => spring2({ frame: (t - f2.a - atraso) * fps, fps, config: { damping: 14, stiffness: 140 } });
422
+ const k = (delay) => spring2({ frame: (t - f2.a - delay) * fps, fps, config: { damping: 14, stiffness: 140 } });
418
423
  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
+ return /* @__PURE__ */ jsxs2(AbsoluteFill2, { style: { opacity: op, background: `radial-gradient(circle at 50% 40%, ${T.box} 0%, ${T.background} 75%)`, alignItems: "center", justifyContent: "center", fontFamily: T.font, textAlign: "center", paddingBottom: 90 }, children: [
425
+ /* @__PURE__ */ jsx3("div", { style: { width: 230, height: 230, borderRadius: 44, overflow: "hidden", border: `8px solid ${f2.image ? "#3a3566" : T.accent}`, background: f2.image ? "#1c1a33" : void 0, boxShadow: `0 0 70px ${rgba(T.accent, 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.image ?? T.avatar), style: { width: 230, height: 230, imageRendering: "pixelated", objectFit: "contain" } }) }),
426
+ f2.text ? /* @__PURE__ */ jsx3("div", { style: { marginTop: 44, fontSize: 104, fontWeight: 900, color: T.accent, opacity: Math.max(0, k(0.15)), transform: `translateY(${(1 - Math.max(0, k(0.15))) * 24}px)` }, children: f2.text }) : null,
427
+ /* @__PURE__ */ jsx3("div", { style: { marginTop: 28, fontSize: 54, fontWeight: 900, color: T.text, opacity: Math.max(0, k(0.3)) }, children: T.handle }),
428
+ f2.link ? /* @__PURE__ */ jsx3("div", { style: { marginTop: 18, fontSize: 38, fontFamily: T.mono, color: T.muted, opacity: Math.max(0, k(0.45)) }, children: f2.link }) : null
424
429
  ] });
425
430
  };
426
- var Longo = ({ d }) => {
427
- const T = useTema();
431
+ var Long = ({ d }) => {
432
+ const T = useTheme();
428
433
  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 } })
434
+ const G = splitGroups(d.words);
435
+ return /* @__PURE__ */ jsxs2(AbsoluteFill2, { style: { background: T.background, color: T.text, fontFamily: T.font }, children: [
436
+ /* @__PURE__ */ jsx3(AbsoluteFill2, { style: { background: `radial-gradient(1200px 700px at 70% 20%, ${rgba(T.accent, 0.07)}, transparent 70%)` } }),
437
+ /* @__PURE__ */ jsx3(Audio2, { src: staticFile2(d.voice) }),
438
+ d.scenes.map((c) => /* @__PURE__ */ jsx3(SceneFrame, { t, a: c.a, b: c.b, n: c.n, title: c.title, children: /* @__PURE__ */ jsx3(c.C, { t }) }, c.n)),
439
+ d.endcard && t >= d.endcard.a ? /* @__PURE__ */ jsx3(EndcardScreen, { f: d.endcard, t }) : null,
440
+ /* @__PURE__ */ jsx3(Captions, { t, G }),
441
+ /* @__PURE__ */ jsx3("div", { style: { position: "absolute", left: 0, top: 0, height: 8, width: `${t / d.durS * 100}%`, background: T.accent } })
437
442
  ] });
438
443
  };
439
444
  export {
440
- Episodio,
445
+ Background,
446
+ DEFAULT_THEME,
447
+ Episode,
441
448
  FPS,
442
- Fundo,
443
- Longo,
444
- Rotulo,
445
- TEMA_PADRAO,
446
- TemaProvider,
447
- agrupar,
449
+ Label,
450
+ Long,
451
+ ThemeProvider,
452
+ appear,
448
453
  clamp,
454
+ fadeWindow,
449
455
  fmt,
450
- janela,
451
- mkAt,
452
- mkAtG,
456
+ groupWords,
457
+ makeAt,
458
+ makeAtGlobal,
453
459
  norm,
454
460
  pop,
455
461
  ramp,
456
462
  rgba,
457
- surge,
458
- temaDaMarca,
459
- useTema
463
+ themeFromBrand,
464
+ useTheme
460
465
  };
package/dist/long.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { Word } from "./word.js";
2
+ /**
3
+ * LONG ENGINE (16:9, several minutes). A long video is a list of scenes in GLOBAL time: each scene receives `t` (s since the start of the video)
4
+ * and ties itself to the voice with `makeAtGlobal(WORDS)(phrase, nth, from)`. The engine handles the background, the voice, the scene header (number + title),
5
+ * grouped captions and the progress bar. Scenes are project TSX, built with the parts of `@praeko/studio-kit`.
6
+ */
7
+ export type LongScene = {
8
+ n: string;
9
+ title: string;
10
+ a: number;
11
+ b: number;
12
+ C: React.FC<{
13
+ t: number;
14
+ }>;
15
+ };
16
+ /** Optional end card: avatar, text (e.g. the signature), brand handle and link. Starts at `a` (s) and stays until the end. */
17
+ export type LongEndcard = {
18
+ a: number;
19
+ text?: string;
20
+ link?: string;
21
+ image?: string;
22
+ };
23
+ export type LongData = {
24
+ id: string;
25
+ durS: number;
26
+ voice: string;
27
+ words: Word[];
28
+ scenes: LongScene[];
29
+ width?: number;
30
+ height?: number;
31
+ endcard?: LongEndcard;
32
+ };
33
+ /** Fades in and out over `d` s at the ends of the window [a, b]. */
34
+ export declare const fadeWindow: (t: number, a: number, b: number, d?: number) => number;
35
+ /** Global anchor: `at("phrase", nth, from)` = instant (s) at which the phrase starts; `from` ignores occurrences before `from` s. Fails loudly if not found. */
36
+ export declare const makeAtGlobal: (P: Word[]) => (phrase: string, nth?: number, from?: number) => number;
37
+ export declare const Long: React.FC<{
38
+ d: LongData;
39
+ }>;
@@ -0,0 +1,6 @@
1
+ import type { Theme } from "./theme.js";
2
+ export declare const ThemeProvider: React.FC<{
3
+ theme: Theme;
4
+ children: React.ReactNode;
5
+ }>;
6
+ export declare const useTheme: () => Theme;
@@ -0,0 +1,37 @@
1
+ /** Brand tokens. Everything that is a color, font, name or avatar comes from here; the engine and the kit know no brand. */
2
+ export type Theme = {
3
+ background: string;
4
+ coverBackground: string;
5
+ accent: string;
6
+ text: string;
7
+ muted: string;
8
+ ok: string;
9
+ error: string;
10
+ info: string;
11
+ extra: string;
12
+ box: string;
13
+ longBox?: string;
14
+ code: string;
15
+ font: string;
16
+ mono: string;
17
+ handle: string;
18
+ avatar: string;
19
+ /** Mascot image path template under public/; `{name}` is replaced. Default `mascots/{name}.png`. */
20
+ mascotPath?: string;
21
+ };
22
+ export declare const DEFAULT_THEME: Theme;
23
+ type BrandLike = {
24
+ colors?: Partial<Theme>;
25
+ fonts?: {
26
+ title?: string;
27
+ mono?: string;
28
+ };
29
+ handle?: string;
30
+ avatar?: string;
31
+ mascotPath?: string;
32
+ };
33
+ /** Reads a project's `brand.json` (missing fields use the default theme). */
34
+ export declare const themeFromBrand: (m: BrandLike) => Theme;
35
+ /** "#f4b942" + 0.16 -> "rgba(244,185,66,0.16)". */
36
+ export declare const rgba: (hex: string, a: number) => string;
37
+ export {};
package/dist/util.d.ts CHANGED
@@ -1,17 +1,18 @@
1
- import { Palavra } from "./palavra";
1
+ import type { Word } from "./word.js";
2
2
  export declare const FPS = 30;
3
3
  export declare const clamp: {
4
4
  readonly extrapolateLeft: "clamp";
5
5
  readonly extrapolateRight: "clamp";
6
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;
7
+ /** 0 -> 1 from instant `q` (s), with a soft ease-out. */
8
+ export declare const appear: (t: number, q: number, d?: number) => number;
9
9
  export declare const ramp: (t: number, a: number, b: number) => number;
10
10
  export declare const pop: (t: number, q: number) => number;
11
+ /** Number with "." as thousands separator (pt-BR style): 13500 -> "13.500". */
11
12
  export declare const fmt: (n: number) => string;
12
13
  export declare const norm: (s: string) => string;
13
14
  /**
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).
15
+ * Phrase anchors: `makeAt(words, starts)(i)(phrase, n)` returns the LOCAL instant (s since the start of scene i) at which the phrase
16
+ * starts in the voice. Fails loudly if the phrase does not exist (the aligner sometimes writes numbers as digits).
16
17
  */
17
- export declare const mkAt: (P: Palavra[], S: number[]) => (i: number) => (frase: string, nth?: number) => number;
18
+ export declare const makeAt: (P: Word[], S: number[]) => (i: number) => (phrase: string, nth?: number) => number;
package/dist/word.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** A spoken word with its timing: `t` text, `a` start (s), `b` end (s). */
2
+ export type Word = {
3
+ t: string;
4
+ a: number;
5
+ b: number;
6
+ };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@praeko/studio-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
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`.",
5
+ "description": "The Praeko Studio engine: voice-driven episodes with timed words, word-by-word captions, editing hits, outro and brand theme (Remotion parts). A library for projects created with `praeko studio init`.",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "praeko",
@@ -44,7 +44,8 @@
44
44
  "scripts": {
45
45
  "build": "node ../../../scripts/build-studio.mjs core",
46
46
  "prepack": "npm run build",
47
- "typecheck": "tsc --noEmit -p tsconfig.json"
47
+ "typecheck": "tsc --noEmit -p tsconfig.json",
48
+ "test": "node --test test/*.test.ts"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "react": "^19.0.0",
package/dist/longo.d.ts DELETED
@@ -1,39 +0,0 @@
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
- }>;
package/dist/motor.d.ts DELETED
@@ -1,56 +0,0 @@
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
- }>;
package/dist/palavra.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export type Palavra = {
2
- t: string;
3
- a: number;
4
- b: number;
5
- };
package/dist/tema.d.ts DELETED
@@ -1,37 +0,0 @@
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;