diff --git a/.github/workflows/dod.yml b/.github/workflows/dod.yml
index 5c1dd99..5e1c304 100644
--- a/.github/workflows/dod.yml
+++ b/.github/workflows/dod.yml
@@ -1,6 +1,14 @@
-# DoD gate — valida Definition of Done em todo PR antes do merge
-# Verifica: coverage >= 80%, evidência Playwright presente, conventional commit,
-# referência a task no body, ADR linkado se houver mudanças em architecture/
+# DoD Gate — validates Definition of Done on every PR before merge.
+#
+# This repo is a Python CLI (cli/cli.py + pyproject.toml) plus an optional
+# Remotion subproject (remotion-tutorial/). Heavy lifting (ruff/mypy/py_compile,
+# install.sh smoke, shellcheck, markdown links) is already handled by ci.yml
+# and lint.yml — this gate stays meta and only enforces what those don't:
+#
+# 1. PR title follows Conventional Commits.
+# 2. PR body is non-empty.
+# 3. Changes under .specs/architecture/ link to an ADR.
+# 4. When remotion-tutorial/ changes, the TypeScript composition typechecks.
name: DoD Gate
@@ -23,82 +31,37 @@ jobs:
dod-check:
name: Definition of Done
runs-on: ubuntu-latest
- timeout-minutes: 15
+ timeout-minutes: 8
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: npm
-
- - name: Install dependencies
- run: npm ci
-
- # Roda testes com coverage para gerar relatório
- - name: Run unit tests with coverage
- run: npm test -- --coverage
-
- # Bloqueia se coverage < 80%
- - name: Check coverage threshold (>=80%)
- run: |
- set -euo pipefail
- if [ ! -f coverage/coverage-summary.json ]; then
- echo "::error::coverage/coverage-summary.json não encontrado. Configure o reporter json-summary no test runner."
- exit 1
- fi
- PCT=$(node -e "const c=require('./coverage/coverage-summary.json');console.log(c.total.lines.pct)")
- echo "Coverage de linhas: ${PCT}%"
- awk -v p="$PCT" 'BEGIN { if (p+0 < 80) { print "::error::Coverage abaixo de 80% (atual: " p "%)"; exit 1 } }'
-
- # Instala browsers e roda Playwright para gerar evidências
- - name: Install Playwright browsers
- run: npx playwright install --with-deps
-
- - name: Run Playwright tests
- run: npx playwright test
-
- # Bloqueia se evidência Playwright (junit ou test-results) não existe
- - name: Verify Playwright evidence
- run: |
- set -euo pipefail
- if [ ! -d test-results ] && [ ! -f test-results/results.xml ] && [ ! -f test-results/results.json ]; then
- echo "::error::Evidência Playwright ausente. Rode 'npx playwright test' e gere artifacts em test-results/."
- exit 1
- fi
- echo "Evidência Playwright OK em test-results/"
-
- # Lint do título do PR seguindo conventional commits
- name: Validate PR title (conventional commit)
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
- echo "$PR_TITLE" | npx --yes commitlint --extends @commitlint/config-conventional || {
- echo "::error::Título do PR não segue Conventional Commits (ex: feat: ..., fix: ..., chore: ...)."
+ # Types per .skills/conventional-commits/SKILL.md
+ if ! echo "$PR_TITLE" | grep -Eq '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-zA-Z0-9_.-]+\))?!?: .+'; then
+ echo "::error::PR title must follow Conventional Commits (feat:, fix:, docs:, ...). Got: $PR_TITLE"
exit 1
- }
+ fi
+ echo "PR title OK: $PR_TITLE"
- # Verifica se PR body referencia uma task (#NNN, task-NNN.md, etc.)
- - name: Check task reference in PR body
+ - name: Verify PR body is non-empty
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -euo pipefail
- if [ -z "${PR_BODY:-}" ]; then
- echo "::error::PR body vazio. Inclua referência à task (ex: 'Closes #42' ou link para .specs/sprints/.../task.md)."
- exit 1
- fi
- if ! echo "$PR_BODY" | grep -Eq '(#[0-9]+|task-?[0-9]+\.md|\.specs/sprints/.+\.task\.md)'; then
- echo "::error::PR body deve referenciar uma task (#NNN, task-NNN.md ou link em .specs/sprints/)."
+ stripped=$(printf '%s' "${PR_BODY:-}" | tr -d '[:space:]')
+ if [ -z "$stripped" ]; then
+ echo "::error::PR body is empty. Describe what changed and why."
exit 1
fi
+ echo "PR body present (${#PR_BODY} chars)."
- # Se mexeu em architecture/, exige ADR linkado no body
- name: Require ADR link when architecture changes
env:
PR_BODY: ${{ github.event.pull_request.body }}
@@ -107,10 +70,49 @@ jobs:
run: |
set -euo pipefail
CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true)
- if echo "$CHANGED" | grep -Eq '^\.specs/architecture/|^architecture/'; then
- echo "Mudanças detectadas em architecture/. Verificando ADR linkado..."
- if ! echo "${PR_BODY:-}" | grep -Eq '(ADR-[0-9]+|adr-[0-9]+)'; then
- echo "::error::Mudanças em architecture/ exigem ADR linkado no PR body (ex: ADR-001, ADR-042)."
+ if echo "$CHANGED" | grep -Eq '^\.specs/architecture/'; then
+ echo "Changes detected under .specs/architecture/. Checking ADR reference..."
+ if ! echo "${PR_BODY:-}" | grep -Eiq '(ADR-[0-9]+)'; then
+ echo "::error::Changes under .specs/architecture/ require an ADR link in the PR body (e.g. ADR-001)."
exit 1
fi
+ echo "ADR reference OK."
+ else
+ echo "No architecture changes — skipping ADR requirement."
fi
+
+ - name: Detect remotion-tutorial changes
+ id: remotion
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: |
+ set -euo pipefail
+ CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true)
+ if echo "$CHANGED" | grep -Eq '^remotion-tutorial/'; then
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ echo "remotion-tutorial/ touched — typecheck enabled."
+ else
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ echo "remotion-tutorial/ untouched — skipping typecheck."
+ fi
+
+ - name: Setup Node (for Remotion typecheck)
+ if: steps.remotion.outputs.changed == 'true'
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
+ - name: Install Remotion deps
+ if: steps.remotion.outputs.changed == 'true'
+ working-directory: remotion-tutorial
+ run: npm install --no-audit --no-fund --loglevel=error
+
+ - name: Typecheck Remotion composition
+ if: steps.remotion.outputs.changed == 'true'
+ working-directory: remotion-tutorial
+ run: npx tsc -p tsconfig.json --noEmit
+
+ - name: DoD summary
+ run: |
+ echo "::notice::DoD gate passed — PR title, body, ADR rule and Remotion typecheck (if applicable) are all green."
diff --git a/.gitignore b/.gitignore
index 256266f..d5004eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,3 +47,4 @@ htmlcov/
*.mp3
!references/**/*.png
!examples/**/*.png
+!docs/media/**
diff --git a/README.md b/README.md
index 33cc857..0c716d2 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,69 @@
> One install. Every agent. Full WaveSpeedAI inference platform — 700+ media models and 290+ OpenAI-compatible LLMs — wired into Claude Code, Codex, Hermes Agent, OpenClaw, Cursor, Windsurf, and any other host that follows the [agentskills.io](https://agentskills.io) `SKILL.md` spec.
+## 60-second tour
+
+
+
+
+
+
+
+
+ ▶ Watch the 60s MP4 ·
+ 1920×1080 · 30 fps
+
+
+
+Play inline on GitHub (auto-embedded video)
+
+https://github.com/wesleysimplicio/WaveSpeedAI-Skills/raw/main/docs/media/tutorial-en.mp4
+
+
+
+### Storyboard — every scene, frame-by-frame
+
+The tutorial is a 7-scene Remotion composition. Each still below is a frame from the middle of its scene, captured after the entrance animations have settled.
+
+
+
+
+
+ 1 · Intro — 5.0 s · logo, title, host chips, waveform pulse
+
+
+
+ 2 · What is the skill — 9.0 s · 700+ media · 290+ LLMs · one CLI
+
+
+
+ 3 · Install — 10.0 s · animated terminal + installer steps
+
+
+
+
+
+ 4 · Supported hosts — 8.0 s · grid of 7 hosts + SKILL.md paths
+
+
+
+ 5 · CLI in action — 12.0 s · wavespeed-cli typing demo
+
+
+
+ 6 · Examples — 10.0 s · image · video · LLM previews
+
+
+
+
+
+ 7 · Outro / CTA — 6.0 s · one-line install + repo + license
+
+
+
+
+> Source code for the video lives in [`remotion-tutorial/`](remotion-tutorial/) — pure [Remotion](https://www.remotion.dev/) (TypeScript + React), no external assets. Re-render with `cd remotion-tutorial && npm install && npm run build:en` (or `npm run build` for the Portuguese version embedded in [README.pt-BR.md](README.pt-BR.md)). Higher-resolution stills are in [`docs/media/scenes/en/`](docs/media/scenes/en/).
+
```bash
# One-line install (interactive, picks the agents you have)
bash <(curl -fsSL https://raw.githubusercontent.com/wesleysimplicio/WaveSpeedAI-Skills/main/install.sh)
diff --git a/README.pt-BR.md b/README.pt-BR.md
index 43ec042..fda3b4f 100644
--- a/README.pt-BR.md
+++ b/README.pt-BR.md
@@ -9,6 +9,70 @@
> Uma instalação. Todo agente. Plataforma WaveSpeedAI completa — 700+ modelos de mídia e 290+ LLMs compatíveis com OpenAI — conectada em Claude Code, Codex, Hermes Agent, OpenClaw, Cursor, Windsurf e qualquer host que siga a spec [agentskills.io](https://agentskills.io) `SKILL.md`.
+## Tour de 60 segundos
+
+
+
+
+
+
+
+
+ ▶ Assista ao MP4 de 60 s ·
+ WebM (VP9) ·
+ 1920×1080 · 30 fps
+
+
+
+Tocar inline no GitHub (vídeo embedado)
+
+https://github.com/wesleysimplicio/WaveSpeedAI-Skills/raw/main/docs/media/tutorial.mp4
+
+
+
+### Storyboard — cena por cena
+
+O tutorial é uma composição Remotion de 7 cenas. Cada still abaixo é um frame do meio da cena, depois das animações de entrada estabilizarem.
+
+
+
+
+
+ 1 · Intro — 5,0 s · logo, título, chips de hosts, pulse de waveform
+
+
+
+ 2 · O que é a skill — 9,0 s · 700+ mídia · 290+ LLMs · uma CLI
+
+
+
+ 3 · Instalação — 10,0 s · terminal animado + checklist do instalador
+
+
+
+
+
+ 4 · Hosts suportados — 8,0 s · grid de 7 hosts + paths SKILL.md
+
+
+
+ 5 · CLI em ação — 12,0 s · wavespeed-cli typing demo
+
+
+
+ 6 · Exemplos — 10,0 s · imagem · vídeo · LLM previews
+
+
+
+
+
+ 7 · Outro / CTA — 6,0 s · install one-line + repo + licença
+
+
+
+
+> O código-fonte do vídeo fica em [`remotion-tutorial/`](remotion-tutorial/) — puro [Remotion](https://www.remotion.dev/) (TypeScript + React), sem assets externos. Re-renderize com `cd remotion-tutorial && npm install && npm run build`. Stills em maior resolução em [`docs/media/scenes/`](docs/media/scenes/).
+
```bash
# Instalação one-line (interativo, escolhe os agentes que você tem)
bash <(curl -fsSL https://raw.githubusercontent.com/wesleysimplicio/WaveSpeedAI-Skills/main/install.sh)
diff --git a/docs/media/scenes/01-intro.png b/docs/media/scenes/01-intro.png
new file mode 100644
index 0000000..899ec16
Binary files /dev/null and b/docs/media/scenes/01-intro.png differ
diff --git a/docs/media/scenes/02-what.png b/docs/media/scenes/02-what.png
new file mode 100644
index 0000000..39ae24a
Binary files /dev/null and b/docs/media/scenes/02-what.png differ
diff --git a/docs/media/scenes/03-install.png b/docs/media/scenes/03-install.png
new file mode 100644
index 0000000..77871ca
Binary files /dev/null and b/docs/media/scenes/03-install.png differ
diff --git a/docs/media/scenes/04-hosts.png b/docs/media/scenes/04-hosts.png
new file mode 100644
index 0000000..a9ebc5d
Binary files /dev/null and b/docs/media/scenes/04-hosts.png differ
diff --git a/docs/media/scenes/05-cli.png b/docs/media/scenes/05-cli.png
new file mode 100644
index 0000000..f528b50
Binary files /dev/null and b/docs/media/scenes/05-cli.png differ
diff --git a/docs/media/scenes/06-examples.png b/docs/media/scenes/06-examples.png
new file mode 100644
index 0000000..711a4a8
Binary files /dev/null and b/docs/media/scenes/06-examples.png differ
diff --git a/docs/media/scenes/07-outro.png b/docs/media/scenes/07-outro.png
new file mode 100644
index 0000000..ef81f0a
Binary files /dev/null and b/docs/media/scenes/07-outro.png differ
diff --git a/docs/media/scenes/README.md b/docs/media/scenes/README.md
new file mode 100644
index 0000000..087356d
--- /dev/null
+++ b/docs/media/scenes/README.md
@@ -0,0 +1,36 @@
+# Tutorial scene evidence
+
+Per-scene stills rendered from the Remotion composition `WaveSpeedSkillTutorial`
+(see [`remotion-tutorial/`](../../../remotion-tutorial/)). Each PNG is captured
+at a representative frame near the middle of its scene, after entrance
+animations have settled.
+
+These stills are the regression evidence for the animated tutorial: if a
+future change to the composition silently breaks one of the scenes, a new
+render here will surface the visual diff.
+
+| # | Scene | File | Frame | Highlights |
+|---|---|---|---:|---|
+| 1 | Intro | [01-intro.png](01-intro.png) | 90 | Logo, title, host chips, animated waveform pulse |
+| 2 | What is the skill | [02-what.png](02-what.png) | 240 | 3 feature cards: 700+ media · 290+ LLMs · CLI |
+| 3 | Install | [03-install.png](03-install.png) | 520 | Animated terminal + installer step checklist |
+| 4 | Supported hosts | [04-hosts.png](04-hosts.png) | 820 | Grid of 7 hosts with each `SKILL.md` path |
+| 5 | CLI in action | [05-cli.png](05-cli.png) | 1180 | `wavespeed-cli run` typing demo + subcommand grid |
+| 6 | Examples | [06-examples.png](06-examples.png) | 1460 | Image · video · LLM previews |
+| 7 | Outro / CTA | [07-outro.png](07-outro.png) | 1720 | One-line install + repo + license + spec badges |
+
+## Re-generate
+
+```bash
+cd remotion-tutorial
+npm install
+mkdir -p out/regression
+npx remotion still WaveSpeedSkillTutorial out/regression/01-intro.png --frame=90
+npx remotion still WaveSpeedSkillTutorial out/regression/02-what.png --frame=240
+npx remotion still WaveSpeedSkillTutorial out/regression/03-install.png --frame=520
+npx remotion still WaveSpeedSkillTutorial out/regression/04-hosts.png --frame=820
+npx remotion still WaveSpeedSkillTutorial out/regression/05-cli.png --frame=1180
+npx remotion still WaveSpeedSkillTutorial out/regression/06-examples.png --frame=1460
+npx remotion still WaveSpeedSkillTutorial out/regression/07-outro.png --frame=1720
+cp out/regression/*.png ../docs/media/scenes/
+```
diff --git a/docs/media/scenes/en/01-intro.png b/docs/media/scenes/en/01-intro.png
new file mode 100644
index 0000000..351ba46
Binary files /dev/null and b/docs/media/scenes/en/01-intro.png differ
diff --git a/docs/media/scenes/en/02-what.png b/docs/media/scenes/en/02-what.png
new file mode 100644
index 0000000..b984e1f
Binary files /dev/null and b/docs/media/scenes/en/02-what.png differ
diff --git a/docs/media/scenes/en/03-install.png b/docs/media/scenes/en/03-install.png
new file mode 100644
index 0000000..dbde6b4
Binary files /dev/null and b/docs/media/scenes/en/03-install.png differ
diff --git a/docs/media/scenes/en/04-hosts.png b/docs/media/scenes/en/04-hosts.png
new file mode 100644
index 0000000..c72dbf4
Binary files /dev/null and b/docs/media/scenes/en/04-hosts.png differ
diff --git a/docs/media/scenes/en/05-cli.png b/docs/media/scenes/en/05-cli.png
new file mode 100644
index 0000000..5ed1a1c
Binary files /dev/null and b/docs/media/scenes/en/05-cli.png differ
diff --git a/docs/media/scenes/en/06-examples.png b/docs/media/scenes/en/06-examples.png
new file mode 100644
index 0000000..545c39e
Binary files /dev/null and b/docs/media/scenes/en/06-examples.png differ
diff --git a/docs/media/scenes/en/07-outro.png b/docs/media/scenes/en/07-outro.png
new file mode 100644
index 0000000..d4e0bc4
Binary files /dev/null and b/docs/media/scenes/en/07-outro.png differ
diff --git a/docs/media/tutorial-en.mp4 b/docs/media/tutorial-en.mp4
new file mode 100644
index 0000000..edbb0fc
Binary files /dev/null and b/docs/media/tutorial-en.mp4 differ
diff --git a/docs/media/tutorial-poster-en.png b/docs/media/tutorial-poster-en.png
new file mode 100644
index 0000000..7c5b7bc
Binary files /dev/null and b/docs/media/tutorial-poster-en.png differ
diff --git a/docs/media/tutorial-poster.png b/docs/media/tutorial-poster.png
new file mode 100644
index 0000000..a398b02
Binary files /dev/null and b/docs/media/tutorial-poster.png differ
diff --git a/docs/media/tutorial.mp4 b/docs/media/tutorial.mp4
new file mode 100644
index 0000000..0c3a9a4
Binary files /dev/null and b/docs/media/tutorial.mp4 differ
diff --git a/docs/media/tutorial.webm b/docs/media/tutorial.webm
new file mode 100644
index 0000000..7e3bde0
Binary files /dev/null and b/docs/media/tutorial.webm differ
diff --git a/remotion-tutorial/.gitignore b/remotion-tutorial/.gitignore
new file mode 100644
index 0000000..ed14077
--- /dev/null
+++ b/remotion-tutorial/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+out/
+.cache/
+*.log
+.DS_Store
diff --git a/remotion-tutorial/README.md b/remotion-tutorial/README.md
new file mode 100644
index 0000000..2ea6767
--- /dev/null
+++ b/remotion-tutorial/README.md
@@ -0,0 +1,56 @@
+# WaveSpeedAI Skill — Tutorial Animado (Remotion)
+
+Vídeo explicativo de ~60 segundos (1920x1080, 30 fps) que mostra como instalar e usar a skill `wavespeed` em qualquer agent host (Claude, Codex, Hermes, OpenClaw, Cursor, Windsurf, generic).
+
+> Renderizado 100% em código com [Remotion](https://www.remotion.dev/). Sem assets externos — backgrounds, partículas, code blocks e previews de mídia são gerados em runtime.
+
+## Estrutura
+
+```
+src/
+├── index.ts # registerRoot do Remotion
+├── Root.tsx # composition WaveSpeedSkillTutorial
+└── tutorial/
+ ├── Tutorial.tsx # sequencia as 7 cenas
+ ├── timing.ts # tabela de duração + offsets
+ ├── theme.ts # cores, fontes, gradientes
+ ├── components/ # primitivas (Card, CodeBlock, MediaPreview…)
+ └── scenes/ # SceneIntro → SceneOutro
+```
+
+| # | Cena | Duração | Foco |
+|---|---------------------|----------|-------------------------------------------------------|
+| 1 | Intro | 5.0 s | Logo, título, hosts suportados, pulse animado |
+| 2 | O que é | 9.0 s | 3 cards: 700+ modelos · 290+ LLMs · CLI única |
+| 3 | Instalação | 10.0 s | Terminal animado + checklist do instalador |
+| 4 | Hosts suportados | 8.0 s | Grid de 7 hosts com path do `SKILL.md` |
+| 5 | CLI em ação | 12.0 s | Code block com `wavespeed-cli run` + grid de comandos |
+| 6 | Exemplos | 10.0 s | Previews animados de imagem · vídeo · LLM |
+| 7 | Outro / CTA | 6.0 s | One-liner de instalação + repo + badges |
+
+Total: **60 s** = 1800 frames a 30 fps.
+
+## Rodar
+
+```bash
+cd remotion-tutorial
+npm install
+npm run dev # abre o Remotion Studio para preview interativo
+npm run still # gera out/preview.png (frame 90)
+npm run build # renderiza out/wavespeed-skill-tutorial.mp4
+npm run build:webm # renderiza .webm (vp9)
+```
+
+A primeira renderização baixa o Chromium headless (~150 MB) — depois fica em cache.
+
+## Customizar
+
+- **Cores e fontes**: `src/tutorial/theme.ts`.
+- **Duração das cenas**: `src/tutorial/timing.ts` (tabela `SCENES`).
+- **Conteúdo de uma cena**: `src/tutorial/scenes/.tsx` — texto, comandos e props ficam inline para edição rápida.
+- **Trocar formato vertical (Reels/Shorts)**: edite `src/Root.tsx` para `width: 1080, height: 1920` e ajuste padding nas cenas.
+
+## Saída
+
+- MP4 H.264 1920x1080 30 fps em `out/wavespeed-skill-tutorial.mp4`.
+- O diretório `out/` está no `.gitignore` — não commita binário.
diff --git a/remotion-tutorial/package.json b/remotion-tutorial/package.json
new file mode 100644
index 0000000..72498dc
--- /dev/null
+++ b/remotion-tutorial/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "wavespeedai-skill-remotion-tutorial",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Animated Remotion video that explains how to install and use the WaveSpeedAI Skill across agent hosts.",
+ "type": "module",
+ "scripts": {
+ "dev": "remotion studio",
+ "studio": "remotion studio",
+ "build": "remotion render WaveSpeedSkillTutorial out/wavespeed-skill-tutorial.mp4",
+ "build:webm": "remotion render WaveSpeedSkillTutorial out/wavespeed-skill-tutorial.webm --codec=vp9",
+ "build:en": "remotion render WaveSpeedSkillTutorialEN out/wavespeed-skill-tutorial-en.mp4",
+ "build:en:webm": "remotion render WaveSpeedSkillTutorialEN out/wavespeed-skill-tutorial-en.webm --codec=vp9",
+ "still": "remotion still WaveSpeedSkillTutorial out/preview.png --frame=90",
+ "still:en": "remotion still WaveSpeedSkillTutorialEN out/preview-en.png --frame=90",
+ "lint": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@remotion/cli": "4.0.366",
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
+ "remotion": "4.0.366"
+ },
+ "devDependencies": {
+ "@types/react": "19.0.0",
+ "@types/react-dom": "19.0.0",
+ "typescript": "5.6.3"
+ }
+}
diff --git a/remotion-tutorial/remotion.config.ts b/remotion-tutorial/remotion.config.ts
new file mode 100644
index 0000000..fb0a567
--- /dev/null
+++ b/remotion-tutorial/remotion.config.ts
@@ -0,0 +1,6 @@
+import { Config } from '@remotion/cli/config';
+
+Config.setVideoImageFormat('jpeg');
+Config.setOverwriteOutput(true);
+Config.setEntryPoint('src/index.ts');
+Config.setConcurrency(null);
diff --git a/remotion-tutorial/src/Root.tsx b/remotion-tutorial/src/Root.tsx
new file mode 100644
index 0000000..cadef79
--- /dev/null
+++ b/remotion-tutorial/src/Root.tsx
@@ -0,0 +1,30 @@
+import { Composition } from 'remotion';
+import { Tutorial } from './tutorial/Tutorial';
+import { TUTORIAL_DURATION_FRAMES, TUTORIAL_FPS } from './tutorial/timing';
+
+export const Root: React.FC = () => {
+ return (
+ <>
+ {/* Default — pt-BR. Kept under the original id so existing renders
+ (docs/media/tutorial.mp4) keep regenerating to the same path. */}
+
+
+ >
+ );
+};
diff --git a/remotion-tutorial/src/index.ts b/remotion-tutorial/src/index.ts
new file mode 100644
index 0000000..aa152d6
--- /dev/null
+++ b/remotion-tutorial/src/index.ts
@@ -0,0 +1,4 @@
+import { registerRoot } from 'remotion';
+import { Root } from './Root';
+
+registerRoot(Root);
diff --git a/remotion-tutorial/src/tutorial/Tutorial.tsx b/remotion-tutorial/src/tutorial/Tutorial.tsx
new file mode 100644
index 0000000..47bf9c2
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/Tutorial.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { AbsoluteFill, Sequence } from 'remotion';
+import { SCENES } from './timing';
+import { SceneIntro } from './scenes/SceneIntro';
+import { SceneWhatIsSkill } from './scenes/SceneWhatIsSkill';
+import { SceneInstall } from './scenes/SceneInstall';
+import { SceneHosts } from './scenes/SceneHosts';
+import { SceneCli } from './scenes/SceneCli';
+import { SceneExamples } from './scenes/SceneExamples';
+import { SceneOutro } from './scenes/SceneOutro';
+import { colors } from './theme';
+import { LocaleContext, type Locale } from './i18n';
+
+const SCENE_COMPONENTS: Record = {
+ intro: SceneIntro,
+ 'what-is-skill': SceneWhatIsSkill,
+ install: SceneInstall,
+ hosts: SceneHosts,
+ cli: SceneCli,
+ examples: SceneExamples,
+ outro: SceneOutro,
+};
+
+export type TutorialProps = {
+ locale?: Locale;
+};
+
+export const Tutorial: React.FC = ({ locale = 'pt-BR' }) => {
+ let cursor = 0;
+ return (
+
+
+ {SCENES.map((scene) => {
+ const Component = SCENE_COMPONENTS[scene.id];
+ const from = cursor;
+ cursor += scene.durationInFrames;
+ return (
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/BackdropGradient.tsx b/remotion-tutorial/src/tutorial/components/BackdropGradient.tsx
new file mode 100644
index 0000000..8952d43
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/BackdropGradient.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
+import { colors } from '../theme';
+
+type Props = {
+ /** Tint accent for this scene. */
+ accent?: 'violet' | 'cyan' | 'amber' | 'emerald';
+ intensity?: number;
+};
+
+const ACCENT_MAP: Record, string> = {
+ violet: colors.violet,
+ cyan: colors.cyan,
+ amber: colors.amber,
+ emerald: colors.emerald,
+};
+
+export const BackdropGradient: React.FC = ({
+ accent = 'violet',
+ intensity = 1,
+}) => {
+ const frame = useCurrentFrame();
+ const accentColor = ACCENT_MAP[accent];
+
+ const orb1X = interpolate(frame, [0, 240], [10, 30], { extrapolateRight: 'extend' });
+ const orb1Y = interpolate(frame, [0, 240], [20, 40], { extrapolateRight: 'extend' });
+ const orb2X = interpolate(frame, [0, 240], [80, 60], { extrapolateRight: 'extend' });
+ const orb2Y = interpolate(frame, [0, 240], [70, 55], { extrapolateRight: 'extend' });
+ const orb3X = interpolate(frame, [0, 240], [50, 70], { extrapolateRight: 'extend' });
+ const orb3Y = interpolate(frame, [0, 240], [80, 60], { extrapolateRight: 'extend' });
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const NoiseOverlay: React.FC = () => (
+ \")",
+ }}
+ />
+);
+
+const Vignette: React.FC = () => (
+
+);
diff --git a/remotion-tutorial/src/tutorial/components/Card.tsx b/remotion-tutorial/src/tutorial/components/Card.tsx
new file mode 100644
index 0000000..c4c9eea
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/Card.tsx
@@ -0,0 +1,151 @@
+import React from 'react';
+import { spring, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
+import { fonts, colors, shadows } from '../theme';
+
+type Props = {
+ startFrame?: number;
+ delay?: number;
+ accent?: string;
+ icon?: React.ReactNode;
+ title: string;
+ description?: React.ReactNode;
+ footer?: React.ReactNode;
+ width?: number | string;
+ minHeight?: number | string;
+ children?: React.ReactNode;
+};
+
+export const Card: React.FC = ({
+ startFrame = 0,
+ delay = 0,
+ accent = colors.violet,
+ icon,
+ title,
+ description,
+ footer,
+ width = 380,
+ minHeight = 280,
+ children,
+}) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const local = frame - startFrame - delay;
+
+ const enter = spring({
+ frame: local,
+ fps,
+ config: { damping: 18, stiffness: 110, mass: 0.8 },
+ });
+ const translate = interpolate(enter, [0, 1], [38, 0]);
+ const glow = interpolate(enter, [0, 1], [0, 1]);
+
+ return (
+
+
+
+
+ {icon ? (
+
+ {icon}
+
+ ) : null}
+
+ {title}
+
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+ {children ? (
+
{children}
+ ) : null}
+ {footer ? (
+
+ {footer}
+
+ ) : null}
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/Chip.tsx b/remotion-tutorial/src/tutorial/components/Chip.tsx
new file mode 100644
index 0000000..28e3e41
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/Chip.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { spring, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
+import { fonts, colors } from '../theme';
+
+type Props = {
+ label: string;
+ startFrame?: number;
+ delay?: number;
+ color?: string;
+ icon?: React.ReactNode;
+};
+
+export const Chip: React.FC = ({
+ label,
+ startFrame = 0,
+ delay = 0,
+ color = colors.violetSoft,
+ icon,
+}) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const local = frame - startFrame - delay;
+ const enter = spring({
+ frame: local,
+ fps,
+ config: { damping: 16, stiffness: 140, mass: 0.7 },
+ });
+ const scale = interpolate(enter, [0, 1], [0.6, 1]);
+ const translate = interpolate(enter, [0, 1], [10, 0]);
+
+ return (
+
+ {icon}
+ {label}
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/CodeBlock.tsx b/remotion-tutorial/src/tutorial/components/CodeBlock.tsx
new file mode 100644
index 0000000..1fdefa0
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/CodeBlock.tsx
@@ -0,0 +1,159 @@
+import React from 'react';
+import { useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion';
+import { fonts, colors, shadows } from '../theme';
+
+export type CodeLine = {
+ text: string;
+ color?: string;
+ prompt?: string;
+ comment?: boolean;
+};
+
+type Props = {
+ title?: string;
+ lines: CodeLine[];
+ startFrame?: number;
+ lineDuration?: number;
+ charsPerFrame?: number;
+ width?: number | string;
+ showCursor?: boolean;
+ topAccent?: string;
+};
+
+export const CodeBlock: React.FC = ({
+ title = 'terminal',
+ lines,
+ startFrame = 0,
+ lineDuration = 22,
+ charsPerFrame = 1.4,
+ width = 760,
+ showCursor = true,
+ topAccent = colors.violet,
+}) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const local = frame - startFrame;
+
+ const enter = spring({
+ frame: local,
+ fps,
+ config: { damping: 22, stiffness: 110 },
+ });
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+
+ {lines.map((line, i) => {
+ const enterFrame = i * lineDuration;
+ const visible = local >= enterFrame;
+ if (!visible) {
+ return (
+
+ );
+ }
+ const sinceEnter = local - enterFrame;
+ const totalChars = line.text.length;
+ const visibleChars = Math.floor(
+ Math.min(totalChars, sinceEnter * charsPerFrame),
+ );
+ const isTyping = visibleChars < totalChars;
+ return (
+
+ {line.prompt ? (
+ {line.prompt}
+ ) : null}
+ {line.text.slice(0, visibleChars)}
+ {showCursor && isTyping ? (
+
+ ) : null}
+
+ );
+ })}
+
+
+ );
+};
+
+const Dot: React.FC<{ color: string }> = ({ color }) => (
+
+);
+
+const BlinkingCursor: React.FC<{ frame: number }> = ({ frame }) => {
+ const visible = Math.floor(frame / 8) % 2 === 0;
+ return (
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/HostBadge.tsx b/remotion-tutorial/src/tutorial/components/HostBadge.tsx
new file mode 100644
index 0000000..420280f
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/HostBadge.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { spring, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
+import { fonts, colors } from '../theme';
+
+type Props = {
+ name: string;
+ monogram: string;
+ path: string;
+ color: string;
+ startFrame?: number;
+ delay?: number;
+};
+
+export const HostBadge: React.FC = ({
+ name,
+ monogram,
+ path,
+ color,
+ startFrame = 0,
+ delay = 0,
+}) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const local = frame - startFrame - delay;
+ const enter = spring({
+ frame: local,
+ fps,
+ config: { damping: 18, stiffness: 130, mass: 0.7 },
+ });
+ const translate = interpolate(enter, [0, 1], [22, 0]);
+ const scale = interpolate(enter, [0, 1], [0.85, 1]);
+
+ return (
+
+
+
+ {monogram}
+
+
+ {name}
+
+
+
+ {path}
+
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/Logo.tsx b/remotion-tutorial/src/tutorial/components/Logo.tsx
new file mode 100644
index 0000000..13cf9dd
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/Logo.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion';
+import { colors, fonts, gradients } from '../theme';
+
+type Props = {
+ size?: number;
+ startFrame?: number;
+};
+
+export const Logo: React.FC = ({ size = 110, startFrame = 0 }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const local = frame - startFrame;
+ const enter = spring({
+ frame: local,
+ fps,
+ config: { damping: 14, stiffness: 110 },
+ });
+ const wave = (i: number) =>
+ Math.sin((frame * 0.12) + i * 0.7) * 0.45 + 0.55;
+
+ return (
+
+
+ {[0, 1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/MediaPreview.tsx b/remotion-tutorial/src/tutorial/components/MediaPreview.tsx
new file mode 100644
index 0000000..1e41f85
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/MediaPreview.tsx
@@ -0,0 +1,377 @@
+import React from 'react';
+import {
+ useCurrentFrame,
+ spring,
+ useVideoConfig,
+ interpolate,
+ random,
+} from 'remotion';
+import { fonts, colors } from '../theme';
+import { useStrings } from '../i18n';
+
+type Variant = 'image' | 'video' | 'llm';
+
+type Props = {
+ variant: Variant;
+ startFrame?: number;
+ delay?: number;
+ label: string;
+ modelId: string;
+ prompt?: string;
+ width?: number;
+ height?: number;
+};
+
+export const MediaPreview: React.FC = ({
+ variant,
+ startFrame = 0,
+ delay = 0,
+ label,
+ modelId,
+ prompt,
+ width = 380,
+ height = 260,
+}) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const local = frame - startFrame - delay;
+ const enter = spring({
+ frame: local,
+ fps,
+ config: { damping: 18, stiffness: 110 },
+ });
+ const translate = interpolate(enter, [0, 1], [40, 0]);
+
+ return (
+
+
+ {variant === 'image' && }
+ {variant === 'video' && }
+ {variant === 'llm' && }
+
+
+
+
+ {modelId}
+
+
+ {label}
+
+ {prompt ? (
+
+ prompt
+ {prompt}
+
+ ) : null}
+
+
+ );
+};
+
+const Tag: React.FC<{ variant: Variant }> = ({ variant }) => {
+ const map: Record = {
+ image: { label: 'TEXT IMAGE', color: colors.violet },
+ video: { label: 'IMAGE VIDEO', color: colors.cyan },
+ llm: { label: 'CHAT LLM', color: colors.amber },
+ };
+ const tag = map[variant];
+ return (
+
+ {tag.label}
+
+ );
+};
+
+const ImagePreview: React.FC<{ frame: number }> = ({ frame }) => {
+ const blobs = Array.from({ length: 6 }, (_, i) => {
+ const x = random(`img-x-${i}`) * 100;
+ const y = random(`img-y-${i}`) * 100;
+ const size = 80 + random(`img-s-${i}`) * 140;
+ const dx = Math.sin(frame * 0.02 + i) * 8;
+ const palette = [
+ '#f472b6',
+ '#a78bfa',
+ '#22d3ee',
+ '#fbbf24',
+ '#34d399',
+ '#fb7185',
+ ];
+ return {
+ x,
+ y,
+ size,
+ dx,
+ color: palette[i % palette.length],
+ };
+ });
+ return (
+
+ {blobs.map((b, i) => (
+
+ ))}
+
+
+ );
+};
+
+const VideoPreview: React.FC<{ frame: number }> = ({ frame }) => {
+ const t = (frame * 0.04) % (Math.PI * 2);
+ return (
+
+
+ {/* horizon line */}
+
+ {/* moving objects */}
+ {Array.from({ length: 4 }).map((_, i) => {
+ const offset = (frame * 0.6 + i * 90) % 460;
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
+
+const PlayBadge: React.FC = () => (
+
+);
+
+const Timecode: React.FC<{ frame: number }> = ({ frame }) => {
+ const seconds = (frame / 30).toFixed(1);
+ return (
+
+ {`00:${String(Math.floor(Number(seconds))).padStart(2, '0')}`}
+
+ );
+};
+
+const LlmPreview: React.FC<{ frame: number }> = ({ frame }) => {
+ const chat = useStrings().examples.chat;
+ const messages = [
+ { who: 'user', text: chat.user },
+ { who: 'claude', text: chat.assistant },
+ ];
+ return (
+
+ {messages.map((m, i) => {
+ const delay = i * 30;
+ const reveal = Math.max(0, Math.min(1, (frame - delay) / 30));
+ const charLimit = Math.floor(m.text.length * reveal);
+ return (
+
+
+ {m.who}
+
+
{m.text.slice(0, charLimit)}
+
+ );
+ })}
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/ParticleField.tsx b/remotion-tutorial/src/tutorial/components/ParticleField.tsx
new file mode 100644
index 0000000..b56cce9
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/ParticleField.tsx
@@ -0,0 +1,64 @@
+import React, { useMemo } from 'react';
+import { AbsoluteFill, useCurrentFrame, random, interpolate } from 'remotion';
+import { colors } from '../theme';
+
+type Props = {
+ count?: number;
+ seed?: string;
+ accent?: string;
+ speed?: number;
+};
+
+export const ParticleField: React.FC = ({
+ count = 56,
+ seed = 'particles',
+ accent = colors.violetSoft,
+ speed = 1,
+}) => {
+ const frame = useCurrentFrame();
+
+ const particles = useMemo(
+ () =>
+ Array.from({ length: count }, (_, i) => ({
+ x: random(`${seed}-x-${i}`) * 100,
+ y: random(`${seed}-y-${i}`) * 100,
+ size: 2 + random(`${seed}-s-${i}`) * 4,
+ amp: 6 + random(`${seed}-a-${i}`) * 14,
+ phase: random(`${seed}-p-${i}`) * Math.PI * 2,
+ twinkle: random(`${seed}-t-${i}`),
+ color: random(`${seed}-c-${i}`) > 0.7 ? colors.cyanSoft : accent,
+ })),
+ [count, seed, accent],
+ );
+
+ return (
+
+ {particles.map((p, i) => {
+ const phaseFrame = frame * 0.04 * speed + p.phase;
+ const dx = Math.sin(phaseFrame) * p.amp;
+ const dy = Math.cos(phaseFrame * 0.8) * p.amp;
+ const opacity = interpolate(
+ Math.sin(phaseFrame * 1.6 + p.twinkle * 6),
+ [-1, 1],
+ [0.18, 0.85],
+ );
+ return (
+
+ );
+ })}
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/SceneFrame.tsx b/remotion-tutorial/src/tutorial/components/SceneFrame.tsx
new file mode 100644
index 0000000..5fbde2e
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/SceneFrame.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import {
+ AbsoluteFill,
+ useCurrentFrame,
+ useVideoConfig,
+ interpolate,
+ Easing,
+} from 'remotion';
+import { BackdropGradient } from './BackdropGradient';
+import { ParticleField } from './ParticleField';
+import { fonts, colors } from '../theme';
+
+type Accent = 'violet' | 'cyan' | 'amber' | 'emerald';
+
+type Props = {
+ accent?: Accent;
+ particleSeed: string;
+ showParticles?: boolean;
+ children: React.ReactNode;
+ badge?: string;
+ fadeOutAt?: number;
+};
+
+const ACCENT_PARTICLE: Record = {
+ violet: colors.violetSoft,
+ cyan: colors.cyanSoft,
+ amber: colors.amber,
+ emerald: colors.emerald,
+};
+
+export const SceneFrame: React.FC = ({
+ accent = 'violet',
+ particleSeed,
+ showParticles = true,
+ children,
+ badge,
+ fadeOutAt,
+}) => {
+ const frame = useCurrentFrame();
+ const { durationInFrames } = useVideoConfig();
+
+ const fadeIn = interpolate(frame, [0, 18], [0, 1], {
+ extrapolateRight: 'clamp',
+ easing: Easing.out(Easing.cubic),
+ });
+
+ const fadeOutStart = fadeOutAt ?? durationInFrames - 18;
+ const fadeOut = interpolate(
+ frame,
+ [fadeOutStart, fadeOutStart + 18],
+ [1, 0],
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' },
+ );
+
+ const opacity = Math.min(fadeIn, fadeOut);
+
+ return (
+
+
+ {showParticles ? (
+
+ ) : null}
+ {badge ? (
+
+ {badge}
+
+ ) : null}
+ {children}
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/components/SceneTitle.tsx b/remotion-tutorial/src/tutorial/components/SceneTitle.tsx
new file mode 100644
index 0000000..83bc7d4
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/components/SceneTitle.tsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import { useCurrentFrame, spring, useVideoConfig, interpolate } from 'remotion';
+import { fonts, colors, gradients } from '../theme';
+
+type Props = {
+ eyebrow?: string;
+ title: string;
+ subtitle?: string;
+ accent?: string;
+ align?: 'left' | 'center';
+ startFrame?: number;
+};
+
+export const SceneTitle: React.FC = ({
+ eyebrow,
+ title,
+ subtitle,
+ accent = colors.violetSoft,
+ align = 'left',
+ startFrame = 0,
+}) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const local = frame - startFrame;
+
+ const eyebrowSpring = spring({
+ frame: local,
+ fps,
+ config: { damping: 20, stiffness: 110 },
+ });
+ const titleSpring = spring({
+ frame: local - 6,
+ fps,
+ config: { damping: 18, stiffness: 90 },
+ });
+ const subSpring = spring({
+ frame: local - 14,
+ fps,
+ config: { damping: 22, stiffness: 110 },
+ });
+
+ const eyebrowY = interpolate(eyebrowSpring, [0, 1], [12, 0]);
+ const titleY = interpolate(titleSpring, [0, 1], [28, 0]);
+ const subY = interpolate(subSpring, [0, 1], [16, 0]);
+
+ return (
+
+ {eyebrow ? (
+
+
+ {eyebrow}
+
+ ) : null}
+
+ {title}
+
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/i18n.ts b/remotion-tutorial/src/tutorial/i18n.ts
new file mode 100644
index 0000000..0807dbe
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/i18n.ts
@@ -0,0 +1,283 @@
+import React from 'react';
+
+export type Locale = 'pt-BR' | 'en';
+
+export const LOCALES: readonly Locale[] = ['pt-BR', 'en'] as const;
+
+type Strings = {
+ intro: {
+ badge: string;
+ subtitle: string;
+ };
+ whatIs: {
+ badge: string;
+ eyebrow: string;
+ title: string;
+ subtitle: string;
+ cards: {
+ media: { title: string; description: string };
+ llm: { title: string; description: string };
+ cli: { title: string; description: string };
+ };
+ };
+ install: {
+ badge: string;
+ eyebrow: string;
+ title: string;
+ subtitle: string;
+ code: {
+ commentInstall: string;
+ commentClone: string;
+ commentAuth: string;
+ };
+ steps: { label: string; detail: string }[];
+ };
+ hosts: {
+ badge: string;
+ eyebrow: string;
+ title: string;
+ subtitle: string;
+ };
+ cli: {
+ badge: string;
+ eyebrow: string;
+ title: string;
+ subtitle: string;
+ codeTitle: string;
+ cards: { label: string; description: string }[];
+ };
+ examples: {
+ badge: string;
+ eyebrow: string;
+ title: string;
+ subtitle: string;
+ items: {
+ image: { label: string; prompt: string };
+ video: { label: string; prompt: string };
+ llm: { label: string; prompt: string };
+ };
+ chat: {
+ user: string;
+ assistant: string;
+ };
+ };
+ outro: {
+ badge: string;
+ title: string;
+ pills: string[];
+ footer: string;
+ };
+};
+
+export const STRINGS: Record = {
+ 'pt-BR': {
+ intro: {
+ badge: '01 · INTRO',
+ subtitle: 'Uma instalação. Todo agent. 700+ modelos de mídia e LLM em uma só CLI.',
+ },
+ whatIs: {
+ badge: '02 · O QUE É',
+ eyebrow: 'A skill em uma frase',
+ title: 'Um manual que ensina o agent a chamar a WaveSpeedAI',
+ subtitle:
+ 'O arquivo SKILL.md vira capacidade nativa do seu agent — texto curto, frontmatter padronizado, comandos prontos.',
+ cards: {
+ media: {
+ title: '700+ modelos de mídia',
+ description:
+ 'Z-Image, FLUX, Seedance, Kling, Veo, Luma, Wan, Qwen, Higgsfield, ace-step. Imagem, vídeo, áudio e avatares — tudo pelo mesmo gateway.',
+ },
+ llm: {
+ title: '290+ LLMs OpenAI-compatible',
+ description:
+ 'Claude, GPT, Gemini, DeepSeek, Llama, Mistral, Qwen, xAI. Mesma chave, mesma assinatura, sem trocar SDK por provedor.',
+ },
+ cli: {
+ title: 'Uma CLI, qualquer agent',
+ description:
+ 'A skill empacota um shell único: wavespeed-cli. Async + polling automático, upload, webhooks, batch jobs.',
+ },
+ },
+ },
+ install: {
+ badge: '03 · INSTALAÇÃO',
+ eyebrow: 'Setup em uma linha',
+ title: 'curl, instala, configura — e já tem agent + CLI',
+ subtitle:
+ 'O instalador detecta quais agents você usa e só copia o SKILL.md onde faz sentido.',
+ code: {
+ commentInstall: '# install em qualquer host conhecido',
+ commentClone: '# ou clonado localmente',
+ commentAuth: '# autentica e valida',
+ },
+ steps: [
+ { label: 'Provisiona venv isolado', detail: '~/.local/share/wavespeed-skill/venv' },
+ { label: 'Instala SDK Python', detail: 'pip install wavespeed requests' },
+ { label: 'Dropa a CLI', detail: '~/.local/bin/wavespeed-cli' },
+ {
+ label: 'Detecta agents e copia o SKILL.md',
+ detail: 'claude · codex · hermes · cursor · windsurf · openclaw',
+ },
+ ],
+ },
+ hosts: {
+ badge: '04 · ONDE FUNCIONA',
+ eyebrow: 'Mesma skill, todos os agents',
+ title: 'Sete hosts suportados — frontmatter por host',
+ subtitle:
+ 'O instalador escolhe o SKILL.md certo para cada diretório de agent. Você só roda uma vez.',
+ },
+ cli: {
+ badge: '05 · CLI EM AÇÃO',
+ eyebrow: 'Como o agent invoca a skill',
+ title: 'wavespeed-cli — superfície única, contratos previsíveis',
+ subtitle:
+ 'Comandos curtos, JSON nas saídas. O agent gera, espera e devolve URLs prontas.',
+ codeTitle: 'terminal — text→image (Z-Image turbo)',
+ cards: [
+ { label: 'models', description: 'Lista o catálogo vivo de 700+ modelos.' },
+ { label: 'run', description: 'Roda inferência async com polling automático.' },
+ { label: 'submit', description: 'Fire-and-forget com webhook de callback.' },
+ { label: 'upload', description: 'Manda arquivo local e devolve URL hospedada.' },
+ { label: 'llm', description: 'Chat OpenAI-compatible para Claude, GPT, Gemini.' },
+ { label: 'verify-webhook', description: 'Valida HMAC-SHA256 do callback do WaveSpeed.' },
+ ],
+ },
+ examples: {
+ badge: '06 · O QUE DÁ PRA FAZER',
+ eyebrow: 'Three modalities, one CLI',
+ title: 'Imagem, vídeo e LLM com o mesmo padrão de chamada',
+ subtitle:
+ 'Async por padrão. URLs prontas em outputs[0]. Webhooks opcionais para fila longa.',
+ items: {
+ image: { label: 'Text → Image', prompt: 'cat astronaut, neon cyberpunk' },
+ video: { label: 'Image → Video', prompt: 'camera dolly in, soft lights' },
+ llm: { label: 'Chat → LLM', prompt: 'resuma em uma frase' },
+ },
+ chat: {
+ user: 'Resuma o WaveSpeedAI numa frase.',
+ assistant:
+ 'Plataforma única para imagem, vídeo, áudio e LLM em 700+ modelos.',
+ },
+ },
+ outro: {
+ badge: '07 · COMECE AGORA',
+ title: 'Instale agora — uma linha',
+ pills: [
+ 'github.com/wesleysimplicio/WaveSpeedAI-Skills',
+ 'MIT licensed',
+ 'agentskills.io spec',
+ ],
+ footer: 'One install. Every agent. Full WaveSpeedAI inference platform.',
+ },
+ },
+ en: {
+ intro: {
+ badge: '01 · INTRO',
+ subtitle: 'One install. Every agent. 700+ media + LLM models in a single CLI.',
+ },
+ whatIs: {
+ badge: '02 · WHAT IT IS',
+ eyebrow: 'The skill, in one sentence',
+ title: 'A manual that teaches your agent to call WaveSpeedAI',
+ subtitle:
+ 'The SKILL.md file becomes a native capability of your agent — short text, standardized frontmatter, ready-to-run commands.',
+ cards: {
+ media: {
+ title: '700+ media models',
+ description:
+ 'Z-Image, FLUX, Seedance, Kling, Veo, Luma, Wan, Qwen, Higgsfield, ace-step. Image, video, audio and avatars — all through one gateway.',
+ },
+ llm: {
+ title: '290+ OpenAI-compatible LLMs',
+ description:
+ 'Claude, GPT, Gemini, DeepSeek, Llama, Mistral, Qwen, xAI. Same key, same signature, no SDK swap per provider.',
+ },
+ cli: {
+ title: 'One CLI, any agent',
+ description:
+ 'The skill ships a single shell: wavespeed-cli. Async + automatic polling, upload, webhooks, batch jobs.',
+ },
+ },
+ },
+ install: {
+ badge: '03 · INSTALL',
+ eyebrow: 'One-line setup',
+ title: 'curl, install, configure — and the agent + CLI are ready',
+ subtitle:
+ 'The installer detects which agents you use and only copies SKILL.md where it makes sense.',
+ code: {
+ commentInstall: '# install on any known host',
+ commentClone: '# or clone locally',
+ commentAuth: '# authenticate and verify',
+ },
+ steps: [
+ { label: 'Provisions an isolated venv', detail: '~/.local/share/wavespeed-skill/venv' },
+ { label: 'Installs the Python SDK', detail: 'pip install wavespeed requests' },
+ { label: 'Drops the CLI shim', detail: '~/.local/bin/wavespeed-cli' },
+ {
+ label: 'Detects agents and copies SKILL.md',
+ detail: 'claude · codex · hermes · cursor · windsurf · openclaw',
+ },
+ ],
+ },
+ hosts: {
+ badge: '04 · WHERE IT RUNS',
+ eyebrow: 'Same skill, every agent',
+ title: 'Seven supported hosts — frontmatter per host',
+ subtitle:
+ 'The installer picks the right SKILL.md for each agent directory. You only run it once.',
+ },
+ cli: {
+ badge: '05 · CLI IN ACTION',
+ eyebrow: 'How the agent invokes the skill',
+ title: 'wavespeed-cli — single surface, predictable contracts',
+ subtitle:
+ 'Short commands, JSON outputs. The agent generates, waits and hands back ready URLs.',
+ codeTitle: 'terminal — text→image (Z-Image turbo)',
+ cards: [
+ { label: 'models', description: 'Live catalog of 700+ models.' },
+ { label: 'run', description: 'Async inference with automatic polling.' },
+ { label: 'submit', description: 'Fire-and-forget with a webhook callback.' },
+ { label: 'upload', description: 'Sends a local file, returns the hosted URL.' },
+ { label: 'llm', description: 'OpenAI-compatible chat for Claude, GPT, Gemini.' },
+ { label: 'verify-webhook', description: 'Validates the WaveSpeed HMAC-SHA256 callback.' },
+ ],
+ },
+ examples: {
+ badge: '06 · WHAT YOU CAN BUILD',
+ eyebrow: 'Three modalities, one CLI',
+ title: 'Image, video and LLM with the same call shape',
+ subtitle:
+ 'Async by default. Ready URLs at outputs[0]. Optional webhooks for long queues.',
+ items: {
+ image: { label: 'Text → Image', prompt: 'cat astronaut, neon cyberpunk' },
+ video: { label: 'Image → Video', prompt: 'camera dolly in, soft lights' },
+ llm: { label: 'Chat → LLM', prompt: 'summarize in one sentence' },
+ },
+ chat: {
+ user: 'Summarize WaveSpeedAI in one sentence.',
+ assistant: 'One platform for image, video, audio and LLM across 700+ models.',
+ },
+ },
+ outro: {
+ badge: '07 · GET STARTED',
+ title: 'Install now — one line',
+ pills: [
+ 'github.com/wesleysimplicio/WaveSpeedAI-Skills',
+ 'MIT licensed',
+ 'agentskills.io spec',
+ ],
+ footer: 'One install. Every agent. Full WaveSpeedAI inference platform.',
+ },
+ },
+};
+
+export const LocaleContext = React.createContext('pt-BR');
+
+export const useStrings = () => {
+ const locale = React.useContext(LocaleContext);
+ return STRINGS[locale];
+};
+
+export const useLocale = () => React.useContext(LocaleContext);
diff --git a/remotion-tutorial/src/tutorial/scenes/SceneCli.tsx b/remotion-tutorial/src/tutorial/scenes/SceneCli.tsx
new file mode 100644
index 0000000..cd72027
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/scenes/SceneCli.tsx
@@ -0,0 +1,125 @@
+import React from 'react';
+import {
+ AbsoluteFill,
+ spring,
+ useCurrentFrame,
+ useVideoConfig,
+ interpolate,
+} from 'remotion';
+import { SceneFrame } from '../components/SceneFrame';
+import { SceneTitle } from '../components/SceneTitle';
+import { CodeBlock } from '../components/CodeBlock';
+import { fonts, colors } from '../theme';
+import { useStrings } from '../i18n';
+
+const CARD_COLORS = [
+ colors.violetSoft,
+ colors.cyan,
+ colors.amber,
+ colors.emerald,
+ colors.rose,
+ colors.pink,
+];
+
+export const SceneCli: React.FC = () => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const t = useStrings().cli;
+
+ return (
+
+
+
+
+
+
+
+
+ {t.cards.map((cmd, i) => {
+ const enterFrame = 90 + i * 14;
+ const local = frame - enterFrame;
+ const enter = spring({
+ frame: local,
+ fps,
+ config: { damping: 18, stiffness: 130 },
+ });
+ const translate = interpolate(enter, [0, 1], [18, 0]);
+ const scale = interpolate(enter, [0, 1], [0.92, 1]);
+ const cardColor = CARD_COLORS[i % CARD_COLORS.length];
+ return (
+
+
+ wavespeed-cli {cmd.label}
+
+
+ {cmd.description}
+
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/scenes/SceneExamples.tsx b/remotion-tutorial/src/tutorial/scenes/SceneExamples.tsx
new file mode 100644
index 0000000..98e7656
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/scenes/SceneExamples.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { AbsoluteFill } from 'remotion';
+import { SceneFrame } from '../components/SceneFrame';
+import { SceneTitle } from '../components/SceneTitle';
+import { MediaPreview } from '../components/MediaPreview';
+import { colors } from '../theme';
+import { useStrings } from '../i18n';
+
+export const SceneExamples: React.FC = () => {
+ const t = useStrings().examples;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/scenes/SceneHosts.tsx b/remotion-tutorial/src/tutorial/scenes/SceneHosts.tsx
new file mode 100644
index 0000000..e3cecda
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/scenes/SceneHosts.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { AbsoluteFill } from 'remotion';
+import { SceneFrame } from '../components/SceneFrame';
+import { SceneTitle } from '../components/SceneTitle';
+import { HostBadge } from '../components/HostBadge';
+import { colors } from '../theme';
+import { useStrings } from '../i18n';
+
+const HOSTS = [
+ { name: 'Claude Code', monogram: 'C', path: '~/.claude/skills/wavespeed/SKILL.md', color: colors.violetSoft },
+ { name: 'Codex', monogram: 'CX', path: '~/.codex/skills/wavespeed/SKILL.md', color: colors.amber },
+ { name: 'Hermes', monogram: 'H', path: '~/.hermes/skills/creative/wavespeed/SKILL.md', color: colors.rose },
+ { name: 'OpenClaw', monogram: 'OC', path: '~/.openclaw/skills/wavespeed/SKILL.md', color: colors.pink },
+ { name: 'Cursor', monogram: 'CR', path: '~/.cursor/skills/wavespeed/SKILL.md', color: colors.cyan },
+ { name: 'Windsurf', monogram: 'W', path: '~/.windsurf/skills/wavespeed/SKILL.md', color: colors.blue },
+ { name: 'Generic', monogram: 'G', path: '~/.config/agent-skills/wavespeed/SKILL.md', color: colors.emerald },
+];
+
+export const SceneHosts: React.FC = () => {
+ const t = useStrings().hosts;
+ return (
+
+
+
+
+
+ {HOSTS.map((h, i) => (
+
+ ))}
+
+
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/scenes/SceneInstall.tsx b/remotion-tutorial/src/tutorial/scenes/SceneInstall.tsx
new file mode 100644
index 0000000..cde92a6
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/scenes/SceneInstall.tsx
@@ -0,0 +1,134 @@
+import React from 'react';
+import {
+ AbsoluteFill,
+ spring,
+ useCurrentFrame,
+ useVideoConfig,
+ interpolate,
+} from 'remotion';
+import { SceneFrame } from '../components/SceneFrame';
+import { SceneTitle } from '../components/SceneTitle';
+import { CodeBlock } from '../components/CodeBlock';
+import { fonts, colors } from '../theme';
+import { useStrings } from '../i18n';
+
+export const SceneInstall: React.FC = () => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const t = useStrings().install;
+
+ return (
+
+
+
+
+
+
+
+
+ {t.steps.map((step, i) => {
+ const enterFrame = 80 + i * 26;
+ const local = frame - enterFrame;
+ const enter = spring({
+ frame: local,
+ fps,
+ config: { damping: 18, stiffness: 130 },
+ });
+ const translate = interpolate(enter, [0, 1], [24, 0]);
+ return (
+
+
+
+
+ {step.label}
+
+
+ {step.detail}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+const CheckBadge: React.FC = () => (
+
+);
diff --git a/remotion-tutorial/src/tutorial/scenes/SceneIntro.tsx b/remotion-tutorial/src/tutorial/scenes/SceneIntro.tsx
new file mode 100644
index 0000000..2932176
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/scenes/SceneIntro.tsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import {
+ AbsoluteFill,
+ spring,
+ useCurrentFrame,
+ useVideoConfig,
+ interpolate,
+} from 'remotion';
+import { SceneFrame } from '../components/SceneFrame';
+import { Logo } from '../components/Logo';
+import { Chip } from '../components/Chip';
+import { fonts, colors, gradients } from '../theme';
+import { useStrings } from '../i18n';
+
+const HOSTS = [
+ { label: 'Claude', color: colors.violetSoft },
+ { label: 'Codex', color: colors.cyan },
+ { label: 'Hermes', color: colors.amber },
+ { label: 'OpenClaw', color: colors.rose },
+ { label: 'Cursor', color: colors.emerald },
+ { label: 'Windsurf', color: colors.blue },
+];
+
+export const SceneIntro: React.FC = () => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const t = useStrings().intro;
+
+ const titleSpring = spring({
+ frame: frame - 14,
+ fps,
+ config: { damping: 16, stiffness: 100 },
+ });
+ const subSpring = spring({
+ frame: frame - 28,
+ fps,
+ config: { damping: 22, stiffness: 110 },
+ });
+ const taglineSpring = spring({
+ frame: frame - 38,
+ fps,
+ config: { damping: 24, stiffness: 110 },
+ });
+
+ return (
+
+
+
+
+ WaveSpeedAI Skill
+
+
+ {t.subtitle}
+
+
+ {HOSTS.map((h, i) => (
+
+ ))}
+
+
+
+
+ );
+};
+
+const SignaturePulse: React.FC = () => {
+ const frame = useCurrentFrame();
+ const bars = 38;
+ return (
+
+ {Array.from({ length: bars }).map((_, i) => {
+ const t = frame * 0.18 + i * 0.45;
+ const h = 14 + Math.abs(Math.sin(t)) * 36 + Math.abs(Math.sin(t * 0.5)) * 8;
+ const opacity = 0.35 + Math.abs(Math.sin(t * 0.7)) * 0.6;
+ return (
+
+ );
+ })}
+
+ );
+};
diff --git a/remotion-tutorial/src/tutorial/scenes/SceneOutro.tsx b/remotion-tutorial/src/tutorial/scenes/SceneOutro.tsx
new file mode 100644
index 0000000..a870670
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/scenes/SceneOutro.tsx
@@ -0,0 +1,139 @@
+import React from 'react';
+import {
+ AbsoluteFill,
+ spring,
+ useCurrentFrame,
+ useVideoConfig,
+ interpolate,
+} from 'remotion';
+import { SceneFrame } from '../components/SceneFrame';
+import { Logo } from '../components/Logo';
+import { fonts, colors, gradients } from '../theme';
+import { useStrings } from '../i18n';
+
+const PILL_COLORS = [colors.violetSoft, colors.cyan, colors.amber];
+
+export const SceneOutro: React.FC = () => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const t = useStrings().outro;
+
+ const titleSpring = spring({
+ frame: frame - 8,
+ fps,
+ config: { damping: 16, stiffness: 110 },
+ });
+ const cmdSpring = spring({
+ frame: frame - 28,
+ fps,
+ config: { damping: 22, stiffness: 110 },
+ });
+ const linkSpring = spring({
+ frame: frame - 52,
+ fps,
+ config: { damping: 22, stiffness: 110 },
+ });
+
+ return (
+
+
+
+
+ {t.title}
+
+
+
+ $
+ bash <(curl -fsSL https://wavespeed.ai/install.sh)
+
+
+
+ {t.pills.map((label, i) => (
+
+ ))}
+
+
+
+ {t.footer}
+
+
+
+ );
+};
+
+const Pill: React.FC<{ label: string; color: string }> = ({ label, color }) => (
+
+ {label}
+
+);
diff --git a/remotion-tutorial/src/tutorial/scenes/SceneWhatIsSkill.tsx b/remotion-tutorial/src/tutorial/scenes/SceneWhatIsSkill.tsx
new file mode 100644
index 0000000..185c201
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/scenes/SceneWhatIsSkill.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { AbsoluteFill } from 'remotion';
+import { SceneFrame } from '../components/SceneFrame';
+import { SceneTitle } from '../components/SceneTitle';
+import { Card } from '../components/Card';
+import { colors } from '../theme';
+import { useStrings } from '../i18n';
+
+export const SceneWhatIsSkill: React.FC = () => {
+ const t = useStrings().whatIs;
+ return (
+
+
+
+
+
+ }
+ title={t.cards.media.title}
+ description={t.cards.media.description}
+ footer="wavespeed-ai/*"
+ width="100%"
+ />
+ }
+ title={t.cards.llm.title}
+ description={t.cards.llm.description}
+ footer="https://llm.wavespeed.ai/v1"
+ width="100%"
+ />
+ }
+ title={t.cards.cli.title}
+ description={t.cards.cli.description}
+ footer="~/.local/bin/wavespeed-cli"
+ width="100%"
+ />
+
+
+
+ );
+};
+
+const MediaIcon: React.FC = () => (
+
+
+
+
+
+);
+
+const ChatIcon: React.FC = () => (
+
+
+
+
+
+
+);
+
+const TerminalIcon: React.FC = () => (
+
+
+
+
+
+);
diff --git a/remotion-tutorial/src/tutorial/theme.ts b/remotion-tutorial/src/tutorial/theme.ts
new file mode 100644
index 0000000..0511028
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/theme.ts
@@ -0,0 +1,47 @@
+/**
+ * Font stacks. We rely on the system UI/mono fonts on purpose: the rendered
+ * frames look great on every modern OS without paying the network cost (and
+ * cert/CDN fragility) of fetching webfonts during headless rendering.
+ *
+ * To switch to a real webfont (e.g. Inter / JetBrains Mono) install
+ * `@remotion/google-fonts` and replace these strings with `loadFont().fontFamily`.
+ */
+export const fonts = {
+ sans: '"Inter", "SF Pro Display", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif',
+ mono: '"JetBrains Mono", "Fira Code", "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace',
+};
+
+export const colors = {
+ bgDeep: '#05060f',
+ bg: '#0a0b1e',
+ bgSoft: '#13153a',
+ surface: 'rgba(24, 27, 64, 0.72)',
+ surfaceSolid: '#181b40',
+ border: 'rgba(139, 92, 246, 0.28)',
+ borderSoft: 'rgba(255,255,255,0.08)',
+ textPrimary: '#f7f7ff',
+ textSecondary: '#cdd0ee',
+ textMuted: '#888ab2',
+ violet: '#8b5cf6',
+ violetSoft: '#a78bfa',
+ violetDeep: '#6d28d9',
+ cyan: '#22d3ee',
+ cyanSoft: '#67e8f9',
+ emerald: '#34d399',
+ amber: '#fbbf24',
+ rose: '#fb7185',
+ pink: '#f472b6',
+ blue: '#60a5fa',
+};
+
+export const gradients = {
+ brand: `linear-gradient(135deg, ${colors.violet} 0%, ${colors.cyan} 100%)`,
+ brandSoft: `linear-gradient(135deg, rgba(139,92,246,0.45) 0%, rgba(34,211,238,0.45) 100%)`,
+ warm: `linear-gradient(135deg, ${colors.amber} 0%, ${colors.rose} 100%)`,
+ panel: `linear-gradient(180deg, rgba(24,27,64,0.85) 0%, rgba(11,12,30,0.92) 100%)`,
+};
+
+export const shadows = {
+ card: '0 24px 60px -20px rgba(8, 8, 30, 0.85), 0 0 0 1px rgba(139, 92, 246, 0.18)',
+ glow: '0 0 60px rgba(139, 92, 246, 0.35)',
+};
diff --git a/remotion-tutorial/src/tutorial/timing.ts b/remotion-tutorial/src/tutorial/timing.ts
new file mode 100644
index 0000000..3aaea5a
--- /dev/null
+++ b/remotion-tutorial/src/tutorial/timing.ts
@@ -0,0 +1,31 @@
+export const TUTORIAL_FPS = 30;
+
+export type SceneSpec = {
+ id: string;
+ durationInFrames: number;
+};
+
+export const SCENES = [
+ { id: 'intro', durationInFrames: 150 },
+ { id: 'what-is-skill', durationInFrames: 270 },
+ { id: 'install', durationInFrames: 300 },
+ { id: 'hosts', durationInFrames: 240 },
+ { id: 'cli', durationInFrames: 360 },
+ { id: 'examples', durationInFrames: 300 },
+ { id: 'outro', durationInFrames: 180 },
+] as const satisfies readonly SceneSpec[];
+
+export const TUTORIAL_DURATION_FRAMES = SCENES.reduce(
+ (acc, scene) => acc + scene.durationInFrames,
+ 0,
+);
+
+export const SCENE_OFFSETS: Record = (() => {
+ const offsets: Record = {};
+ let acc = 0;
+ for (const scene of SCENES) {
+ offsets[scene.id] = acc;
+ acc += scene.durationInFrames;
+ }
+ return offsets;
+})();
diff --git a/remotion-tutorial/tsconfig.json b/remotion-tutorial/tsconfig.json
new file mode 100644
index 0000000..b94720b
--- /dev/null
+++ b/remotion-tutorial/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ "noEmit": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["src", "remotion.config.ts"]
+}