diff --git a/.babelrc b/.babelrc index ef78a94..c0ac5de 100644 --- a/.babelrc +++ b/.babelrc @@ -3,14 +3,9 @@ "env": { "test": { "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": "current" - } - } - ] + ["@babel/preset-env", { "targets": { "node": "current" } }], + ["@babel/preset-react", { "runtime": "automatic" }], + "@babel/preset-typescript" ] } } diff --git a/.claude/skills/implement-issue/SKILL.md b/.claude/skills/implement-issue/SKILL.md new file mode 100644 index 0000000..d106acd --- /dev/null +++ b/.claude/skills/implement-issue/SKILL.md @@ -0,0 +1,436 @@ +--- +name: implement-issue +description: Implements a code change from an issue prompt. Checks inventory relevance, ensures correct branch, derives acceptance criteria, writes tests per project standards, commits atomically with semantic messages, updates CLAUDE.md/README.md/inventory on completion, and only marks done when all tests pass. +argument-hint: [issue text or path to issue file] +allowed-tools: Read, Edit, Write, Bash, Glob, Grep, TodoWrite +--- + +You are implementing a code change described in an issue or prompt. Follow every step in order. Do not skip steps or combine commits. + +## Arguments + +`$ARGUMENTS` contains either: +- Raw issue text (title, description, acceptance criteria) +- A file path to an issue/doc (read it with Read) +- A plain description of the change to make + +If no arguments are given, ask the user what they want to implement before proceeding. + +--- + +## Step 0 — Parse the issue + +Extract the following from `$ARGUMENTS`: + +| Field | Where to find it | Fallback | +|-------|-----------------|----------| +| **Title** | First heading or first sentence | Ask the user | +| **Branch name** | Explicit branch field in issue, or derive from title | See naming rules below | +| **Description** | Body of the issue | The full prompt | +| **Acceptance criteria** | "Acceptance Criteria" / "AC" / "Definition of Done" section | Derive them yourself (Step 2) | +| **Affected files / scope** | "Files", "Scope", or "Touches" section | Identify from description | +| **Test requirements** | "Testing" section | Follow project defaults | + +**Branch naming rules** (in priority order): +1. Use the branch name explicitly stated in the issue (e.g. `refactor/p0-project-file`) +2. Derive from the issue title: lowercase, hyphens, prefixed with type: + - New feature → `feat/` + - Bug fix → `fix/` + - Refactor → `refactor/` + - Infrastructure / tooling → `chore/` + - Docs only → `docs/` + - Keep slug under 40 characters; drop articles and filler words + +--- + +## Step 0.5 — Inventory relevance check + +Read `docs/REFACTOR_ISSUE_INVENTORY.md` and check whether the issue title or description matches any item in that file. + +**If NOT found in the inventory:** skip this step entirely and continue to Step 1. + +**If found in the inventory**, do the following before writing any code: + +### 0.5a — Locate the issue + +Find the matching issue by number and title. Note its sprint and position. + +### 0.5b — Check for superseding or blocking issues + +Read the issues immediately around it (same sprint and adjacent sprints). Ask: + +1. **Is this issue already superseded?** — does a later issue in the same sprint or a subsequent sprint render this one obsolete or absorbed into a larger change? +2. **Is this issue blocked?** — does it depend on another issue that has not yet been implemented? (Look for dependency language: "consumes", "reads from", "requires", "after X is done".) +3. **Does this issue conflict with an already-completed issue?** — would implementing it undo or contradict work already merged? + +To check what has already been done, run: +```bash +git log --oneline --all | head -40 +``` +Also grep the inventory for any `✅` markers from previous runs of this skill. + +### 0.5c — Relevance decision + +Based on 0.5b, make one of three calls: + +| Verdict | Condition | Action | +|---------|-----------|--------| +| **Relevant — proceed** | No superseding, no conflicts, dependencies met | Continue to Step 1 | +| **Blocked — pause** | A dependency issue is not yet implemented | Report which dependency is missing; ask the developer whether to implement the dependency first or proceed anyway | +| **Stale — flag and stop** | The issue is superseded, absorbed, or would conflict with existing work | Report the exact reason with references to which commits or issues made it stale; do NOT implement; ask the developer to remove or update the inventory entry | + +If the verdict is **Stale**, output a clear flag: + +``` +⚠️ STALE ISSUE DETECTED +Issue: # +Reason: <one sentence> +Evidence: <commit hash or inventory issue # that supersedes it> +Recommendation: remove or update this entry in docs/REFACTOR_ISSUE_INVENTORY.md +``` + +Then stop. Do not proceed to Step 1 without developer confirmation. + +--- + +## Step 1 — Branch setup + +Run these checks in order. Stop and report if anything is unexpected. + +```bash +# 1a. What branch am I on? +git branch --show-current + +# 1b. What is the default branch? (usually main) +git remote show origin | grep 'HEAD branch' +``` + +**If currently on main (or the default branch):** +```bash +git pull origin main +git checkout -b <branch-name> +``` + +**If currently on an existing feature branch that matches the target branch name:** +- Verify it is branched from a recent commit of main: +```bash +git merge-base --is-ancestor $(git rev-parse origin/main) HEAD || echo "BEHIND MAIN" +``` +- If behind, rebase: `git fetch origin && git rebase origin/main` + +**If currently on an unrelated branch:** +- Do NOT check out main and trash the unrelated work. +- Report to the user: "Currently on branch X which does not match the target branch Y. Please confirm you want to switch." +- Wait for confirmation before proceeding. + +**Verify the final state before continuing:** +```bash +git status && git log --oneline -5 +``` + +--- + +## Step 2 — Acceptance criteria + +If the issue already has explicit acceptance criteria, list them verbatim. + +If acceptance criteria are missing or vague, **derive them yourself** from the description. Write them as a numbered checklist of verifiable, binary outcomes. Each criterion must be falsifiable — "the code works" is not acceptable; "running X produces output Y" is. + +Example format: +``` +Acceptance Criteria (derived): +1. `scripts/config/project.ts` exports a `readProject()` function that returns typed `ProjectConfig`. +2. `readProject()` throws a typed `ProjectNotFoundError` when `.ragtech/project.json` is absent. +3. `npm test` passes with ≥ 1 unit test per exported function. +4. `tsc --noEmit` passes with no errors. +``` + +Write the acceptance criteria to a TodoWrite task list so you can track them as you go. + +--- + +## Step 3 — Implementation plan + +Before writing any code, plan the atomic commits you will make. Each commit should: +- Change one logical unit (one module, one type, one feature boundary) +- Leave the repo in a passing-tests state +- Have a semantic commit message (see Step 6 for format) + +Output the commit plan as a numbered list. Example: +``` +Commit plan: +1. feat(config): add ProjectConfig type and schema +2. feat(config): implement readProject() with error handling +3. test(config): unit tests for readProject() +4. chore(config): wire project.ts into existing entry points +``` + +Use TodoWrite to track each commit as a task. + +--- + +## Step 4 — Read testing standards + +Before writing a single test, re-read the relevant sections of `docs/TESTING_STANDARDS.md`. Key decisions: + +| Question | Answer from standards | +|----------|-----------------------| +| Is the new function pure (no I/O)? | Unit test in same directory, `.test.ts` suffix | +| Does it touch the filesystem or spawn processes? | Integration test in `tests/integration/` | +| Does it require a real browser? | E2E test in `e2e/` (only for Phase 8 UI features) | +| Does it involve React? | Jest `react` project, `.test.tsx`, use `@testing-library/react` | +| Does it use Remotion hooks? | Mock `remotion` module per-test | +| Does it need >2 mocked dependencies? | Integration test, not unit test | + +Identify which test types are needed for this issue and note them. + +--- + +## Step 5 — Implement each commit + +For each commit in your plan: + +### 5a. Write the test(s) first (or alongside the implementation) + +- Follow the exact file locations from `docs/TESTING_STANDARDS.md` +- Use the patterns from the standards doc (dependency injection, pure function tests, render-and-assert, page object model) +- Every new pure function: at least one happy-path test + one edge-case test +- Every new React component: at least one smoke render test +- Run only the related tests to confirm they fail correctly before implementing: +```bash +npx jest --testPathPattern="<test-file>" --no-coverage +``` + +### 5b. Write the implementation + +- Stay within the scope of this commit — do not touch unrelated files +- No hardcoded paths; use shared path helpers from `scripts/config/paths.ts` if they exist +- No new duplicated timing constants in `remotion/` — use the constants table in CLAUDE.md +- Follow existing code style in the file being edited + +### 5c. Run the tests + +```bash +# Run related tests +npx jest --testPathPattern="<test-file>" --no-coverage + +# If React component: +npm run test:react -- --testPathPattern="<test-file>" + +# If integration: +npm run test:integration -- --testPathPattern="<test-file>" +``` + +Fix any failures before proceeding to commit. Do not commit with a known failing test. + +### 5d. Type-check (if TypeScript files changed) + +```bash +npx tsc --noEmit +``` + +Fix all type errors before committing. + +### 5e. Commit + +```bash +git add <specific-files> # never git add -A +git commit -m "$(cat <<'EOF' +<type>(<scope>): <short imperative summary under 72 chars> + +<Blank line> +<Body: 2–5 lines explaining WHY this change exists, what problem it solves, +and any non-obvious decisions. Any future agent reading git log should +understand the intent without opening the files.> + +<Optional: list the acceptance criteria this commit satisfies> +AC: #1, #2 +EOF +)" +``` + +**Commit type prefixes:** + +| Prefix | Use when | +|--------|----------| +| `feat` | New production functionality | +| `fix` | Bug correction | +| `test` | Adding or fixing tests only | +| `refactor` | Code restructured, no behaviour change | +| `chore` | Build, tooling, dependencies, config | +| `docs` | Documentation only | +| `perf` | Performance improvement | + +**Scope** = the module or directory being changed (e.g. `config`, `camera`, `segmentPlayer`, `dag`, `editor`). + +Repeat Step 5 for every commit in your plan. + +--- + +## Step 6 — Full test suite + +After all commits are made, run the complete test suite: + +```bash +npm test +``` + +If any test fails: +- Fix the failure +- Add a new commit (`fix(<scope>): <what broke and why>`) +- Re-run until clean + +If the issue touched React components or app routes, also run: +```bash +npm run test:react +``` + +If the issue is Phase 8 or involves user-facing browser flows, also run: +```bash +npm run test:e2e +``` + +Do not mark the work complete until `npm test` exits with code 0. + +--- + +## Step 7 — Acceptance criteria verification + +Go through each acceptance criterion from Step 2. For each one, run the exact command or check the exact condition that proves it is met. Record the result: + +``` +AC #1 — PASS: readProject() exported; tsc --noEmit clean +AC #2 — PASS: throws ProjectNotFoundError when file missing (verified by test) +AC #3 — PASS: npm test passes, 3 unit tests written +AC #4 — PASS: tsc --noEmit passes +``` + +If any criterion is not met, implement what is missing and add a commit before reporting done. + +--- + +## Step 8 — Update living documentation + +Once all acceptance criteria pass, update the three living docs to reflect the completed work. Each update is its own commit if it touches real content; skip a doc if there is genuinely nothing new to record. + +### 8a — Mark the issue complete in the inventory + +Open `docs/REFACTOR_ISSUE_INVENTORY.md`. Find the matching issue heading. Append a `✅ Done` badge and a one-line note on the branch/PR, directly under the heading: + +```markdown +### 1. Create project file configuration layer +✅ Done — `refactor/p0-project-file` — readProject/writeProject helpers, ProjectConfig type +``` + +Do not change the issue text or numbering. If a dependent issue is now unblocked, add a `> Unblocks: #N` note on the same line. + +Commit: +``` +docs(inventory): mark issue #<N> complete +``` + +### 8b — Update CLAUDE.md + +Read `CLAUDE.md` and identify which sections are affected by the work just done. Update only what changed — do not rewrite sections that are still accurate. Common updates: + +| What changed | Where in CLAUDE.md | +|---|---| +| New file created | Add a row to **Key Source Files** table | +| New constant introduced | Add a row to **Common Constants** table | +| Phase branch now active / done | Update the **Phase map** row | +| New script command added | Update the relevant pipeline section | +| Known bug fixed | Remove or update the **Known correctness bugs** list item | +| New target directory created | Update **Target directory additions** tree | + +Do not add information that duplicates what the code already makes obvious. Only record non-obvious facts that a future agent or developer would not discover by reading the files. + +Commit: +``` +docs(claude-md): reflect changes from issue #<N> +``` + +### 8c — Update README.md + +Read `README.md`. If the completed work adds or changes any user-facing behaviour, update the relevant section. Common updates: + +| What changed | Where in README.md | +|---|---| +| New `npm run` command | Add a row to the relevant command table | +| Changed prerequisite | Update **Prerequisites** | +| New wizard mode or step | Update the **Wizard modes** table or the numbered walkthrough | +| New output file or directory | Update the relevant step description | +| Removed or renamed command | Delete/update the old entry | + +Do not add developer-internal details to README.md — it is user-facing. If nothing user-facing changed, skip this commit entirely. + +Commit: +``` +docs(readme): reflect changes from issue #<N> +``` + +### 8d — Update env example files + +If the implementation introduces, renames, or removes any environment variable, update every `.env.example` / `.env.sample` / `.env.template` file in the repo to match. Run: + +```bash +find . -name ".env.example" -o -name ".env.sample" -o -name ".env.template" | grep -v node_modules +``` + +For each file found: +- Add any new variable with a blank or placeholder value and a one-line comment explaining what it is +- Remove or rename any variable that no longer exists +- Never write a real secret or credential into an example file + +If no environment variables changed, skip this step entirely. + +Commit: +``` +chore(env): update example env for issue #<N> +``` + +--- + +## Step 9 — Report + +Summarise the work: + +1. **Branch:** the branch name +2. **Commits:** list each commit hash + message (from `git log --oneline`) +3. **Tests added:** count and type (unit / integration / e2e) +4. **Acceptance criteria:** all passed / any outstanding +5. **Docs updated:** which of inventory / CLAUDE.md / README.md were changed and why; which were skipped and why +6. **Next step:** what the human needs to do (e.g. "review and push", "run Remotion studio to verify frames") + +Keep it under 20 lines. Do not repeat code that is already visible in the diff. + +--- + +## Guardrails + +- **Never** commit to `main` or the default branch directly. +- **Never** use `git add -A` or `git add .` — always name specific files. +- **Never** use `--no-verify` to skip hooks. +- **Never** mark a task done if `npm test` is failing. +- **Never** combine two logical changes into one commit; isolation enables partial recovery. +- **Never** write a commit message that describes WHAT the code does instead of WHY it exists. +- If a pre-commit hook fails, fix the underlying issue and create a new commit — do NOT amend. + +### .gitignore + +If the implementation creates new output directories, build artefacts, temp files, secrets, or generated files that should not be tracked, update `.gitignore` in the same commit that introduces the pattern. Do not leave untracked noise for the developer to clean up. + +### npm vulnerabilities + +The pre-push hook runs `npm audit --audit-level=moderate` and blocks the push if any installed package has a moderate, high, or critical vulnerability. If a dependency you introduced or updated triggers this: + +1. Run `npm audit fix` — applies safe, semver-compatible fixes automatically +2. If `npm audit fix` cannot resolve it (breaking-change required), pin to the last safe version or find an alternative package +3. Never use `--no-verify` to bypass this check — a known-vulnerable dependency in remote is worse than a blocked push + +Low-severity advisories are informational only and do not block the push. + +### Large files in `public/` + +Video editing produces large binary files under `public/` (raw video, synced output, transcription models, audio, rendered MP4s). These **may** be committed to a local feature branch as work-in-progress checkpoints — that is intentional and supported. They must **never** be pushed to the remote. + +The pre-push hook enforces this. Do not bypass it (`--no-verify`). If a push is blocked because `public/` contains large files, that is correct behaviour — remove those files from the commit or move them to a `.gitignore`d path before pushing code changes. diff --git a/.claude/skills/pre-push-audit/SKILL.md b/.claude/skills/pre-push-audit/SKILL.md new file mode 100644 index 0000000..d0afd8d --- /dev/null +++ b/.claude/skills/pre-push-audit/SKILL.md @@ -0,0 +1,502 @@ +--- +name: pre-push-audit +description: Pre-push audit gate for agents. Checks branch is rebased on latest main, validates semantic commit conventions, audits test coverage for all changed files, identifies functionality requiring human manual testing, blocks the push until the developer confirms they have tested those flows, and generates a PR template if no PR exists yet. +argument-hint: [optional: remote and branch, default "origin main"] +allowed-tools: Bash, Read, Grep, Glob, TodoWrite, AskUserQuestion +--- + +You are a pre-push audit gate. An agent is about to push commits to remote. Your job is to validate that the push is safe: the branch is current, every commit message follows the project convention, all automatable tests exist and pass, and anything that requires human eyeballs has been verified by a human. Do not push anything yourself — you are a gate, not a pusher. + +Run every step in order. Stop and report if a step fails. Do not skip steps. + +--- + +## Step 0 — Scope + +```bash +# What branch are we on? +git branch --show-current + +# What commits will be pushed (not yet on origin)? +git fetch origin +git log --oneline origin/main..HEAD +``` + +Capture the branch name and the list of commits. If `git log origin/main..HEAD` is empty, there is nothing to push — report "Nothing to push" and stop. + +Record the push target from `$ARGUMENTS`. Default to `origin main` if not given. + +--- + +## Step 1 — Rebase check + +Verify the branch is rebased on the latest `origin/main`. A non-rebased branch produces merge commits and can introduce conflicts downstream. + +```bash +# Is origin/main an ancestor of HEAD? +git merge-base --is-ancestor origin/main HEAD && echo "REBASED" || echo "BEHIND" +``` + +**If output is `BEHIND`:** + +```bash +# Attempt rebase +git rebase origin/main +``` + +If the rebase succeeds cleanly, continue. If it hits a conflict: +- Report the conflict details to the user +- Do NOT resolve conflicts automatically +- **BLOCK the push** with: + ``` + ✗ PUSH BLOCKED — rebase conflict + Resolve the conflict, then re-run /pre-push-audit. + ``` + Then stop. + +**If output is `REBASED`:** continue to Step 2. + +--- + +## Step 2 — Semantic commit convention check + +Every commit being pushed must follow the project's semantic commit format. Reviewers and the git log both depend on this — a malformed message makes the history unsearchable and breaks the inventory update convention in `implement-issue`. + +### 2a — Collect all commit messages + +```bash +# Full subject line (first line) for every commit not yet on origin/main +git log --format="%H|||%s" origin/main..HEAD +``` + +### 2b — Validate each subject line + +Each subject line must match **all** of the following rules: + +| Rule | Pattern / constraint | +|------|----------------------| +| **Type prefix** | Must start with one of: `feat`, `fix`, `test`, `refactor`, `chore`, `docs`, `perf` | +| **Scope** | Optional; if present must be `(<scope>)` — lowercase, alphanumeric, hyphens or slashes only | +| **Separator** | Immediately after the type/scope, a colon and a single space: `: ` | +| **Subject** | Imperative mood, no trailing period, ≤ 72 characters total (including type/scope) | +| **No merge commits** | `Merge branch ...` or `Merge pull request ...` are always violations | + +Regex (for reference): `^(feat|fix|test|refactor|chore|docs|perf)(\([a-z0-9/_-]+\))?: .{1,60}[^.]$` + +### 2c — Report violations + +For each non-conforming commit, print: + +``` +✗ Bad commit message: + SHA: abc1234 + Message: "add stuff to the editor" + Problem: missing type prefix — must start with feat|fix|test|refactor|chore|docs|perf + +✗ Bad commit message: + SHA: def5678 + Message: "feat(Editor): Updated the timeline component." + Problems: + - scope must be lowercase ("Editor" → "editor") + - trailing period not allowed +``` + +### 2d — Block or continue + +**If there are violations:** + +Print a remediation guide: + +``` +To fix a commit message, use git rebase to reword it: + + # For the most recent commit only: + git commit --amend --no-edit -m "fix(editor): correct description here" + + # For older commits, identify the parent SHA of the earliest bad commit, then: + GIT_SEQUENCE_EDITOR="sed -i 's/^pick <sha>/reword <sha>/'" git rebase -i <parent-sha> + # Then edit the message in the editor that opens. + +After rewording, re-run /pre-push-audit. +``` + +**BLOCK the push.** Do not proceed to Step 3 until all commit messages are valid. + +**If all messages are valid:** print a summary and continue to Step 3. + +``` +✓ Commit messages: N commit(s) — all conform to semantic convention +``` + +--- + +## Step 3 — Identify changed files + +```bash +# All files changed relative to origin/main +git diff --name-only origin/main...HEAD +``` + +Partition the changed files into these buckets. A file can appear in multiple buckets. + +| Bucket | Pattern | +|--------|---------| +| **Scripts / pipeline logic** | `scripts/**/*.{ts,js}` (excluding `*.test.*`) | +| **Remotion lib (pure logic)** | `remotion/lib/**/*.{ts,tsx}` (excluding `*.test.*`) | +| **Remotion components (visual)** | `remotion/components/**/*.{ts,tsx}` | +| **Remotion composition** | `remotion/Composition.tsx` | +| **App components (UI)** | `app/**/*.{ts,tsx}` (excluding `*.test.*`, `api/`) | +| **App API routes** | `app/api/**/*.{ts,tsx}` | +| **Test files** | `**/*.test.{ts,tsx,js}`, `e2e/**/*.test.ts` | +| **Config / tooling** | `*.config.*`, `.husky/**`, `package.json`, `tsconfig*.json` | +| **Docs** | `docs/**`, `*.md` | +| **Assets / data** | `public/**`, `brands/**` | + +Record the bucket membership for each file. You will use this in Steps 4 and 5. + +--- + +## Step 4 — Test coverage audit + +For each non-test source file in the changed set, determine whether an adequate test exists. Follow the rules from `docs/TESTING_STANDARDS.md` exactly. + +### 4a — Locate existing test files + +For each changed source file `path/to/foo.ts`, check whether any of these test files exist: + +```bash +# Unit test next to the file +ls path/to/foo.test.ts 2>/dev/null || echo "MISSING" +ls path/to/foo.test.tsx 2>/dev/null || echo "MISSING" + +# For scripts/ files, also check __tests__/ sibling +ls path/to/__tests__/foo.test.js 2>/dev/null || echo "MISSING" + +# For app/ components, check the same directory +ls path/to/foo.test.tsx 2>/dev/null || echo "MISSING" +``` + +For integration-worthy modules (files that do real I/O, spawn processes, or require >2 mocked deps), also check: + +```bash +ls tests/integration/ 2>/dev/null +``` + +### 4b — Classify each gap + +For every source file with a missing test, classify it using this decision table: + +| Condition | Required test type | Location | +|-----------|--------------------|----------| +| File exports pure functions (no I/O, no side effects) | Unit test | Next to the file | +| File does real filesystem I/O or spawns processes | Integration test | `tests/integration/` | +| File is a React component | Smoke render test | Next to the file (react project) | +| File is an app API route | Unit or integration test | Next to or in `tests/integration/` | +| File is a Remotion component | Smoke render test (mock Remotion hooks) | Next to the file (react project) | +| File is a pure Remotion lib function | Unit test | Next to the file | +| File is config / tooling / docs / assets only | No test required | — | + +Produce a gap table: + +``` +Test coverage gaps: +┌─────────────────────────────────────────────┬──────────────────┬───────────────────────────────────────┐ +│ File │ Missing test type│ Where to add it │ +├─────────────────────────────────────────────┼──────────────────┼───────────────────────────────────────┤ +│ scripts/config/project.ts │ Unit │ scripts/config/project.test.ts │ +│ app/components/EpisodePill.tsx │ Smoke render │ app/components/EpisodePill.test.tsx │ +└─────────────────────────────────────────────┴──────────────────┴───────────────────────────────────────┘ +``` + +If there are **no gaps**, print "All changed source files have corresponding tests." and skip to Step 4c. + +If there are gaps, **write the missing tests now** before continuing. Follow `docs/TESTING_STANDARDS.md` patterns exactly: +- Pure function → dependency-injection unit test +- React component → `render(...)` smoke test (using `@testing-library/react`) +- Remotion component → mock `remotion` module, then render +- Integration → temp dir + real pipeline stage + +After writing each test file, add a commit: + +```bash +git add <test-file> +git commit -m "$(cat <<'EOF' +test(<scope>): add missing tests for <subject> + +Pre-push audit identified these tests were absent. Added: +- <brief list of what each test covers> +EOF +)" +``` + +### 4c — Run the full test suite + +```bash +npm test +``` + +If tests fail: +- Fix the failure +- Add a fix commit +- Re-run until clean + +If the changed files include React components or app routes, also run: + +```bash +npm run test:react +``` + +If the changes touch any Phase 8 user-facing browser flows, also run: + +```bash +npm run test:e2e +``` + +**BLOCK the push if any test runner exits non-zero.** Report which suite failed and why. + +--- + +## Step 5 — Human testing gate + +Some changes cannot be validated by automated tests and require human eyes. Identify all such changes from the buckets in Step 3. + +### 5a — Classify manual testing requirements + +Build a checklist of manual checks required. Use this table: + +| Bucket | Always requires human testing? | Specific manual check | +|--------|--------------------------------|-----------------------| +| Remotion components (visual) | **Yes** | Run `remotion studio`, scrub through affected frames, verify visual output matches intent | +| Remotion composition | **Yes** | Verify composition duration, hook timing, section boundaries in studio | +| App components (UI) | **Yes** — unless purely logic changes | `npm run dev`, navigate to the affected route, exercise the interaction | +| App API routes | No (automated tests sufficient) | — | +| Scripts / pipeline logic | Only if it changes output format or video timing | Run the affected pipeline stage on real data and inspect output | +| Short-form pipeline | **Yes** | Run `npm run shorts:wizard`, verify clip output | +| Brand / assets | **Yes** | Visual inspection in Remotion studio and/or browser | +| Camera profiles | **Yes** | Verify face boxes and angle switching in camera GUI (`/camera`) | +| Config / tooling / docs | No | — | + +### 5b — Produce the manual testing checklist + +Format: + +``` +Manual testing required before push: + +□ [REMOTION-VISUAL] Open Remotion Studio (`npx remotion studio`) and scrub through + frames 0–600 on ragTechVodcast. Verify hook section renders correctly. + Affected: remotion/components/HookOverlay.tsx + +□ [UI-BROWSER] Start dev server (`npm run dev`), open /editor, make a cut, save. + Verify the cut is reflected in the transcript panel without a page reload. + Affected: app/editor/page.tsx, app/editor/Timeline.tsx + +□ [PIPELINE] Run `node scripts/wizard.js` and select the edit-transcript step. + Inspect the output transcript.doc.txt — verify sentence merging is unchanged. + Affected: scripts/edit-transcript.js +``` + +Save this checklist — it feeds directly into the PR template in Step 7. + +If **no manual testing is required** (only config, docs, assets, or API routes changed), print "No manual testing required." and skip to Step 6. + +### 5c — Block and wait for human confirmation + +**Stop here.** Do NOT push yet. + +Present the checklist to the developer and ask for confirmation using the `AskUserQuestion` tool: + +``` +Pre-push audit: manual testing required + +The following changes need human verification before the push proceeds: + +[paste the manual testing checklist from 5b] + +Have you completed all of the above checks? Reply YES to proceed with the push, or NO to abort. +``` + +**If the developer replies NO or anything other than YES (case-insensitive):** + +``` +✗ PUSH BLOCKED — manual testing not confirmed. +Complete the checks above, then re-run /pre-push-audit. +``` + +Stop. Do not push. + +**If the developer replies YES:** continue to Step 6. + +--- + +## Step 6 — Final scan + +Run the pre-push hook checks that are cheap to re-verify: + +```bash +# No debugger statements +grep -rn 'debugger' \ + --include='*.ts' --include='*.tsx' --include='*.js' \ + --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=coverage \ + --exclude-dir=e2e \ + . && echo "FOUND" || echo "CLEAN" + +# No .only() in test files +grep -rn '\.\bonly\b\s*(' \ + --include='*.test.ts' --include='*.test.tsx' --include='*.test.js' \ + --exclude-dir=node_modules \ + . && echo "FOUND" || echo "CLEAN" + +# No large files in public/ staged for push +git diff --name-only origin/main...HEAD | grep '^public/' | while read f; do + size=$(stat -f%z "$f" 2>/dev/null || stat -c%s "$f" 2>/dev/null) + [ "$size" -gt 52428800 ] && echo "LARGE: $f ($size bytes)" +done +``` + +If any of these fail, **block the push** with the specific reason. + +--- + +## Step 7 — Clearance report + +All checks have passed. Print the clearance report: + +``` +✓ PRE-PUSH AUDIT PASSED + +Branch: <branch-name> +Rebased on: origin/main @ <short-sha> +Commits: <N> commit(s) — all messages conform to semantic convention +Tests: All suites green (npm test, npm run test:react if applicable) +Coverage: <N> test files added or already present +Manual QA: Confirmed by developer (or: not required) +``` + +Then output the exact push command for the agent to run. Do not run it yourself. + +``` +Cleared to push: git push <remote> <branch> +``` + +Continue to Step 8 before the agent runs that command. + +--- + +## Step 8 — PR template + +Check whether a pull request already exists for this branch: + +```bash +gh pr list --head "$(git branch --show-current)" --json number,title,url --state open +``` + +**If an open PR exists:** print the PR URL and skip the rest of Step 8. + +``` +PR already open: <url> — no template needed. +``` + +**If no PR exists:** generate a PR template the agent or developer can use to open one. The template must be specific to the actual changes — do not use placeholder language. + +Construct the template as follows: + +### 8a — Summary section + +Use the commit messages from Step 2 to derive 2–4 bullet points summarising what changed and why. Each bullet should explain the *effect*, not just restate the commit message. + +Example: +``` +## Summary +- Consolidated `hookClipEnd()` into a single `remotion/lib/hookTiming.ts` — previously four separate + implementations could disagree by 1–3 frames, causing hook sections to cut early in some renders. +- Added unit tests covering the bounded and unbounded hook timing paths. +``` + +### 8b — How to review section + +List the key files changed (from Step 3 buckets) and what a reviewer should focus on in each: + +``` +## How to review +- `remotion/lib/hookTiming.ts` — new canonical implementation; verify the bounded/unbounded + logic matches the intent in CLAUDE.md and that constants match the table there. +- `remotion/components/SegmentPlayer.tsx` — now imports from hookTiming.ts; diff should show + only the import change and deletion of the old inline function. +- `remotion/lib/hookTiming.test.ts` — confirm tests cover the edge cases noted in CLAUDE.md. +``` + +### 8c — Test plan section + +Combine the automated test results and the manual testing checklist from Step 5b: + +``` +## Test plan +- [ ] `npm test` passes (all Jest suites) +- [ ] `npm run test:react` passes (if React components changed) +- [ ] `npm run test:e2e` passes (if UI flows changed) + +Manual verification required: +- [ ] [REMOTION-VISUAL] Open Remotion Studio and scrub through frames 0–600 on ragTechVodcast. + Hook section must not cut early compared to the baseline in docs/render-baselines/. +- [ ] [UI-BROWSER] Navigate to /editor, make a cut, confirm timeline updates correctly. +``` + +If no manual testing was required (from Step 5), omit the "Manual verification required" subsection. + +### 8d — Output the complete template + +Print the full PR body so the agent or developer can copy it: + +``` +------- PR TEMPLATE (copy below this line) ------- + +## Summary +<derived bullets from 8a> + +## How to review +<file-by-file notes from 8b> + +## Test plan +<checklist from 8c> + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +------- (end of template) ------- +``` + +Then ask the developer whether to open the PR now using the `AskUserQuestion` tool: + +``` +A PR template has been generated above. + +Would you like me to open the PR now using `gh pr create`? +Reply YES to open it, or NO to copy the template manually. +``` + +**If YES:** run: + +```bash +gh pr create \ + --title "<type>(<scope>): <subject from the first or most significant commit>" \ + --body "$(cat <<'EOF' +<paste the template body here> +EOF +)" +``` + +Print the returned PR URL. + +**If NO:** print "Template ready — open the PR manually when ready." and stop. + +--- + +## Guardrails + +- **Never** run `git push` yourself. Your job is clearance, not execution. +- **Never** skip or suppress the pre-push hook (`--no-verify`). The hook is additive to this audit, not a replacement. +- **Never** mark the audit complete without the developer's YES if there are manual testing items. +- **Never** write tests that always pass regardless of implementation (e.g., `expect(true).toBe(true)`). +- **Never** open a PR without asking the developer first (Step 8d). +- If `npm test` is failing for a pre-existing reason unrelated to the current branch, note it clearly and ask the developer whether to unblock. Do not silently ignore failures. +- If a test file already exists but is empty or trivially passing, flag it as a gap — it counts as missing. +- When generating the PR template, make it specific to the actual diff — never use boilerplate placeholders that a reviewer would have to fill in. diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md new file mode 100644 index 0000000..ccdc090 --- /dev/null +++ b/.claude/skills/review-pr/SKILL.md @@ -0,0 +1,664 @@ +--- +name: review-pr +description: Reviewer-side PR audit. Detects generator bias and starts a fresh session if needed, rebases the branch and resolves straightforward conflicts, collects PR context and derives a test plan if absent, checks code quality (no eslint-disable, no duplicate constants, no orphaned files, env var coverage), audits test coverage against acceptance criteria, runs the full build and test suite, documents findings, and self-updates this skill with any new gap patterns observed. +argument-hint: [optional: PR number or branch name, default is current branch] +allowed-tools: Bash, Read, Edit, Write, Glob, Grep, TodoWrite, AskUserQuestion +--- + +You are a PR reviewer. Your job is to catch problems before a merge — not to implement fixes yourself, but to identify, document, and block on issues that matter. Be specific: name the file, line, and rule. Do not approve by silence. + +Run every step in order. Do not skip steps. If a step produces blockers, document them and continue to the next step — collect all findings before reporting. + +--- + +## Step 0 — Generator bias detection + +Before reviewing any code, determine whether this session generated the code you are about to review. Reviewing code you wrote in the same session introduces generator bias: you will miss your own blind spots. + +### 0a — Identify changed files + +```bash +git fetch origin +git diff --name-only origin/main...HEAD +``` + +Capture the list of changed files. + +### 0b — Introspect session history + +Scan your own conversation context for this session. Look for any `Write` or `Edit` tool calls that targeted a file appearing in the diff list from Step 0a. + +This is a self-check: if you wrote or edited any file in the diff during this session, you have generator bias. + +### 0c — Decision + +| Condition | Action | +|-----------|--------| +| No overlap — you did not generate any of these files | Continue to Step 1 | +| Overlap found — you wrote or edited ≥ 1 file in the diff | Stop and present the generator bias warning below | + +**Generator bias warning (present and block):** + +``` +⚠️ GENERATOR BIAS DETECTED + +This agent session was used to write or modify the following files that are now under review: + + <list the overlapping files> + +Reviewing code you generated in the same session is unreliable — you will tend to overlook +the same mistakes you made when writing it. + +Recommended action: start a fresh Claude Code session and run /review-pr from there. + +Reply PROCEED to continue this review anyway (at your own risk), or STOP to abort. +``` + +Use `AskUserQuestion` to ask the reviewer. If they reply `STOP` or anything other than `PROCEED` (case-insensitive), print "Review aborted — restart in a fresh session." and stop. + +If they reply `PROCEED`, print a note that the review is proceeding with known generator bias and continue. + +--- + +## Step 1 — Rebase check + +Verify the branch is on top of the latest `origin/main`. Stale branches produce false conflicts and make diffs harder to read. + +```bash +git merge-base --is-ancestor origin/main HEAD && echo "REBASED" || echo "BEHIND" +``` + +**If `REBASED`:** continue to Step 2. + +**If `BEHIND`:** attempt a rebase: + +```bash +git rebase origin/main +``` + +If the rebase completes cleanly, print: + +``` +✓ Rebase: branch fast-forwarded onto origin/main +``` + +Continue to Step 2. + +**If the rebase hits conflicts:** + +For each conflicted file, open it and classify the conflict: + +| Conflict type | Condition | Action | +|---------------|-----------|--------| +| **Trivial** | One side added entirely new lines with no overlap (e.g., new import, new constant, new function) | Resolve by accepting both sides; keep incoming addition + main addition | +| **Formatting-only** | The only difference is whitespace, trailing commas, or import order | Resolve by accepting the branch version (it came last) | +| **Unambiguous** | One side deleted a line the other side did not touch, or one side is a pure addition the other does not conflict with | Resolve deterministically; log the decision | +| **Ambiguous** | Both sides changed the same logic, or the intent of one side is unclear without domain knowledge | Stop, show the conflict, and ask the human | + +For each **ambiguous conflict**, use `AskUserQuestion`: + +``` +Rebase conflict — human decision required + +File: <path/to/file.ts> +Conflict: + +<<<<<<< HEAD (main) +<main side — paste the relevant lines> +======= +<branch side — paste the relevant lines> +>>>>>>> <commit-sha> (<branch-name>) + +Context: <one sentence explaining what each side is doing> + +Which version should win, or how should the two sides be merged? +Reply with: MAIN | BRANCH | <custom resolution> +``` + +Apply the human's resolution, stage the file, and continue the rebase: + +```bash +git add <file> +git rebase --continue +``` + +Repeat for each conflicted file. If the human ever replies `ABORT`, run `git rebase --abort` and stop with: + +``` +✗ REVIEW BLOCKED — rebase aborted at developer request. +Resolve the conflicts manually, then re-run /review-pr. +``` + +After all conflicts are resolved, print a summary: + +``` +✓ Rebase complete + Trivial conflicts resolved automatically: <N> + Conflicts resolved with human input: <N> +``` + +--- + +## Step 2 — Collect PR context + +Gather the PR description, acceptance criteria, and any linked issues. + +### 2a — Try GitHub + +```bash +gh pr list --head "$(git branch --show-current)" --json number,title,body,url --state open +``` + +If a PR is found, extract: +- **PR number and URL** +- **Summary / description** +- **Acceptance criteria** (look for "Acceptance Criteria", "AC", "Definition of Done" sections) +- **Test plan** (look for "Test plan", "Testing", "QA" sections) +- **Linked issues** (look for `Closes #N`, `Fixes #N`, `Resolves #N`) + +If no open PR is found, continue to 2b. + +### 2b — Fallback: ask the developer + +Use `AskUserQuestion`: + +``` +No open PR found for this branch. + +Please paste the PR description (body) here, or reply NONE if there is no PR yet. +``` + +If the developer replies `NONE` or pastes nothing useful, note "No PR body available" and continue to Step 3 using only the diff as context. + +### 2c — Record context + +Print a summary of what was found: + +``` +PR context: + PR: #<N> — <title> (<url>) | NONE + Acceptance criteria: found (<N> items) | not found + Test plan: found | not found + Linked issues: #N, #M | none +``` + +--- + +## Step 3 — Test plan + +### 3a — If a test plan exists + +Extract it verbatim from the PR body. Convert it to a checklist if it is not already one. + +Print: + +``` +Test plan (from PR): +□ <item 1> +□ <item 2> +... +``` + +### 3b — If no test plan exists + +Derive one. Use the following inputs: +- Acceptance criteria (if any, from Step 2) +- Changed files (from Step 0a), partitioned into buckets (scripts, Remotion, app components, API routes, config, docs) +- Any obvious user-facing or pipeline-facing behaviour changed by the diff + +Produce a checklist of the form: + +``` +Test plan (derived — no test plan in PR): +Automated: +□ npm test passes +□ npm run test:react passes (React files changed) +□ npm run test:integration passes (integration files changed) +□ npm run test:e2e passes (Phase 8 / UI flows changed) +□ tsc --noEmit clean + +Manual (requires human): +□ [REMOTION-VISUAL] <specific check> — Affected: <file> +□ [UI-BROWSER] <specific check> — Affected: <file> +□ [PIPELINE] <specific check> — Affected: <file> +``` + +Only include buckets that have changed files. Be specific — name the component or script being tested. + +Note the derivation for the review findings doc in Step 7. + +--- + +## Step 4 — Code quality checks + +Run each check against all files in the diff. Collect all findings; do not stop on first failure. + +### 4a — ESLint disable comments + +```bash +git diff origin/main...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' | grep '^\+' | grep -E 'eslint-disable' +``` + +Every `eslint-disable` or `eslint-disable-next-line` added in this diff is a finding. + +For each occurrence, record: + +``` +[QUALITY] eslint-disable used + File: <path> + Line: <approximate location> + Rule: <which rule is disabled, if specified> + Risk: Suppressing lint rules hides real bugs. Prefer fixing the root cause. + Verdict: BLOCKER — remove the disable comment or justify it explicitly in the PR. +``` + +### 4b — Duplicate constants + +Check whether any magic number or string literal in the diff already exists as a named constant in: +- `CLAUDE.md` → **Common Constants** table +- Any `*constants.ts`, `*config.ts`, or `remotion/lib/*.ts` file in the repo + +```bash +# Find existing constant files +find . -name "*constants*" -o -name "*config*" \ + | grep -v node_modules | grep -v .next | grep -v coverage \ + | grep -E '\.(ts|js)$' +``` + +For each magic literal in the diff that matches a value in those files, record: + +``` +[QUALITY] Duplicate constant + File: <path-in-diff> + Value: <literal value> + Already defined in: <existing-file>:<constant-name> + Verdict: BLOCKER — import the shared constant instead of re-declaring. +``` + +### 4c — Orphaned files + +For each **new file** added in the diff (not modified — added), check whether it is referenced anywhere in the codebase: + +```bash +# For a new file scripts/foo/bar.ts: +grep -rn "bar" --include="*.ts" --include="*.tsx" --include="*.js" \ + --exclude-dir=node_modules --exclude-dir=.next . | grep -v "bar.ts" +``` + +Also check: +- Is it exported from an `index.ts`? +- Is it imported anywhere? +- Is it referenced in `scripts/wizard.js` or a pipeline runner? + +If a new file is unreferenced, record: + +``` +[QUALITY] Orphaned file + File: <path> + Status: Added but not imported or referenced anywhere in the codebase. + Verdict: WARNING — confirm this file is intentional and will be wired up, or delete it. +``` + +### 4d — Environment variable coverage + +Find all `process.env.` references added in the diff: + +```bash +git diff origin/main...HEAD -- '*.ts' '*.tsx' '*.js' | grep '^\+' | grep 'process\.env\.' +``` + +For each `process.env.VAR_NAME` found, check whether `VAR_NAME` appears in `.env.example` (or `.env.sample` / `.env.template`): + +```bash +find . -name ".env.example" -o -name ".env.sample" -o -name ".env.template" \ + | grep -v node_modules +``` + +```bash +grep "VAR_NAME" .env.example 2>/dev/null || echo "MISSING" +``` + +For each missing variable, record: + +``` +[QUALITY] Missing env var documentation + Variable: <VAR_NAME> + Used in: <file> + Missing from: .env.example + Verdict: BLOCKER — add VAR_NAME= with a placeholder value and a comment to .env.example. +``` + +--- + +## Step 5 — Test coverage audit + +### 5a — Identify source files changed + +From the Step 0a diff, filter to non-test source files: + +```bash +git diff --name-only origin/main...HEAD \ + | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '^e2e/' \ + | grep -E '\.(ts|tsx|js|jsx)$' +``` + +### 5b — Check for corresponding test files + +For each source file `path/to/foo.ts`, check: + +```bash +ls path/to/foo.test.ts 2>/dev/null || echo "MISSING" +ls path/to/foo.test.tsx 2>/dev/null || echo "MISSING" +ls tests/integration/ 2>/dev/null +``` + +Use the decision table from `docs/TESTING_STANDARDS.md`: + +| Condition | Required test type | Location | +|-----------|--------------------|----------| +| Pure functions (no I/O) | Unit test | Next to the file | +| Filesystem I/O or spawns processes | Integration test | `tests/integration/` | +| React component | Smoke render test | Next to the file (react project) | +| App API route | Unit or integration test | Next to or in `tests/integration/` | +| Remotion component | Smoke render test (mock remotion hooks) | Next to the file (react project) | +| Pure Remotion lib function | Unit test | Next to the file | +| Config / tooling / docs / assets | No test required | — | + +### 5c — Cross-check tests against acceptance criteria + +For each acceptance criterion in the test plan (Step 3), verify that at least one test exists that could falsify it. A criterion like "readProject() throws ProjectNotFoundError when file missing" must have a test that exercises that path. + +If a criterion has no corresponding test, record: + +``` +[COVERAGE] Uncovered acceptance criterion + AC: <criterion text> + Gap: No test exercises this path. + Verdict: BLOCKER — add a test before merging. +``` + +### 5d — Test quality spot-check + +For each test file in the diff, scan for trivially passing tests: +```bash +grep -n "expect(true)" <test-file> +grep -n "expect.*toBeTruthy()" <test-file> +grep -n "toBeDefined()" <test-file> +``` + +A test that only asserts `expect(true).toBe(true)` or `expect(result).toBeDefined()` with no behavioural assertion is a gap — it passes regardless of implementation. + +For each such test, record: + +``` +[COVERAGE] Trivially passing test + File: <test-file> + Line: <N> + Issue: Assertion does not verify behaviour — will pass even if implementation is broken. + Verdict: WARNING — replace with a behavioural assertion. +``` + +--- + +## Step 6 — Build and test suite + +### 6a — Type check + +```bash +npx tsc --noEmit +``` + +If this exits non-zero, record each error as: + +``` +[BUILD] TypeScript error + File: <path> + Error: <message> + Verdict: BLOCKER +``` + +### 6b — Unit and integration tests + +```bash +npm test +``` + +If tests fail, record: + +``` +[TEST] Test suite failure + Suite: npm test + Failures: <test names> + Verdict: BLOCKER +``` + +### 6c — React tests (if applicable) + +If any `app/**` or `remotion/**` files changed: + +```bash +npm run test:react +``` + +Record failures as above with `Suite: npm run test:react`. + +### 6d — E2E tests (if applicable) + +If the diff includes Phase 8 user-facing browser flows or changes to `app/` routes: + +```bash +npm run test:e2e +``` + +Record failures as above with `Suite: npm run test:e2e`. + +### 6e — Build + +```bash +npm run build 2>/dev/null || npx next build 2>/dev/null || echo "NO_BUILD_SCRIPT" +``` + +If the build fails and it is not `NO_BUILD_SCRIPT`, record it as a BLOCKER. + +--- + +## Step 7 — Document findings + +Create a findings file: + +``` +docs/review-findings/YYYY-MM-DD-<branch-name>.md +``` + +(Use today's date from the environment. Branch name: `git branch --show-current`.) + +### 7a — Structure + +```markdown +# Review: <branch-name> +Date: YYYY-MM-DD +Reviewer: AI (review-pr skill) — session bias: CLEAN | GENERATOR BIAS (developer overrode) +PR: #<N> <url> | NONE + +## Verdict +APPROVED | APPROVED WITH SUGGESTIONS | CHANGES REQUESTED + +## Summary +<2–3 sentences: what the PR does, what the main risk areas are> + +## Blockers (must fix before merge) +<!-- One entry per BLOCKER finding --> +### B1 — <short title> +- **Type:** QUALITY | COVERAGE | BUILD | TEST +- **File:** <path> +- **Finding:** <description> +- **Fix:** <specific remediation> + +## Warnings (should address) +<!-- One entry per WARNING finding --> +### W1 — <short title> +- **Type:** QUALITY | COVERAGE +- **File:** <path> +- **Finding:** <description> +- **Suggestion:** <specific suggestion> + +## Suggestions (optional improvements) +<!-- Lightweight items that don't block the merge --> + +## Test plan verification +<!-- Reproduce the test plan from Step 3 with status for each item --> +| Item | Status | Notes | +|------|--------|-------| +| npm test passes | PASS / FAIL / NOT RUN | | +| ... | | | + +## Patterns observed +<!-- Reserved for Step 8 — leave blank here --> +``` + +### 7b — Verdict rules + +| Condition | Verdict | +|-----------|---------| +| Zero BLOCKER findings, zero WARNING findings | APPROVED | +| Zero BLOCKER findings, ≥ 1 WARNING findings | APPROVED WITH SUGGESTIONS | +| ≥ 1 BLOCKER finding | CHANGES REQUESTED | + +Print the verdict prominently after generating the file: + +``` +Review complete → docs/review-findings/YYYY-MM-DD-<branch-name>.md + +Verdict: CHANGES REQUESTED | APPROVED WITH SUGGESTIONS | APPROVED + +Blockers: <N> +Warnings: <N> +``` + +--- + +## Step 8 — Pattern recognition and self-update + +Review all findings collected across Steps 4–6. Ask: **is this a pattern I have seen before, or a new class of problem?** + +### 8a — Check known patterns + +Read the `## Known Gap Patterns` section at the bottom of this SKILL.md. Each pattern has a name and a description. If a current finding matches a known pattern, note it in the findings doc under "Patterns observed" and skip to Step 9. + +### 8b — Identify new patterns + +A finding qualifies as a **new pattern** if: +- It represents a class of mistake (not a one-off), AND +- It is not already described in the known patterns list, AND +- It would be useful to watch for in future reviews of this codebase + +Examples of pattern-worthy findings: +- A lint disable comment used to suppress a real type error +- A magic number duplicated from an existing constant +- An orphaned migration or schema file added without a corresponding loader +- A `process.env` read added without `.env.example` coverage +- A test file that only asserts `.toBeDefined()` without a behavioural assertion + +### 8c — Add new patterns to this skill + +For each new pattern, append an entry to the `## Known Gap Patterns` section of this file: + +```markdown +### <Pattern name> +**Category:** QUALITY | COVERAGE | BUILD | CONVENTION +**Trigger:** <one sentence describing when to look for this> +**Check:** <specific grep or inspection step to detect it> +**Verdict:** BLOCKER | WARNING +**First seen:** <branch-name> — <YYYY-MM-DD> +``` + +Write the update with the Edit tool targeting this file (`SKILL.md`). The patterns section is append-only — never remove entries, only add them. + +### 8d — Update CLAUDE.md if warranted + +A new pattern warrants a CLAUDE.md update when it represents a **code convention** that all contributors (human and AI) should follow going forward — not just something to catch in review. + +Criteria: +- The pattern involves a structural or architectural decision (e.g., "all env vars must appear in `.env.example`") +- It would prevent the same mistake from being generated in the first place +- It is not already captured in CLAUDE.md + +If warranted, add a concise rule to the appropriate CLAUDE.md section. Common targets: +- **Common Constants** table — if a constant was duplicated +- A new "Code conventions" subsection under the relevant Phase or component section +- **Key Source Files** — if a new canonical file should always be the source of truth + +Commit the CLAUDE.md update: + +```bash +git add CLAUDE.md +git commit -m "$(cat <<'EOF' +docs(claude-md): add convention rule from review-pr pattern detection + +Pattern: <pattern name> +Observed in: <branch-name> +EOF +)" +``` + +--- + +## Step 9 — Final report + +Print a terse, scannable summary: + +``` +Review: <branch-name> +═══════════════════════════════════════════════════════════════ + +Verdict: CHANGES REQUESTED | APPROVED WITH SUGGESTIONS | APPROVED +Blockers: <N> (must fix before merge) +Warnings: <N> (should address) + +Rebase: ✓ clean | ✓ resolved (<N> trivial, <N> human) | ✗ aborted +Generator bias: CLEAN | OVERRIDDEN by developer + +Test suite: npm test — PASS | FAIL + npm run test:react — PASS | FAIL | SKIPPED + npm run test:e2e — PASS | FAIL | SKIPPED +TypeScript: CLEAN | <N> errors + +Findings doc: docs/review-findings/YYYY-MM-DD-<branch-name>.md +New patterns: <N> added to SKILL.md | none +CLAUDE.md: updated | unchanged +``` + +Then list each BLOCKER concisely (one line each) so the developer knows exactly what to fix: + +``` +Blockers to fix: + B1 — <file>: <one-line description> + B2 — <file>: <one-line description> +``` + +If there are no blockers, print: + +``` +No blockers — branch is ready to merge. +``` + +--- + +## Guardrails + +- **Never** approve a PR with a BLOCKER finding — the verdict must be CHANGES REQUESTED. +- **Never** skip the generator bias check — it runs first, before touching any diff. +- **Never** resolve an ambiguous rebase conflict without asking the human. +- **Never** run `git push` — this skill reviews, it does not push. +- **Never** write a trivially passing test yourself to close a coverage gap — that would defeat the purpose; flag it as a BLOCKER. +- **Never** add a `docs/review-findings/` entry without filling in all sections. +- If `npm test` is failing for a pre-existing reason, note it clearly and ask the developer before treating it as a BLOCKER introduced by this branch. +- The self-update in Step 8 is an append — never delete or modify existing pattern entries; only add new ones. + +--- + +## Known Gap Patterns + +_This section is populated automatically by Step 8c as patterns are observed in real reviews. Do not edit manually._ + +<!-- Entries are appended here by the skill --> diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..4a44c45 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,55 @@ +name: E2E Tests + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: # allow manual runs from the GitHub Actions UI + +jobs: + e2e: + name: Playwright (Chromium) + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browser + OS dependencies + run: npx playwright install --with-deps chromium + + - name: Run E2E tests + run: npm run test:e2e + env: + CI: true + # Add any env vars the Next.js dev server needs here, e.g.: + # AUTH_EMAIL: ${{ secrets.AUTH_EMAIL }} + # AUTH_PASSWORD: ${{ secrets.AUTH_PASSWORD }} + # SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + + # Always upload the HTML report so failures are inspectable in the UI + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 14 + + # Traces are larger; only keep them when a test actually failed + - name: Upload failure traces + uses: actions/upload-artifact@v4 + if: failure() + with: + name: test-results + path: test-results/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 1d52704..32ae9d1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ # testing /coverage +/playwright-report +/test-results # next.js /.next/ diff --git a/.husky/pre-commit b/.husky/pre-commit index 9d0cd16..38acbdb 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,23 @@ -npm test -- --testPathPattern=edit-transcript --no-coverage +#!/usr/bin/env bash +set -e + +# 1. ESLint on staged TS/JS files; JSON syntax check; Python syntax check +npx lint-staged + +# 2. TypeScript type check whenever .ts/.tsx files are staged +if git diff --cached --name-only | grep -qE '\.(ts|tsx)$'; then + echo "▶ tsc --noEmit" + npx tsc --noEmit +fi + +# 3. Run Jest for staged files across node + react projects (multi-project config) +# e2e/ is excluded — Playwright tests require a live server; run in CI or pre-push instead +STAGED_TESTABLE=$(git diff --cached --name-only \ + | grep -E '\.(js|ts|tsx)$' \ + | grep -v 'node_modules' \ + | grep -v '^e2e/' \ + || true) +if [ -n "$STAGED_TESTABLE" ]; then + echo "▶ jest --findRelatedTests" + echo "$STAGED_TESTABLE" | xargs npx jest --findRelatedTests --passWithNoTests --no-coverage +fi diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..42bf13e --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -e + +# ── 1. Full Jest test suite ──────────────────────────────────────────────────── +# Pre-commit only runs tests related to staged files. Pre-push runs the full +# suite to catch cross-file regressions introduced across multiple commits. +# +# Four suites are excluded due to pre-existing failures unrelated to agent work +# — remove each exclusion once the underlying issue is resolved: +# transcript-caption.test.js → needs public/edit/transcript.json (not committed) +# CaptionExtractor.test.js → youtube-transcript ships ESM-only; transform mismatch +# CarouselGenerator.test.js → puppeteer mock missing setRequestInterception +# generate-carousel.test.js → same puppeteer mock issue +echo "▶ jest (full suite)" +npx jest --no-coverage \ + --testPathIgnorePatterns='transcript-caption\.test|CaptionExtractor\.test|CarouselGenerator\.test|generate-carousel\.test' + +# ── 2. E2E smoke tests ───────────────────────────────────────────────────────── +# Playwright starts the dev server automatically (see playwright.config.ts). +# Agents cannot read CI results, so this is their only feedback loop on E2E. +echo "▶ playwright test" +npx playwright test + +# ── 3. No debugger statements ────────────────────────────────────────────────── +# Agents frequently omit cleanup of debugger statements. These are never valid +# in committed source. e2e/ is excluded as Playwright uses its own debug mode. +echo "▶ scanning for debugger statements" +if grep -rn 'debugger' \ + --include='*.ts' --include='*.tsx' --include='*.js' \ + --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=coverage \ + --exclude-dir=e2e \ + .; then + echo "✗ 'debugger' found in source — remove before pushing." + exit 1 +fi + +# ── 4. No .only() in test files ──────────────────────────────────────────────── +# A focused .only() silently skips all other tests, creating a false green. +# Playwright's forbidOnly catches this in CI; this check gives agents local +# feedback before the push reaches CI. +echo "▶ scanning for .only() in tests" +if grep -rn '\.\bonly\b\s*(' \ + --include='*.test.ts' --include='*.test.tsx' --include='*.test.js' \ + --include='*.spec.ts' --include='*.spec.tsx' --include='*.spec.js' \ + --exclude-dir=node_modules \ + .; then + echo "✗ .only() found in test file — remove before pushing." + exit 1 +fi + +# ── 5. No secret tokens in source ───────────────────────────────────────────── +# Scans for high-confidence token formats (GitHub PAT, AWS access key, Anthropic +# API key, generic OpenAI-format key). False-positive rate is near zero; these +# patterns must never appear in committed files. +echo "▶ scanning for secret tokens" +if grep -rEn \ + 'ghp_[A-Za-z0-9]{36}|AKIA[0-9A-Z]{16}|sk-ant-[A-Za-z0-9_\-]{90,}|sk-[A-Za-z0-9]{48}' \ + --include='*.ts' --include='*.tsx' --include='*.js' \ + --include='*.json' --include='*.py' \ + --exclude-dir=node_modules --exclude-dir=.next \ + .; then + echo "✗ Secret token detected — remove from source before pushing." + exit 1 +fi + +# ── 6. npm audit — no moderate/high/critical vulnerabilities ────────────────── +# Blocks pushes when installed packages carry known vulnerabilities at moderate +# severity or above. Low-severity advisories are ignored; they are informational +# and rarely exploitable in this context. +# +# If npm audit cannot reach the registry (offline / VPN), the check is skipped +# with a warning rather than a hard block — a network failure is not a security +# failure. Remove the offline bypass if this repo ever enters a stricter security +# posture. +echo "▶ npm audit (moderate+)" +AUDIT_OUTPUT=$(npm audit --audit-level=moderate 2>&1) || { + EXIT=$? + # npm audit exits 1 for vulnerabilities found, ENOTFOUND/ECONNREFUSED for network errors + if echo "$AUDIT_OUTPUT" | grep -qE 'ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network'; then + echo "⚠ npm audit skipped — registry unreachable (offline?)" + else + echo "$AUDIT_OUTPUT" + echo "✗ npm audit found moderate or higher vulnerabilities — run 'npm audit fix' or pin to a safe version before pushing." + exit $EXIT + fi +} + +# ── Phase-gated guards ───────────────────────────────────────────────────────── +# Uncomment each block after its phase merges to main. These prevent agents from +# reintroducing patterns that a phase explicitly removed. + +# Phase 5 — hookTiming.ts consolidates all timing constants into one file. +# After merge, HOOK_TAIL_PAD_UNBOUNDED_SECONDS must exist in exactly one place. +# COUNT=$(grep -rn 'HOOK_TAIL_PAD_UNBOUNDED_SECONDS\s*=' \ +# --include='*.ts' --include='*.tsx' --exclude-dir=node_modules . \ +# | wc -l | tr -d ' ') +# if [ "$COUNT" -gt 1 ]; then +# echo "✗ HOOK_TAIL_PAD_UNBOUNDED_SECONDS declared in $COUNT files — must live only in remotion/lib/constants.ts" +# exit 1 +# fi + +# Phase 7 — removes DEMO_CREDENTIALS from AuthContext.tsx. +# After merge, this pattern must never reappear. +# if grep -rn 'DEMO_CREDENTIALS\|demo_password' \ +# --include='*.ts' --include='*.tsx' --exclude-dir=node_modules .; then +# echo "✗ Hardcoded credential pattern found — must not exist post-Phase 7." +# exit 1 +# fi + +# Phase 0.5 — brand content moves out of remotion/components/ into brand.json. +# After merge, host names and brand handles must not appear in remotion/components/. +# if grep -riE 'natasha|saloni|victoria|techybara|ragtechdev|~/ragtech' \ +# remotion/components/ 2>/dev/null; then +# echo "✗ Hardcoded brand content found in remotion/components/ — must live in brands/ragtech/brand.json post-Phase 0.5." +# exit 1 +# fi diff --git a/.husky/validate-json.cjs b/.husky/validate-json.cjs new file mode 100644 index 0000000..1fa3abd --- /dev/null +++ b/.husky/validate-json.cjs @@ -0,0 +1,11 @@ +#!/usr/bin/env node +// JSON syntax validator — called by lint-staged for staged *.json files +const fs = require('fs'); +for (const file of process.argv.slice(2)) { + try { + JSON.parse(fs.readFileSync(file, 'utf8')); + } catch (e) { + process.stderr.write(`✗ Invalid JSON: ${file}\n ${e.message}\n`); + process.exit(1); + } +} diff --git a/AGENTS.md b/AGENTS.md index bfeb40e..022a7ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,340 +1,3 @@ -# DeckCreate — Architecture Reference +# AGENTS.md -End-to-end guide for AI agents and contributors. See `CLAUDE.md` for project/brand context. - ---- - -## Agent implementation convention - -Any multi-step feature must be broken into isolated, independently-testable commits. -Implementation docs live in `docs/`. Each commit message slug must match the heading -in the corresponding doc exactly so that a resuming agent can locate its starting point. - -**Resuming interrupted work:** -1. Run `git log --oneline` to see completed commits. -2. Open the relevant doc in `docs/`. -3. Match the last commit message against the slugs in the checklist. -4. Continue from the next unstarted step. Do not re-do completed commits. - -**When writing an implementation doc:** -- Each step must include a "Status check" — a single command or file-existence test - that confirms the step is already done. -- Steps must be ordered by dependency. Note cross-step dependencies explicitly. -- Commits must not be combined. Isolation allows partial recovery. - ---- - -## Pipeline - -``` -Raw audio/video - ↓ [sync] FFT cross-correlation → synced-output-{N}.mp4 (one per angle) - ↓ [transcribe] Whisper.cpp → token-level timestamps → transcript.raw.json - ↓ [diarize] Speaker turn detection → diarization.json - ↓ [assign-speakers] Labels segments with speaker names - ↓ [align] WhisperX forced alignment → refines t_dtw, populates t_end - ↓ [edit-transcript] Merges phrases → sentences → transcript.doc.txt + transcript.json - Human edits doc (cuts, corrections, hooks, camera cues) - ↓ [merge-doc] Applies doc edits → transcript.json - ↓ [setup-camera] Face detection per angle → camera-profiles.json - ↓ Remotion transcript.json + camera-profiles.json → composed video -``` - -Intermediate files: `public/transcribe/output/`. Synced video(s): `public/sync/output/`. - ---- - -## transcript.json schema - -``` -meta - videoSrc?: string path relative to /public (overrides composition src prop) - videoSrcs?: string[] all angle paths (multi-angle); used by setup-camera - videoStart?: number source seconds; segments before are excluded - videoEnd?: number source seconds; segments after are excluded - fps: 60 - -segments[] - id, start, end source-video timestamps in seconds - speaker display name (e.g. "Natasha") - text human-readable sentence - cut: boolean true = entire segment removed - tokens[] - t_dtw: number word start time (WhisperX-aligned or Whisper t_dtw) - t_end?: number word end time (forced alignment only) - when present, deriveCuts uses exact boundaries - when absent, falls back to heuristic CUT_START_BIAS - text: string - cut: boolean - cuts: TimeCut[] [{from, to}] intra-segment ranges to skip - populated by edit-transcript from token.cut flags - NOT set manually — edit transcript.doc.txt instead - hook?: boolean when true, prepended as hook/teaser before main - hookFrom?, hookTo? clip bounds within the segment (seconds) - cameraCues[] explicit camera shot overrides (> CAM directives in doc) -``` - ---- - -## camera-profiles.json schema - -```json -{ - "sourceWidth": 1920, "sourceHeight": 1080, - "outputWidth": 1920, "outputHeight": 1080, - "wideViewport": { "cx": 0.5, "cy": 0.5, "w": 1, "h": 1 }, - - // Multi-angle only — one entry per camera angle - "angles": { - "angle1": { "videoSrc": "sync/output/synced-output-1.mp4", - "sourceWidth": 1920, "sourceHeight": 1080, - "wideViewport": { "cx": 0.5, "cy": 0.5, "w": 1, "h": 1 } }, - "angle2": { "videoSrc": "sync/output/synced-output-2.mp4", - "sourceWidth": 1920, "sourceHeight": 1080 } - }, - - "speakers": { - "Natasha": { - "label": "Natasha", - "angleName": "angle1", // omit for single-angle - "closeupViewport": { "cx": 0.3, "cy": 0.4, "w": 0.35, "h": 0.35 }, - "portraitCx": 0.3 // portrait-mode centre - } - } -} -``` - -`CropViewport`: `cx/cy` = normalised centre (0–1), `w/h` = crop dimensions (0–1). - ---- - -## Rendering model — "inclusive by default" - -Full range `[videoStart, lastSegment.end]` plays continuously. Cuts are opt-in: - -| Source | Mechanism | -|--------|-----------| -| Entire segment removed | `segment.cut = true` | -| Intra-segment word/phrase | `segment.cuts[]` entries (from `{curly braces}` in doc) | -| Inter-segment silence | `merge-doc:cut-pauses` writes silence ranges into `cuts[]` | - -No implicit cuts. Gaps between segments play as silence unless you run `merge-doc:cut-pauses`. - ---- - -## Remotion component architecture - -### Data flow -``` -transcript.json - → getActiveSegments() filter by meta.videoStart/videoEnd - → buildSections() → {hookSections[], mainSections[]} - → SegmentPlayer / CameraPlayer - → SectionGroupPlayer OffthreadVideo + trimBefore per section -``` - -### buildSections (`SegmentPlayer.tsx`) - -Two independent section arrays — hooks and main — each in its own `<Sequence>` with a local frame counter. Separation prevents negative `trimBefore` (hooks originate deep in source time). - -**Main sections** (`buildMainSubClips`): -1. Range = `[videoStart, lastActiveSegment.end]` -2. Collect exclusions: `cut=true` spans + all `cuts[]` entries -3. Merge overlapping exclusions; invert → playable `SubClip[]` -4. Convert: `trimBefore = Math.floor(start*fps)`, `trimAfter = Math.ceil(end*fps)` - -**Hook sections** (`getHookSubClips`): uses `hookFrom/hookTo` bounds, extends end when spoken tokens drift past `hookTo`, bridges to next hook if gap ≤ 1 s. - -### SectionGroupPlayer (`SegmentPlayer.tsx`) - -Jump-cut engine. At composition frame `f`: -``` -summedDurations = Σ(section.trimAfter - section.trimBefore) for sections[0..k] -trimBefore = section.trimAfter - summedDurations (= S(f) - f) -sourceFrame = trimBefore + f (= S(f)) -``` -`OffthreadVideo` receives `trimBefore` and renders source frame `S(f)`. Cuts are skipped because no section covers those source frames. - -`muted?: boolean` prop silences audio — used by `CameraPlayer` for non-active angle layers. - -### CameraPlayer (`CameraPlayer.tsx`) - -Applies viewport transforms (scale + translate) to simulate punch-in/punch-out camera cuts. - -**Single-angle**: wraps one `SegmentPlayer` in an `AbsoluteFill`, animates `CropViewport` transform. - -**Multi-angle**: stacks one `SegmentPlayer` per unique `videoSrc` referenced in shots. At each frame, the active angle layer has `opacity: 1`; all others `opacity: 0, muted`. Viewport transform and source dimensions are per-angle. - -**`buildCameraShots`** — builds `CameraShot[]` timeline: -- Shot boundaries at segment start times via `sourceToOutputFrame(seg.start, mainSections, fps)` -- `emitShot()` looks up `speaker.angleName → profiles.angles[name].videoSrc` to set `shot.videoSrc` -- Pacing constants: `MIN_WIDE_S=1.5s`, `MAX_CLOSEUP_S=20s`, `PERIODIC_WIDE_S=45s` -- Speaker changes trigger immediate cut to new closeup if previous shot ≥ 1 s - -**`CameraShot`**: `{ startFrame, endFrame, viewport: CropViewport, videoSrc?: string }` - -**Viewport transform**: -``` -scale = max(outW / (srcW × vp.w), outH / (srcH × vp.h)) -tx = (0.5 - vp.cx) × 100 % -ty = (0.5 - vp.cy) × 100 % -→ CSS: scale(${scale}) translate(${tx}%, ${ty}%) -``` - -**Explicit overrides** (`cameraCues[]`): `collectCameraOverrides` maps cue timestamps to output frames via `sourceToOutputFrame`; `applyOverrides` splices them into the pacing shot list, propagating `videoSrc` from the cue's speaker profile. - -### Hook rendering - -1. `hookSections` → frames `[0, hookDuration)` -2. `PodcastIntro` → frames `[hookDuration, hookDuration + INTRO_DURATION_FRAMES)` -3. `mainSections` → from `hookDuration + introFrames` (passed as `mainOffset`) - -Hook music looped over hook duration. `HookOverlay` shows karaoke captions + Techybara mascot; mounted for full composition duration, returns `null` outside hook frames. - ---- - -## Multi-angle sync (`AudioSyncer.syncMultiple`) - -`AudioSyncer.syncMultiple(videoPaths, audioPath, outputDir)` — static method in `scripts/sync/AudioSyncer.js`. Syncs each video independently to the same audio via FFT cross-correlation. Outputs `synced-output-1.mp4`, `synced-output-2.mp4`, etc. Returns `[{ outputPath, videoSrc, sourceWidth, sourceHeight }]`. - ---- - -## Camera setup — multi-angle (`setup-camera.js`) - -`--videos p1 p2 ...` OR reads `meta.videoSrcs` from transcript. Per angle: -- Extracts `frame-angle{N}.jpg` at `meta.videoStart` -- Runs MediaPipe face detection → `detections-angle{N}.json` - -Writes `angles.json` (manifest for the camera GUI): -```json -[{ "angleName": "angle1", "videoSrc": "...", "frameFile": "frame-angle1.jpg", - "detectFile": "detections-angle1.json" }, ...] -``` - -Camera GUI (`app/camera/page.tsx`): loads `angles.json`, shows angle tabs, tags each face box with `angleName`, saves `angleName` per speaker + `angles` map to `camera-profiles.json`. - ---- - -## Silence removal - -`merge-doc:cut-pauses` (`--auto-cut-pauses N`) detects silence gaps and writes `TimeCut` entries into `segment.cuts[]`. The renderer excludes those ranges identically to any other cut. Default threshold: `0.5 s`. - -With `token.t_end` (after forced alignment): silence = `next.t_dtw − curr.t_end` (exact). -Without: estimate = `next.t_dtw − curr.t_dtw − WORD_DURATION_ESTIMATE (0.4 s)`. - ---- - -## Cut boundary precision - -`deriveCuts` in `edit-transcript.js`: - -| Field available | Cut start | Cut end | -|---|---|---| -| `prevWord.t_end` present | `prevWord.t_end` (exact) | `nextWord.t_dtw` (exact) | -| `t_end` absent | `prevWord.t_dtw + CUT_START_BIAS × gap` | `nextWord.t_dtw` (exact) | - ---- - -## Key source files - -| File | Purpose | -|------|---------| -| `remotion/Composition.tsx` | Root composition, duration calc, asset loading | -| `remotion/components/SegmentPlayer.tsx` | `buildSections`, `buildMainSubClips`, jump-cut player | -| `remotion/components/CameraPlayer.tsx` | `buildCameraShots`, `sourceToOutputFrame`, multi-angle viewport | -| `remotion/components/HookOverlay.tsx` | Hook captions, Techybara, hook timing | -| `remotion/types/transcript.ts` | `Segment`, `Token`, `TimeCut`, `Transcript` | -| `remotion/types/camera.ts` | `CameraProfiles`, `AngleConfig`, `SpeakerProfile`, `CameraShot`, `CropViewport` | -| `scripts/edit-transcript.js` | Sentence merging, `deriveCuts`, doc generation | -| `scripts/sync/AudioSyncer.js` | FFT sync, `syncMultiple` | -| `scripts/camera/setup-camera.js` | Frame extraction, face detection, `angles.json` | -| `app/camera/page.tsx` | Camera GUI (face box editor, angle tabs, save profiles) | -| `scripts/wizard.js` | Interactive pipeline runner | - ---- - -## Common constants - -| Constant | Value | File | -|----------|-------|------| -| `PAUSE_THRESHOLD` (sentences) | 0.8 s | `edit-transcript.js` | -| `WORD_DURATION_ESTIMATE` | 0.4 s | `edit-transcript.js` | -| `CUT_START_BIAS` | 1.0 | `edit-transcript.js` | -| `HOOK_TAIL_PAD_UNBOUNDED_SECONDS` | 0.16 s | `SegmentPlayer.tsx` | -| `HOOK_TAIL_PAD_BOUNDED_SECONDS` | 0.02 s | `SegmentPlayer.tsx` | -| `HOOK_BRIDGE_MAX_GAP_SECONDS` | 1.0 s | `SegmentPlayer.tsx` | -| `HOOK_END_FADE_FRAMES` | 12 | `SegmentPlayer.tsx` | -| `DECLICK_FRAMES` | 3 | `SegmentPlayer.tsx` | -| `MIN_WIDE_S` | 1.5 s | `CameraPlayer.tsx` | -| `MAX_CLOSEUP_S` | 20 s | `CameraPlayer.tsx` | -| `PERIODIC_WIDE_S` | 45 s | `CameraPlayer.tsx` | - ---- - -## Short-form pipeline - -Implementation plan: `docs/SHORT_FORM_WIZARD.md`. Entry point: `npm run shorts:wizard`. - -### Two paths - -**Path A — clip from longform:** `public/edit/transcript.json` must exist. User selects a -time range; wizard creates one or more clips without re-running sync or transcription. - -**Path B — dedicated portrait recording:** Own sync/transcribe/align pipeline rooted at -`public/shorts/`, then user defines clips from the result. - -### Output structure - -``` -public/shorts/ - camera-profiles.json ← shared portrait profiles for ALL clips (created once) - short-{id}/ - transcript.doc.txt ← full longform doc copy, > START / > END mark clip bounds - transcript.json ← merged short transcript - preview-cut.mp4 - # Path B only: input/ sync/ transcribe/ -``` - -### Short transcript.json extra meta fields - -``` -meta.outputAspect: "9:16" -meta.videoStart: float — clip start in source video seconds -meta.videoEnd: float — clip end in source video seconds -meta.parentTranscript: string — path to longform transcript.json (Path A only) -``` - -Segments keep absolute timestamps from the source video. `videoStart` / `videoEnd` -are derived from the `> START` / `> END` markers in the doc. - -### Short-form transcript doc - -`extract-short-doc.js` copies the full longform `transcript.doc.txt` and inserts -`> START` / `> END` around the clip range. It strips `> CAM` and `> HOOK` lines. -`> GRAPHIC` lines are optionally carried over (user-prompted at clip creation). - -The same `> START` / `> END` mechanism used in the longform editor applies here — -everything outside those markers is excluded by the merge step. - -### ShortFormClip composition - -- ID: `ShortFormClip` — registered in `remotion/Root.tsx` -- Dimensions: 1080 × 1920 @ 60 fps -- Reuses `SegmentPlayer` (jump-cuts) and `CameraPlayer` (portrait profiles) -- `CaptionOverlay` covers the full video duration (not only hook segments) -- No `PodcastIntro` - -### Portrait camera profiles - -`public/shorts/camera-profiles.json` — same schema as the longform `camera-profiles.json` -with `outputWidth: 1080, outputHeight: 1920`. Created by `portrait-camera-setup.js` once -and shared by all clips. Longform `camera-profiles.json` is the starting point for Path A. - -### Key scripts - -| Script | File | -|--------|------| -| `shorts:wizard` | `scripts/shorts-wizard.js` | -| `shorts:extract-doc` | `scripts/shorts/extract-short-doc.js` | -| `shorts:merge-doc` | `scripts/shorts/merge-short-doc.js` | -| `shorts:camera-setup` | `scripts/shorts/portrait-camera-setup.js` | +Architecture reference has moved to [CLAUDE.md](CLAUDE.md). diff --git a/CLAUDE.md b/CLAUDE.md index b1297fb..bcbce72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,14 @@ -# RAG Tech Podcast — Project Context +# DeckCreate — Architecture Reference -## Show -**RAG Tech** — biweekly tech podcast. Episodes drop every other week. +Single source of truth for AI agents and contributors. -## Cohosts +--- + +## Project: ragTech Podcast + +**RAG Tech** — biweekly tech podcast. Handle: `@ragtechdev` on Spotify · YouTube · Apple Podcasts · Instagram · TikTok · LinkedIn. + +### Cohosts | Name | Role | Image | |------|------|-------| | Natasha | Software Engineer | `public/assets/team/natasha.PNG` | @@ -12,80 +17,84 @@ All cohost images have transparent backgrounds. -## Brand -- Config: `public/brand.json` (colors, typography, logo, shape radius) +### Brand & Assets +- Config: `public/brand.json` — **being migrated to `brands/ragtech/brand.json`** (Phase 0.5) - Logo: `public/assets/logo/transparent-bg-logo.png` - Font: Nunito (variable, loaded via `remotion/loadFonts.ts`) - Mascot: **Techybara** (capybara) — PNGs in `public/assets/techybara/` +- Intro/outro music: `public/sounds/intro-outro-music.mp3` +- Background music: `public/sounds/jazz-cafe-music.mp3` -## Platforms -Spotify · YouTube · Apple Podcasts · Instagram · TikTok · LinkedIn — handle `@ragtechdev` +--- -## Key assets -| Asset | Path | -|-------|------| -| Intro/outro music | `public/sounds/intro-outro-music.mp3` | -| Background music | `public/sounds/jazz-cafe-music.mp3` | -| Techybara images | `public/assets/techybara/` | -| Cohost photos | `public/assets/team/` | -| Logo | `public/assets/logo/` | - -## Remotion compositions -| ID | Component | Notes | -|----|-----------|-------| -| `ragTechVodcast` | `MyComposition` | Full episode: hooks → intro → main video | -| `PodcastIntro` | `PodcastIntroComposition` | 7 s intro (420 frames @ 60 fps) | +## Pipeline -## Pipeline overview ``` -[sync] Audio ↔ video alignment → synced-output.mp4 -[transcribe] Whisper.cpp → token-level timestamps -[diarize] Speaker turn detection -[assign-speakers] Labels segments with speaker names -[align] WhisperX forced alignment → populates token.t_end -[edit-transcript] Merges phrases into sentences → transcript.doc.txt -Human edits doc (cuts, corrections, hooks, camera cues) -[merge-doc] Applies doc edits → transcript.json -[setup-camera] Face detection + GUI → camera-profiles.json -Remotion transcript.json + camera-profiles.json → composed video +Raw audio/video + ↓ [sync] FFT cross-correlation → synced-output-{N}.mp4 (one per angle) + ↓ [transcribe] Whisper.cpp → token-level timestamps → transcript.raw.json + ↓ [diarize] Speaker turn detection → diarization.json + ↓ [assign-speakers] Labels segments with speaker names + ↓ [align] WhisperX forced alignment → refines t_dtw, populates t_end + ↓ [edit-transcript] Merges phrases → sentences → transcript.doc.txt + transcript.json + Human edits doc (cuts, corrections, hooks, camera cues) + ↓ [merge-doc] Applies doc edits → transcript.json + ↓ [setup-camera] Face detection per angle → camera-profiles.json + ↓ Remotion transcript.json + camera-profiles.json → composed video ``` -Intermediate files: `public/transcribe/output/`. Synced video: `public/sync/output/`. +Intermediate files: `public/transcribe/output/`. Synced video(s): `public/sync/output/`. + +Entry point: `scripts/wizard.js` (60KB procedural — being replaced with typed DAG runner in Phase 2). + +--- + +## Data Schemas + +### transcript.json -## transcript.json key schema ``` meta - videoSrc?: string path relative to /public (overrides composition prop) - videoSrcs?: string[] all angle paths for multi-angle shoots - videoStart?: number source seconds — segments before excluded - videoEnd?: number source seconds — segments after excluded + videoSrc?: string path relative to /public (overrides composition src prop) + videoSrcs?: string[] all angle paths (multi-angle); used by setup-camera + videoStart?: number source seconds; segments before are excluded + videoEnd?: number source seconds; segments after are excluded fps: 60 + outputAspect?: "9:16" short-form only + segments[] - id, start, end, speaker, text, cut: boolean - tokens[]: { t_dtw, t_end?, text, cut } - cuts[]: [{ from, to }] intra-segment ranges to skip - hook? hookFrom?, hookTo? hook clip bounds - cameraCues[] explicit camera overrides (> CAM directives) + id, start, end source-video timestamps in seconds + speaker display name (e.g. "Natasha") + text human-readable sentence + cut: boolean true = entire segment removed + tokens[] + t_dtw: number word start time (WhisperX-aligned or Whisper t_dtw) + t_end?: number word end time (forced alignment only) + text: string + cut: boolean + cuts: TimeCut[] [{from, to}] intra-segment ranges to skip + hook?: boolean when true, prepended as hook/teaser before main + hookFrom?, hookTo? clip bounds within the segment (seconds) + cameraCues[] explicit camera shot overrides (> CAM directives in doc) ``` -`token.t_end` is populated only after forced alignment — enables exact cut boundaries; without it, heuristic biases apply. +`token.t_end` is populated only after forced alignment. Without it, `deriveCuts` falls back to `CUT_START_BIAS` heuristic. + +### camera-profiles.json -## camera-profiles.json key schema ```json { "sourceWidth": 1920, "sourceHeight": 1080, "outputWidth": 1920, "outputHeight": 1080, "wideViewport": { "cx": 0.5, "cy": 0.5, "w": 1, "h": 1 }, - "angles": { // multi-angle only + "angles": { "angle1": { "videoSrc": "sync/output/synced-output-1.mp4", - "sourceWidth": 1920, "sourceHeight": 1080 }, - "angle2": { "videoSrc": "sync/output/synced-output-2.mp4", "sourceWidth": 1920, "sourceHeight": 1080 } }, "speakers": { "Natasha": { "label": "Natasha", - "angleName": "angle1", // multi-angle only + "angleName": "angle1", "closeupViewport": { "cx": 0.3, "cy": 0.4, "w": 0.35, "h": 0.35 }, "portraitCx": 0.3 } @@ -93,16 +102,215 @@ segments[] } ``` -**Multi-angle rendering**: `CameraPlayer` stacks one `SegmentPlayer` per unique angle video, switches visibility via `opacity` at shot boundaries. Non-active layers are `muted`. All angles share the same jump-cut sections (cuts are audio-driven). See `AGENTS.md` for full architecture. +`CropViewport`: `cx/cy` = normalised centre (0–1), `w/h` = crop dimensions (0–1). + +--- + +## Rendering Model + +Full range `[videoStart, lastSegment.end]` plays continuously. Cuts are opt-in: + +| Source | Mechanism | +|--------|-----------| +| Entire segment removed | `segment.cut = true` | +| Intra-segment word/phrase | `segment.cuts[]` (from `{curly braces}` in doc) | +| Inter-segment silence | `merge-doc:cut-pauses` writes silence ranges into `cuts[]` | + +No implicit cuts. Gaps between segments play as silence unless `merge-doc:cut-pauses` is run. + +--- + +## Remotion Component Architecture + +### Compositions + +| ID | Component | Notes | +|----|-----------|-------| +| `ragTechVodcast` | `MyComposition` | Full episode: hooks → intro → main video | +| `PodcastIntro` | `PodcastIntroComposition` | 7 s intro (420 frames @ 60 fps) | +| `ShortFormClip` | `ShortFormClip` | 1080 × 1920 @ 60 fps portrait | + +### Data flow + +``` +transcript.json + → getActiveSegments() filter by meta.videoStart/videoEnd + → buildSections() → {hookSections[], mainSections[]} + → SegmentPlayer / CameraPlayer + → SectionGroupPlayer OffthreadVideo + trimBefore per section +``` + +### Key components + +- **`SegmentPlayer`** — `buildSections`, `buildMainSubClips`, jump-cut engine. At frame `f`: `sourceFrame = S(f)` via summed section durations. +- **`CameraPlayer`** — `buildCameraShots`, `sourceToOutputFrame`, multi-angle viewport. Stacks one `SegmentPlayer` per unique angle; active angle `opacity:1`, others `opacity:0, muted`. +- **`SectionGroupPlayer`** — renders `OffthreadVideo` with `trimBefore`/`trimAfter` per section. +- **`HookOverlay`** — hook karaoke captions, Techybara mascot, hook timing. +- **`CaptionOverlay`** — short-form full-duration captions. +- **`OverlayRenderer`** — dispatches `GraphicsCue` → component; currently uses `React.FC<any>` (fixed in Phase 5). + +**Viewport transform:** +``` +scale = max(outW / (srcW × vp.w), outH / (srcH × vp.h)) +tx = (0.5 - vp.cx) × 100% +ty = (0.5 - vp.cy) × 100% +→ CSS: scale(${scale}) translate(${tx}%, ${ty}%) +``` + +### Known correctness bugs (Phase 5 targets) + +- `hookClipEnd()` has 4 separate implementations (`CameraPlayer`, `SegmentPlayer`, `Composition`, `HookOverlay`) — can disagree by 1–3 frames. Fix: `remotion/lib/hookTiming.ts`. +- `buildCaptions()` duplicated across `HookOverlay` and `CaptionOverlay`. Fix: `remotion/lib/captions.ts`. +- No `OverlayErrorBoundary` — overlay crash kills the composition. +- No transcript validation on load. + +--- + +## Common Constants + +| Constant | Value | File | +|----------|-------|------| +| `PAUSE_THRESHOLD` | 0.8 s | `edit-transcript.js` | +| `WORD_DURATION_ESTIMATE` | 0.4 s | `edit-transcript.js` | +| `CUT_START_BIAS` | 1.0 | `edit-transcript.js` | +| `HOOK_TAIL_PAD_UNBOUNDED_SECONDS` | 0.16 s | `SegmentPlayer.tsx` | +| `HOOK_TAIL_PAD_BOUNDED_SECONDS` | 0.02 s | `SegmentPlayer.tsx` | +| `HOOK_BRIDGE_MAX_GAP_SECONDS` | 1.0 s | `SegmentPlayer.tsx` | +| `HOOK_END_FADE_FRAMES` | 12 | `SegmentPlayer.tsx` | +| `DECLICK_FRAMES` | 3 | `SegmentPlayer.tsx` | +| `MIN_WIDE_S` | 1.5 s | `CameraPlayer.tsx` | +| `MAX_CLOSEUP_S` | 20 s | `CameraPlayer.tsx` | +| `PERIODIC_WIDE_S` | 45 s | `CameraPlayer.tsx` | + +--- + +## Key Source Files + +| File | Purpose | Refactor note | +|------|---------|---------------| +| `remotion/Composition.tsx` | Root composition, duration calc, asset loading | Add transcript validation (Phase 5) | +| `remotion/components/SegmentPlayer.tsx` | Jump-cut player, section builders | Extract hookTiming, captions (Phase 5) | +| `remotion/components/CameraPlayer.tsx` | Camera shots, multi-angle viewport (779 lines) | Extract cameraShots lib → <350 lines (Phase 6) | +| `remotion/components/HookOverlay.tsx` | Hook captions, Techybara (518 lines) | Extract captions.ts (Phase 5) | +| `remotion/components/OverlayRenderer.tsx` | Graphics cue dispatcher | Remove brand hardcoding (Phase 0.5+5) | +| `remotion/types/transcript.ts` | `Segment`, `Token`, `TimeCut`, `Transcript` | Will import from `scripts/types/` (Phase 6) | +| `remotion/types/camera.ts` | `CameraProfiles`, `CameraShot`, `CropViewport` | Will import from `scripts/types/` (Phase 6) | +| `remotion/types/brand.ts` | Brand design tokens only | Extend with identity/hosts/mascot/audio (Phase 0.5) | +| `scripts/edit-transcript.js` | Sentence merging, `deriveCuts`, doc generation | Migrate to .ts (Phase 3) | +| `scripts/sync/AudioSyncer.js` | FFT sync, `syncMultiple` | Add FFT tie-breaking (Phase 0) | +| `scripts/wizard.js` | Interactive pipeline runner (60KB) | Replace with DAG runner (Phase 2) | +| `scripts/camera/setup-camera.js` | Frame extraction, face detection, `angles.json` | | +| `app/camera/page.tsx` | Camera GUI (face box editor, angle tabs) | | +| `app/editor/page.tsx` | Transcript editor | Add PreviewPlayer + scroll-sync (Phase 8) | +| `app/editor/Timeline.tsx` | Timeline component (630 lines) | Decompose + add waveform (Phase 8) | +| `app/components/AutoCarouselForm.tsx` | Carousel generator (810 lines) | Decompose (Phase 8) | +| `app/context/AuthContext.tsx` | Auth context with hardcoded credentials | Fix (Phase 7) | +| `vscode-transcript-language/src/extension.js` | VSCode transcript extension | Add Wrap-in-cut command (Phase 0.5) | + +--- + +## Short-form Pipeline + +Entry: `npm run shorts:wizard`. Two paths: + +**Path A — clip from longform:** `public/edit/transcript.json` must exist. User selects time range; wizard creates clips without re-running sync/transcription. + +**Path B — dedicated portrait recording:** Own sync/transcribe/align pipeline rooted at `public/shorts/`. + +Output: `public/shorts/short-{id}/transcript.json` with `meta.outputAspect: "9:16"`, `meta.videoStart/videoEnd`, `meta.parentTranscript` (Path A only). Segments keep absolute timestamps from source. + +Scripts: `scripts/shorts/extract-short-doc.js`, `scripts/shorts/merge-short-doc.js`, `scripts/shorts/portrait-camera-setup.js`. + +--- + +## Refactor Plan + +**Master doc:** `docs/PRODUCTION_REFACTOR_PLAN.md` — read in full before starting any phase. + +Core problems being fixed: non-deterministic output, no project file, no pipeline DAG, no type safety across scripts, `hookClipEnd()` bug in 4 files, brand content hardcoded in TypeScript, no GPU acceleration. + +### Phase map + +| Phase | Branch | Goal | +|-------|--------|------| +| 0 | `refactor/p0-project-file` | `.ragtech/project.json`, deterministic runs, content-addressed artifacts | +| 0.5 | `refactor/p0-brand` | `brands/ragtech/` directory, extended Brand type, brand abstraction | +| 1 | `refactor/p1-hardware` | `scripts/config/hardware.ts`, GPU-accelerated FFmpeg encode/decode | +| 2 | `refactor/p2-dag` | Typed pipeline DAG replaces `wizard.js`; AI hook suggestions; captions export | +| 3 | `refactor/p3-scripts-ts` | Migrate 53 `.js` scripts → `.ts strict` | +| 4 | `refactor/p4-scripts-tests` | Unit tests, ≥60% coverage on `scripts/**/*.ts` | +| 5 | `refactor/p5-remotion-correctness` | Fix hook timing bug, add error boundaries, transcript validation | +| 6 | `refactor/p6-remotion-arch` | `BrandContext`, extract `cameraShots.ts`, merge duplicate components | +| 7 | `refactor/p7-app-api` | Remove hardcoded credentials, `withErrorHandler` middleware | +| 8 | `refactor/p8-app-components` | `PreviewPlayer`, waveform, scroll-sync, decompose large components | +| 9 | `refactor/p9-polish` | ESLint `no-explicit-any`, dead code removal | + +Phases 0–4 (scripts) and 5–6 (Remotion) can proceed on separate branches in parallel. + +### Target directory additions (post-refactor) + +``` +.ragtech/ + project.json episode metadata, tool versions, run parameters + artifacts/{sha256}.mp4 content-addressed artifact store + runs/{timestamp}/ run logs per pipeline stage + +brands/ + ragtech/ + brand.json extended Brand config (identity, hosts, mascot, audio) + assets/ team/, logo/, techybara/, episodes/ + sounds/ + components/ brand-specific overlays (AIOverlay, RagtechOverlay, etc.) + +scripts/ + types/ shared TS interfaces (imported by scripts + remotion) + config/ project.ts, hardware.ts, paths.ts, parseArgs.ts, artifacts.ts + pipeline/ dag.ts, runner.ts, nodes/{sync,transcribe,...}.ts + +remotion/ + lib/ hookTiming.ts, captions.ts, cameraShots.ts, constants.ts + context/ BrandContext.tsx + types/overlayProps.ts discriminated union for all overlay prop types +``` + +--- + +## Testing + +**Standards doc:** `docs/TESTING_STANDARDS.md` — read before writing any test. + +Three test types, three runners: + +| Type | Location | Runner | Command | +|------|----------|--------|---------| +| Unit (scripts/pipeline) | `scripts/**/*.test.ts` | Jest `node` project | `npm test` | +| Unit (React components) | `app/**/*.test.tsx`, `remotion/**/*.test.tsx` | Jest `react` project | `npm run test:react` | +| Integration | `tests/integration/**/*.test.ts` | Jest `node` project | `npm run test:integration` | +| E2E | `e2e/**/*.test.ts` | Playwright | `npm run test:e2e` | + +**First-time Playwright setup:** `npx playwright install chromium` + +Every new pure function gets a unit test. Every new React component gets a smoke render test. E2E tests are required for Phase 8 user-facing features. Coverage thresholds are defined per phase in the standards doc. + +--- + +## Agent Implementation Convention + +For any multi-step task: +- Write an implementation doc in `docs/implementation-guides/` before starting +- Each step must have a **Status check** — a single command or file-existence test that confirms it is done +- One isolated commit per step; commit message slug must match the doc heading exactly +- Never combine steps — isolation enables partial recovery -## Agent implementation convention +**Resuming interrupted work:** +1. `git log --oneline` — see completed commits +2. Open the relevant `docs/implementation-guides/` file +3. Match the last commit slug against doc headings → continue from next unstarted step -For any multi-step implementation task: -- Break work into isolated commits. Each commit covers one coherent unit and must be - independently testable before the next begins. -- Write an implementation doc in `docs/` before starting. Each step in the doc must have - a "Status check" so a resuming agent can verify it is already done. -- Commit message slugs must match the doc headings exactly. -- To resume interrupted work: run `git log --oneline`, open the relevant `docs/` file, - find the last matching slug, continue from the next step. -- Never combine steps into one commit. Isolation is intentional — it enables partial recovery. +**Per-PR checklist:** +1. Behaviour parity — smoke-test relevant `npm run` scripts or Remotion compositions +2. `npm run test` passes +3. `tsc --noEmit` passes (where applicable) +4. Scope discipline — only files listed in the implementation doc touched +5. No new hardcoded paths in `scripts/`; no new duplicated timing constants in `remotion/` +6. *(Remotion phases)* — `remotion studio` launches; frame comparison against `docs/render-baselines/` diff --git a/docs/PRODUCTION_REFACTOR_PLAN.md b/docs/PRODUCTION_REFACTOR_PLAN.md new file mode 100644 index 0000000..089b6b0 --- /dev/null +++ b/docs/PRODUCTION_REFACTOR_PLAN.md @@ -0,0 +1,748 @@ +# DeckCreate — Production Architecture Overhaul + +> **For agents picking this up:** This is the master refactor plan. Read it fully before starting any phase. Each phase has explicit status checks so you can verify prior work and resume correctly. Follow the CLAUDE.md commit convention: one isolated commit per step, commit slugs matching doc headings exactly. + +--- + +## Critical Assessment + +This app works, but it is not production software by professional video tool standards. The core problems are architectural, not cosmetic: + +1. **Non-deterministic output.** Re-running the pipeline on the same inputs can produce different output frames. Whisper model versions are not recorded, the diarization seed is not pinned, FFT peak selection has no tie-breaking, and the timestamp offset flag is not persisted in the JSON it affects. No professional video tool allows this. + +2. **No project model.** DaVinci Resolve has a `.drp` file. Premiere has `.prproj`. This app has no equivalent — tool versions, codec parameters, pipeline flags, and artifact lineage exist only in the heads of the people who ran the wizard. Re-rendering episode 12 from three months ago is impossible. + +3. **The rendering engine is fundamentally throughput-limited.** Remotion renders via headless Chromium (Puppeteer) — each frame is rendered, extracted via IPC, PNG-encoded, and piped to FFmpeg. The practical ceiling is 2–5 fps on a MacBook Air M3. A 60-minute episode at 60fps (216,000 frames) takes 6–18 hours. This is not a configuration issue; it is inherent to the Puppeteer architecture. + +4. **GPU acceleration is almost entirely missing.** M3 Metal, VideoToolbox, NVIDIA NVENC/NVDEC, CUDA FFT for sync, Core ML for face detection — none of it is used. The app leaves 5–50× speedups on the table. + +5. **Hook timing logic has three separate implementations.** `CameraPlayer`, `SegmentPlayer`, and `Composition` all implement `hookClipEnd()` with slightly different formulas. They can disagree by 1–3 frames per hook segment. This is a correctness bug, not a style issue. + +6. **No dependency graph.** The wizard runs steps serially with a "jump to step" menu, but has no idea which downstream artifacts are invalidated when you re-run sync. The DAG is implied, not enforced. + +7. **Scripts are untested, untyped, and not structured for resumption.** 53 scripts with hardcoded paths, hand-rolled argument parsing, and no shared error handling. A crash mid-encode leaves orphaned temp files and broken outputs. + +8. **All brand content is hardcoded.** 45+ host name references, 19 hardcoded mascot paths, terminal prompts, social handles — in TypeScript files, not config. Supporting a second brand requires code changes, not file creation. + +--- + +## Target Standard + +Match what a single-developer professional tool (Kdenlive, Gyroflow-style FFmpeg pipeline) achieves: + +- **Frame-exact, byte-reproducible output** given the same inputs and tool versions +- **Content-addressed artifacts** — every intermediate file named by SHA-256 hash +- **A project file** capturing tool versions, model hashes, pipeline parameters +- **GPU-accelerated encode/decode** with automatic hardware detection (M3 VideoToolbox, NVIDIA NVENC/NVDEC) +- **ML models pinned by hash**, downloaded on first use +- **A proper DAG** so re-running sync automatically marks downstream artifacts stale +- **Typed, validated data contracts** between every pipeline stage +- **Multi-brand ready** — adding a new brand client requires creating files, not modifying code + +--- + +## Architecture: What To Build + +### Layer 1 — Project File (`.ragtech/project.json`) + +Every episode has a project directory: + +``` +.ragtech/ + project.json — episode metadata, tool versions, run parameters + artifacts/ + {sha256[:12]}.mp4 — content-addressed video artifacts + {sha256[:12]}.json — content-addressed JSON artifacts + runs/ + {iso-timestamp}/ — run log: which scripts, which flags, which outputs +``` + +`project.json` schema: +```json +{ + "version": "1", + "episode": { "id": "ep42", "title": "...", "fps": 60 }, + "brandId": "ragtech", + "tools": { + "whisper_cpp": "1.5.5", + "whisper_model": "medium.en", + "whisper_model_sha256": "abc123...", + "whisperx": "3.1.2", + "pyannote": "3.1.0", + "ffmpeg": "7.1", + "remotion": "4.0.451" + }, + "params": { + "timestamp_offset": 0, + "diarization_seed": 42, + "num_speakers": 3, + "sync_window_seconds": null + }, + "artifacts": { + "raw_video": [{ "path": "input/raw.mp4", "sha256": "..." }], + "synced": { "artifact": "abc123", "created": "2026-05-07T..." }, + "transcript_raw": { "artifact": "def456", "created": "..." }, + "transcript_aligned": { "artifact": "ghi789", "created": "..." }, + "transcript_final": { "artifact": "jkl012", "created": "..." }, + "camera_profiles": { "artifact": "mno345", "created": "..." } + } +} +``` + +### Layer 2 — Pipeline DAG + +Replace `wizard.js` (60KB procedural script) with a typed pipeline runner: + +``` + raw video + audio + │ + [sync] ←── diarization_seed, sync_window_seconds + │ + synced-output.mp4 + │ + [transcribe] ←── whisper_model, timestamp_offset + │ + transcript.raw.json + │ │ + [diarize] [align] ←── whisperx_model + │ │ + diarization.json transcript.aligned.json + │ + [assign-speakers] + │ + transcript.assigned.json + │ + [edit-transcript] + │ + transcript.doc.txt ←── human edit + │ + [merge-doc] + │ + transcript.final.json + │ + [camera-setup] ←── face detection model + │ + camera-profiles.json + │ + [render] ←── remotion, ffmpeg, hardware flags + │ + episode.mp4 +``` + +Each node: declares its inputs (content hashes) and outputs (content hashes), checks if outputs are already cached (hash match) → skips if yes, writes a run log on completion. + +Implementation: `scripts/pipeline/dag.ts` — a lightweight DAG runner, not a full build tool. + +### Layer 3 — Deterministic Reproducibility + +| Issue | Fix | +|-------|-----| +| Diarization seed not pinned | Add `diarization_seed: 42` to `project.json`; pass to `run_diarize.py` | +| Whisper model version not recorded | Store `whisper_model_sha256` in `project.json`; verify on each run | +| Timestamp offset not persisted | Store in `project.json` params; never a CLI flag | +| FFT peak selection non-deterministic | Add tie-breaking: prefer earliest peak when SNR within 0.5 of max | +| Floating-point lag → frame conversion | Round lag to nearest frame boundary at sync time; store as integer frames | +| Camera face detection non-deterministic | Store detection results in artifact; only re-run if source video changes | +| Alignment coverage threshold | Make `ALIGNMENT_COVERAGE_THRESHOLD = 0.35` explicit in `project.json` params | + +### Layer 4 — GPU Acceleration + +All FFmpeg invocations go through a single `buildFfmpegArgs(profile, task)` function in `scripts/config/hardware.ts`. No more scattered platform checks. + +| Component | M3 Mac | NVIDIA RTX 5050 | Current | +|-----------|--------|-----------------|---------| +| H.264 encode | `h264_videotoolbox` ✓ | `h264_nvenc` — missing | `libx264` on Linux | +| H.264 decode | `videotoolbox` input | `-hwaccel cuda` — missing | software | +| HDR tonemapping | `scale_metal` (macOS 13+) | `scale_cuda` | CPU `zscale` | +| Face detection | Core ML (.mlpackage) | TensorRT (ONNX) | CPU MediaPipe | +| Whisper | Native arm64 whisper.cpp | faster-whisper + CUDA | CPU whisper.cpp | +| Diarization | CPU PyTorch (native arm64) | GPU PyTorch (`cu124`) | CPU PyTorch | +| Audio FFT (sync) | CPU FFT.js | cuFFT | CPU FFT.js | +| Remotion rendering | 6-thread CPU (max) | 6-thread CPU (no GPU path) | same | + +Note: Remotion (Puppeteer → PNG → FFmpeg) has no GPU path. Accept this. Focus GPU effort on encode/decode and ML. + +### Layer 5 — Data Contracts (Typed + Validated) + +Single source of truth: `scripts/types/` +``` +scripts/types/ + project.ts — ProjectFile interface + transcript.ts — mirrors remotion/types/transcript.ts + camera.ts — mirrors remotion/types/camera.ts + python-interop.ts — all JSON emitted by Python scripts + pipeline.ts — DAG node input/output contracts +``` + +`remotion/types/` imports from `scripts/types/` — one schema, two consumers, no duplication. + +### Layer 6 — Remotion Component Architecture + +Remotion is the right tool. CSS-based video compositions in React are maintainable. The issues are implementation quality. + +| Problem | Fix | +|---------|-----| +| `hookClipEnd()` in 4 files (correctness bug) | Single `remotion/lib/hookTiming.ts` | +| `buildCaptions()` duplicated | Single `remotion/lib/captions.ts` | +| `Brand` drilled through 4 levels | `BrandContext` + `useBrand()` hook | +| `GraphicsCue.props: Record<string,unknown>` | Discriminated union in `remotion/types/overlayProps.ts` | +| No error boundary | `OverlayErrorBoundary` wraps every overlay instance | +| `CameraPlayer` 779 lines | Extract `computeCameraShots()` → `remotion/lib/cameraShots.ts` | +| `NameTitle.tsx` + `NameTitle.short.tsx` (90% dup) | Single component with `isShortForm?: boolean` | +| No transcript validation on load | `remotion/lib/validateTranscript.ts` after `fetchJson` | + +--- + +## Language/Runtime Recommendations + +| Layer | Current | Recommendation | Reason | +|-------|---------|---------------|--------| +| Pipeline orchestration | JavaScript (untyped) | **TypeScript strict** | Type-checking across boundaries prevents silent failures | +| Video processing | FFmpeg via `child_process` | **FFmpeg via typed wrapper** | Keep FFmpeg; add hardware detection + typed command builders | +| ML: transcription | whisper.cpp (CPU) | **faster-whisper on NVIDIA; whisper.cpp arm64 on M3** | 4× speedup on CUDA; arm64 already optimal on M3 | +| ML: diarization | pyannote (CPU PyTorch) | **pyannote (GPU PyTorch on NVIDIA; CPU on M3)** | 5× speedup on GPU | +| ML: face detection | MediaPipe (CPU) | **Core ML on M3; ONNX/TensorRT on NVIDIA** | 10–50× speedup | +| Rendering | Remotion (React + Chromium) | **Keep Remotion** | Right tool for CSS-driven video overlays | +| UI | Next.js + Mantine | **Keep; add proper auth** | Appropriate for internal tooling | +| Data persistence | JSON files | **JSON + content-addressed artifact store** | Human-readable + reproducible | +| Project file | None | **`.ragtech/project.json`** | Critical missing piece | + +**Do not rewrite:** Python ML stack (pyannote, whisperx). Python owns the ML ecosystem. The JS/Python boundary is fine with typed JSON contracts on both sides. + +--- + +## AI Model Recommendations + +| Task | Current | Recommended | Why | +|------|---------|-------------|-----| +| Transcription | Whisper medium.en (whisper.cpp) | **Whisper large-v3** (faster-whisper on NVIDIA) | 30% fewer word errors; 4× faster on CUDA | +| Speaker diarization | pyannote (unpinned) | **pyannote/speaker-diarization-3.1** (pin in requirements.txt) | Better short segments; must be pinned | +| Forced alignment | WhisperX (unpinned) | **WhisperX 3.1.x** (pin + store model hash) | Alignment outputs change across versions | +| Face detection | MediaPipe BlazeFace | **MediaPipe Face Landmarker** → Core ML on M3 | 6 landmarks (better viewports); 50× faster | +| Carousel LLM | Unknown | **claude-sonnet-4-6** (hardcode model ID, never "latest") | Model changes break reproducibility | +| Thumbnail selection | Frame sampling | Keep current | Already good enough | + +--- + +## Multi-Brand Architecture + +### The Core Problem + +The current `Brand` type only covers design tokens. All brand content is hardcoded in TypeScript: +- 45+ host name references (Natasha, Saloni, Victoria) across 4 files +- 19 hardcoded Techybara asset paths across 9 components +- Terminal prompt `~/ragtech` hardcoded in 4 overlay files +- `@ragtechdev` social handle hardcoded in 2 files +- Episode grid (12 thumbnail paths) hardcoded in `PodcastIntro`/`PodcastOutro` +- Audio file paths hardcoded in 3 files + +Supporting a second brand requires code changes. That is the problem to fix. + +### Principle: Don't Build Multi-Tenancy, Build the Abstraction + +Build the file structure and type schema that makes adding a new brand a matter of creating files, not modifying existing code. + +### Brand Directory Structure + +``` +brands/ + ragtech/ ← current brand, migrated here + brand.json ← full extended Brand config + assets/ + team/ ← natasha.PNG, saloni.PNG, victoria.PNG + logo/ + techybara/ ← all mascot PNGs + episodes/ ← episode grid thumbnails + sounds/ + intro-outro-music.mp3 + jazz-cafe-music.mp3 + components/ ← brand-specific overlay components + index.ts ← exports component registry + acme-corp/ ← future client + brand.json + assets/ + sounds/ + components/ ← optional +``` + +Active brand set in `project.json` as `"brandId": "ragtech"`. + +### Extended Brand Type + +```typescript +// remotion/types/brand.ts (extended) + +export type BrandHost = { + name: string; + role: string; + imgSrc: string; // relative to brands/{brandId}/ + nameBgColor: string; +}; + +export type BrandMascot = { + enabled: boolean; + name: string; + assets: { + holdingMic?: string; + teacher?: string; + raisingHand?: string; + holdingLaptop?: string; + holdingLaptop2?: string; + sparkleEyes?: string; + [key: string]: string | undefined; + }; +}; + +export type Brand = { + // Existing (keep) + colors: BrandColors; + typography: BrandTypography; + logo: string; + shape: { borderRadius: number; borderRadiusSmall: number }; + + // NEW: Identity + identity: { + name: string; // 'RAG Tech' + terminalPath: string; // '~/ragtech' + socialHandle: string; // '@ragtechdev' + website?: string; + }; + + // NEW: Team + hosts: BrandHost[]; + + // NEW: Mascot + mascot: BrandMascot; + + // NEW: Media + audio: { + introOutroMusic: string; + backgroundMusic: string; + }; + background: { + episodeGridAssets: string[]; + }; +}; +``` + +### Overlay Classification: Core Infrastructure vs. Brand-Owned + +All existing keyword overlays are RAG Tech branded assets — they carry Techybara, the `~/ragtech` terminal chrome, and the RAG Tech visual vocabulary. They cannot be reused by a different brand. They are RAG Tech's creative work. + +**Stays in `remotion/components/` (core):** + +| Component | Why it's core | +|-----------|--------------| +| `BaseOverlay` | Pure animation + positioning infrastructure | +| `OverlayErrorBoundary` | Error handling infrastructure | +| `PodcastIntro`, `PodcastOutro`, `PodcastThumbnail` | Layout structure; parameterized by `brand.hosts`, `brand.audio`, `brand.background` | +| `NameTitle`, `ConceptExplainer`, `TextOverlay`, `CodeBlock`, `ChapterMarker`, `EpisodePill`, `HookTitle`, `ShortFormOutro`, `ImageWindowOverlay`, `GifWindowOverlay` | Layout structure; move to `remotion/components/overlays/templates/`; mascot conditional on `brand.mascot.enabled`; terminal path from `brand.identity.terminalPath` | +| `OverlayRenderer` | Dispatch system; brand overlays injected, not hardcoded | + +**Moves to `brands/ragtech/components/`:** + +| Component | Why it's brand-specific | +|-----------|------------------------| +| `AIOverlay`, `AwardsOverlay`, `CodingOverlay`, `EngineeringOverlay`, `FrameworkOverlay`, `LanguageOverlay`, `InfrastructureOverlay`, `EducationOverlay`, `BestPracticesOverlay`, `RolesOverlay` | RAG Tech's keyword vocabulary — different show = different topics | +| `RagtechOverlay` | Brand introduction overlay — every brand builds their own | +| All `*/ragtech/` overlays | Already brand-namespaced | + +### The Overlay Registry + +```typescript +// remotion/components/OverlayRenderer.tsx +import { CORE_TEMPLATE_MAP } from './overlays/templates'; +import { getBrandOverlays } from '../lib/brandRegistry'; + +const COMPONENT_MAP = { + ...CORE_TEMPLATE_MAP, + ...getBrandOverlays(brand.id), +}; +``` + +```typescript +// remotion/lib/brandRegistry.ts +export function getBrandOverlays(brandId: string) { + // Static imports — esbuild tree-shakes unused branches + if (brandId === 'ragtech') { + return require('../../brands/ragtech/components').default; + } + // Future brands: add an if-branch here + return {}; +} +``` + +Static build-time imports only — no dynamic `import()`, no runtime plugins. + +### How a New Brand Gets Overlays + +- **Option A — Custom:** Build `brands/{clientId}/components/` with their mascot and keyword vocabulary. Maximum creative expression. +- **Option B — Templates only:** Use core template overlays styled via `brand.json`. Set `mascot.enabled: false`. Zero custom code. +- **Option C — Hybrid:** Core templates + one custom brand-intro overlay. + +When a client signs: create `brands/{clientId}/`, fill `brand.json`, add assets, optionally add components, add one `if`-branch in `brandRegistry.ts`. No existing file changes. + +--- + +## Editor Strategy: Code + Web Hybrid (Gap Analysis Integration) + +Based on the Descript comparison in `docs/research/TRANSCRIPT_EDITOR_GAP_ANALYSIS.md`, the optimal strategy is a hybrid approach: + +### Code Editor (VSCode) Remains Primary For: +- Text corrections and transcript editing +- Hook placement and content decisions +- Camera directives and speaker assignments +- Graphics and overlay markup +- All creative decisions that map to named doc directives + +**Why:** The code editor already provides autocomplete, syntax highlighting, and VSCode's native undo/redo. Most "smart" features (filler detection, silence removal) are already implemented in the pipeline — the gap is UI discoverability, not capability. + +### Web Editor Focuses On Three Critical Features (in priority order): +1. **`PreviewPlayer`** — Real-time video preview of cuts using `HTMLVideoElement` seek + `timeupdate`. This alone makes the editor feel like Descript's core experience. All required data exists in `transcript.json`. + +2. **Waveform visualization** — Pre-computed during sync pipeline, rendered in `timelineCanvas.ts`. Makes silence and breath visible without listening. + +3. **Transcript text panel with scroll-sync** — Read-only transcript view that highlights current word during playback. Enables inline text correction and filler word review in the web editor. + +### Implementation Priority: +- **Phase 8** implements all three critical web features +- **Phase 0.5** adds VSCode extension improvements (`Wrap in cut` command, `> NOTE` support) +- **Phase 2** adds AI-powered features (hook suggestions, captions export) +- **Phase 0** enables version history/snapshots for undo/redo + +### Features Explicitly Out of Scope: +- Voice synthesis (Overdub) — Descript's moat feature, complex and expensive +- Eye contact correction — Requires per-frame ML inference +- Direct platform publishing — Lower priority than core editing experience + +--- + +## Phased Rebuild Plan + +### Phase 0 — Project File & Determinism +**Branch:** `refactor/p0-project-file` +**Doc:** `docs/implementation-guides/REFACTOR_P0_PROJECT_FILE.md` +**Goal:** Make every subsequent run reproducible + enable version history. + +Steps (each = one isolated commit): +1. Create `scripts/config/project.ts` — `ProjectFile` interface + `readProject()` / `writeProject()` helpers +2. Store `diarization_seed`, `timestamp_offset`, `num_speakers` in project file; remove from CLI flags +3. Pin all ML model versions in `requirements.txt` with `==`; add SHA verification on startup +4. Add `schema_version` and `tool_versions` to every JSON artifact written by any script +5. Add FFT tie-breaking in `AudioSyncer`: prefer earliest peak when SNR within 0.5 of max; store lag as integer frame offset, not float seconds +6. Create `scripts/config/artifacts.ts` — `storeArtifact(content): string` returns SHA256-based filename, writes to `.ragtech/artifacts/` +7. Add version history support: every `merge-doc` and web editor save snapshots transcript to `.ragtech/artifacts/` with timestamp; implement `npm run transcript:history` to list/restore snapshots + +**Status checks:** +- `node -e "require('./scripts/config/project.ts')"` loads without error +- `diff <(npm run sync && cat .ragtech/artifacts/...) <(npm run sync && cat .ragtech/artifacts/...)` → identical output on second run +- `grep "timestamp_offset" scripts/**/*.ts` → only appears in `project.ts`, nowhere else + +--- + +### Phase 0.5 — Brand Abstraction Layer +**Branch:** `refactor/p0-brand` +**Doc:** `docs/implementation-guides/REFACTOR_P0_BRAND.md` +**Goal:** All brand content out of code into config. Adding a new brand = creating files only + VSCode extension improvements. + +Steps: +1. Extend `remotion/types/brand.ts` with `identity`, `hosts`, `mascot`, `audio`, `background` types +2. Create `brands/ragtech/`; migrate `public/brand.json` → `brands/ragtech/brand.json` with all new fields; update `BrandContext` to load from `brands/{brandId}/brand.json` using `brandId` from `project.json` +3. Move all keyword overlays (`AIOverlay`, `AwardsOverlay`, `CodingOverlay`, `EngineeringOverlay`, `FrameworkOverlay`, `LanguageOverlay`, `InfrastructureOverlay`, `EducationOverlay`, `BestPracticesOverlay`, `RolesOverlay`, `RagtechOverlay`) → `brands/ragtech/components/`; export from `brands/ragtech/components/index.ts` +4. Create `remotion/lib/brandRegistry.ts` with `getBrandOverlays(brandId)` static switch +5. Update `OverlayRenderer`: remove hardcoded keyword imports; use `{ ...CORE_TEMPLATE_MAP, ...getBrandOverlays(brand.id) }` +6. Move remaining overlays to `remotion/components/overlays/templates/`; parameterize mascot with `brand.mascot.enabled` guard; replace `~/ragtech` with `brand.identity.terminalPath` +7. Update `PodcastIntro`, `PodcastOutro`, `PodcastThumbnail`: `COHOSTS` → `brand.hosts`; `EPISODES` → `brand.background.episodeGridAssets`; audio paths → `brand.audio.*` +8. **VSCode extension improvements:** Add `Wrap in cut` command (Cmd+D) to wrap selected text in `{}`; add `> NOTE` directive support; add `> SPEAKER` snippet + +**Status checks:** +- `grep -r "natasha\|saloni\|victoria\|techybara\|ragtechdev\|~/ragtech" remotion/components/` → zero results +- `grep -r "AIOverlay\|RagtechOverlay" remotion/components/OverlayRenderer.tsx` → zero results +- `tsc --noEmit` passes +- Frame comparison: RAG Tech compositions pixel-identical to pre-refactor baseline + +--- + +### Phase 1 — Hardware Detection & GPU Acceleration +**Branch:** `refactor/p1-hardware` +**Doc:** `docs/implementation-guides/REFACTOR_P1_HARDWARE.md` +**Goal:** Use the hardware you have. + +Steps: +1. `scripts/config/hardware.ts` — `detectHardware(): Promise<HardwareProfile>`; detects M3 / NVIDIA / CPU +2. `scripts/lib/ffmpeg.ts` — `buildFfmpegCommand(profile, task)` typed wrapper; NVENC, NVDEC, `scale_cuda`, `scale_metal` paths +3. Replace all inline `ffmpeg` spawn calls with `buildFfmpegCommand()` +4. Update `Dockerfile`: `ARG CUDA_ENABLED=false`; install GPU PyTorch when enabled +5. `detect-faces.py`: branch to Core ML on arm64 Darwin; CPU MediaPipe otherwise +6. `transcribe-audio.ts`: use faster-whisper when CUDA available; whisper.cpp otherwise + +**Status checks:** +- On M3: `npm run video:optimize` ffmpeg command contains `h264_videotoolbox` +- On NVIDIA Docker: command contains `h264_nvenc` +- `grep -r "process.platform" scripts/` → only appears in `hardware.ts` + +--- + +### Phase 2 — Pipeline DAG +**Branch:** `refactor/p2-dag` +**Doc:** `docs/implementation-guides/REFACTOR_P2_DAG.md` +**Goal:** Replace wizard with dependency-tracked runner + add AI-powered features. + +Steps: +1. `scripts/pipeline/dag.ts` — `PipelineNode` type: `{ id, inputs: Artifact[], outputs: Artifact[], run() }` +2. `scripts/pipeline/nodes/` — one file per stage (`sync.ts`, `transcribe.ts`, `diarize.ts`, `align.ts`, `assign.ts`, `edit.ts`, `merge.ts`, `camera.ts`, `render.ts`) +3. `scripts/pipeline/runner.ts` — checks cached outputs (hash match); skips if yes; writes run log to `.ragtech/runs/` +4. `scripts/wizard.ts` — rewritten as thin interactive wrapper over `runner.ts`; keeps existing UX +5. `npm run pipeline:status` — prints which stages are stale and why +6. Add AI hook/chapter suggestions: `scripts/suggest-hooks.ts` calling Claude API with `transcript.json`; outputs commented `> HOOK` and `> ChapterMarker` suggestions in doc or `suggestions.json` sidecar +7. Add captions export: `scripts/export-captions.ts` generating SRT/VTT from token timestamps with cut sections excluded; reuse `SegmentPlayer` section math for timing + +**Status checks:** +- Re-running sync: runner reports `transcript.raw.json` as stale, prompts re-transcription +- Skipped stage output: `✓ transcribe (cached sha:def456)` +- `.ragtech/runs/{timestamp}/run-log.json` exists after each stage + +--- + +### Phase 3 — Scripts: TypeScript + Shared Infrastructure +**Branch:** `refactor/p3-scripts-ts` +**Doc:** `docs/implementation-guides/REFACTOR_P3_SCRIPTS_TS.md` +**Goal:** Type safety and shared utilities across all 53 scripts. + +Steps: +1. `scripts/config/paths.ts` — centralized path resolver; `PATHS.transcribeInput(cwd)` etc. +2. `scripts/config/parseArgs.ts` — schema-driven CLI parser replacing 28 hand-rolled loops; generates `--help` +3. `scripts/config/exitOnError.ts` — `handleFatalError(err)` + `withCleanup(main, cleanup)` for SIGTERM/temp cleanup +4. `scripts/types/python-interop.ts` — TypeScript interfaces for all Python JSON output (`DiarizationSegment`, `AlignmentResult`, `TranscriberToken`) +5. Migrate all 53 scripts `.js` → `.ts` (dependency order: config → shared → individual scripts → `edit-transcript.ts` last) +6. Add `tsconfig.scripts.json` with `strict: true, noEmit: true`; must pass clean + +**Status checks:** +- `npx tsc -p tsconfig.scripts.json --noEmit` → zero errors +- `grep -rn "any" scripts/**/*.ts` → zero untyped `any` (use `unknown` + narrowing) +- `npm run transcribe -- --help` prints usage +- `npm run transcribe -- --unknown-flag` exits 1 with clear stderr message + +--- + +### Phase 4 — Scripts: Testability + Coverage +**Branch:** `refactor/p4-scripts-tests` +**Doc:** `docs/implementation-guides/REFACTOR_P4_SCRIPTS_TESTS.md` +**Goal:** Critical pipeline logic is unit-tested. + +Steps: +1. Add optional `{ fs, spawn }` injection to `Transcriber`, `Diarizer`, `AudioSyncer` constructors; entry points pass production defaults +2. Extract pure functions from `edit-transcript.ts`: `buildTranscript(rawJson, options)` and `mergeDoc(transcript, docText)` — no I/O +3. Write tests: `parseArgs.test.ts`, `paths.test.ts`, `exitOnError.test.ts`, `Transcriber.test.ts`, `buildTranscript.test.ts`, `mergeDoc.test.ts`, `alignTranscript.test.ts` +4. Update Jest config for `.ts` files; coverage threshold ≥60% on `scripts/**/*.ts` + +**Status checks:** +- `npm run test` passes; coverage report shows ≥60% on `scripts/**/*.ts` +- No test file uses real file I/O (all use temp dirs or mocks) +- Existing carousel/caption tests still pass + +--- + +### Phase 5 — Remotion: Fix Correctness Issues First +**Branch:** `refactor/p5-remotion-correctness` +**Doc:** `docs/implementation-guides/REFACTOR_P5_REMOTION_CORRECTNESS.md` +**Goal:** Fix the bugs before reorganizing. + +Steps: +1. Create `remotion/lib/hookTiming.ts` — extract `hookClipEnd()`, `getHookSubClips()`, `buildHookSections()` from 4 files; all import from here +2. Create `remotion/lib/captions.ts` — extract `buildCaptions()` and all token-classification helpers from `HookOverlay.tsx` + `CaptionOverlay.tsx` +3. Create `remotion/lib/constants.ts` — all timing constants (`HOOK_TAIL_PAD_*`, `HOOK_BRIDGE_*`, `FPS`, `DECLICK_FRAMES`); delete all duplicate declarations +4. Create `remotion/components/OverlayErrorBoundary.tsx` — catches render errors, shows transparent fallback; wrap every overlay instance in `OverlayRenderer` +5. Create `remotion/lib/validateTranscript.ts` — manual shape-checking; call after `fetchJson` in `Composition.tsx` and `ShortFormClip.tsx`; show error card in Studio on failure +6. `CameraPlayer`: add `console.warn` + wide-shot fallback when speaker profile missing; never silently skip + +**Status checks:** +- `grep -r "HOOK_TAIL_PAD_UNBOUNDED_SECONDS = 0.16" remotion/` → zero results +- `grep -r "hookClipEnd" remotion/components/` → zero results (only in `lib/hookTiming.ts`) +- `remotion studio` shows error card (not crash) when `segments` field is null +- Frame comparison: frames 0, first hook start, hook→main transition pixel-identical to pre-refactor baseline screenshots in `docs/render-baselines/` + +--- + +### Phase 6 — Remotion: Architecture Cleanup +**Branch:** `refactor/p6-remotion-arch` +**Doc:** `docs/implementation-guides/REFACTOR_P6_REMOTION_ARCH.md` +**Goal:** Maintainable component structure. + +Steps: +1. `BrandContext` (`remotion/context/BrandContext.tsx`) + `SectionsContext` — remove 4-level prop drilling; components call `useBrand()` / `useSections()` +2. Create `remotion/types/overlayProps.ts` — discriminated union for all overlay prop types; `OverlayRenderer` switch-narrows `cue.type`; eliminate all `React.FC<any>` +3. Merge `NameTitle.tsx` + `NameTitle.short.tsx` → single component with `isShortForm?: boolean`; `.short` is thin re-export. Same for `ConceptExplainer` +4. Extract `computeCameraShots()` + `resolveVideoSrc()` + `sourceToOutputFrame()` from `CameraPlayer.tsx` → `remotion/lib/cameraShots.ts` (pure TS, no React/Remotion imports); `CameraPlayer` under 350 lines +5. Extract intro animation helpers → `remotion/lib/introAnimations.ts` +6. `remotion/types/` imports from `scripts/types/` — no duplicate type definitions + +**Status checks:** +- `grep -r "React.FC<any>" remotion/` → zero results +- `grep -r "brand\b" remotion/components/SegmentPlayer.tsx` → zero results (uses context, not prop) +- `wc -l remotion/components/CameraPlayer.tsx` → under 350 +- `tsc --noEmit` strict: zero errors +- Pixel-identical frame comparison to Phase 5 output + +--- + +### Phase 7 — App: Auth + API Hardening +**Branch:** `refactor/p7-app-api` +**Doc:** `docs/implementation-guides/REFACTOR_P7_APP_API.md` +**Goal:** No hardcoded credentials; proper API error handling. + +Steps: +1. Remove `DEMO_CREDENTIALS` from `AuthContext.tsx`; new `POST /api/auth/login` reads `AUTH_EMAIL`/`AUTH_PASSWORD`/`SESSION_SECRET` env vars; returns httpOnly SameSite=Strict cookie session +2. `GET /api/auth/me` validates session cookie +3. `app/api/middleware.ts`: `withErrorHandler(handler)` wraps all 7 route handlers; unhandled throws → `{ error: "..." }` JSON with status 500 + +**Status checks:** +- `grep -r "DEMO_CREDENTIALS\|demo_password\|localStorage" app/` → zero results +- Route that throws returns `{ error: "..." }` JSON with status 500, not HTML +- Cookie is httpOnly in browser devtools + +--- + +### Phase 8 — App: Component Decomposition + Editor Features +**Branch:** `refactor/p8-app-components` +**Doc:** `docs/implementation-guides/REFACTOR_P8_APP_COMPONENTS.md` +**Goal:** No component over 350 lines + add critical Descript-like editing features. + +Steps: +1. `AutoCarouselForm.tsx` (810 lines) → orchestrator <150 lines + `VideoInput`, `CarouselConfig`, `ManualPromptMode`, `CarouselResults`, `SlideEditor` sub-components (each <250 lines) +2. `Timeline.tsx` (630 lines) → extract `useDragHandlers`, `useAutoScroll`, `useMarkIn` custom hooks; component under 300 lines +3. **Critical: Add PreviewPlayer** - browser video player that reads `transcript.json` cuts and plays back with sections skipped using `HTMLVideoElement` seek + `timeupdate`; port section calculation logic from `remotion/components/SegmentPlayer.tsx` +4. **Add transcript text panel with scroll-sync** - read-only transcript view that highlights current word during playback; map playback time → token → doc line +5. **Add waveform visualization** - pre-compute peak amplitude per frame during sync pipeline step; store as sidecar JSON; render in `timelineCanvas.ts` as waveform layer +6. **Add filler word review panel** - list all `{word}` cuts with per-word restore buttons +7. **Add silence removal preview** - show proposed silence cuts as pending yellow bands in timeline before user confirms +8. **Add speaker assignment UI** - right-click on segment track → "Change speaker" dropdown; writes `> SPEAKER` directive back to doc +9. **Add undo/redo functionality** - implement command pattern or `useReducer` with history stack for web editor edits +10. **Add audio scrubbing** - connect timeline drag events to `videoElement.currentTime` updates in real time + +**Status checks:** +- `wc -l app/components/AutoCarouselForm.tsx` → under 150 +- `wc -l app/editor/Timeline.tsx` → under 300 +- Full auto-carousel flow works end-to-end +- Timeline trim drags, camera cue drags, auto-scroll all function correctly + +--- + +### Phase 9 — Lint + Dead Code +**Branch:** `refactor/p9-polish` +**Doc:** `docs/implementation-guides/REFACTOR_P9_POLISH.md` +**Goal:** Enforce standards automatically. + +Steps: +1. Audit and delete confirmed-unused components: `grep -r "BaseOverlay\|TextOverlay\|IconBadge\|CodeBlock" remotion/ app/` — delete any with no imports outside their own file +2. Add ESLint rules: `@typescript-eslint/no-explicit-any` as error (in `remotion/`, `app/`), `no-console` as warning outside `scripts/` +3. Fix or suppress (with explicit comments) all new lint violations + +**Status checks:** +- `npm run lint` → zero errors +- `tsc --noEmit` → zero errors +- `remotion studio` starts; `npm run test` passes + +--- + +## Phase Dependency Order + +``` +Phase 0 (project file + determinism) +Phase 0.5 (brand abstraction) ← can run in parallel with Phase 0 + └── Phase 1 (hardware + GPU) ← independent of brand work + └── Phase 2 (pipeline DAG) + └── Phase 3 (scripts TS) + └── Phase 4 (scripts tests) + +Phase 5 (remotion correctness) ← can start after Phase 0.5 + └── Phase 6 (remotion architecture) + └── Phase 7 (app API) + └── Phase 8 (app components) + └── Phase 9 (lint) +``` + +Phases 1–4 (scripts) and Phases 5–6 (Remotion) can proceed on separate branches in parallel. + +--- + +## Per-PR Review Checklist + +Every PR must pass this before merge: + +1. **Behaviour parity** — smoke-test the `npm run` scripts or Remotion compositions relevant to this phase on a real episode +2. **No test regressions** — `npm run test` passes +3. **TypeScript** — `tsc --noEmit` passes (where applicable to the phase's layer) +4. **Scope discipline** — PR touches only files listed in the implementation doc +5. **Doc completeness** — every step in the phase's implementation doc has its Status check marked done +6. **No new hardcoded paths** in `scripts/`; no new duplicated timing constants in `remotion/` +7. *(Remotion phases)* — `remotion studio` launches; frame comparison against `docs/render-baselines/` baseline + +--- + +## Operational Continuity Rules + +- `main` is never broken — always production-safe for real episode production +- Remotion phases (5–6) merge only after `npx remotion render ragTechVodcast --frames 0,420,900` diff passes against `docs/render-baselines/` +- Python scripts are never modified (out of scope for this refactor) +- Implementation docs stay in `docs/implementation-guides/` permanently as audit trail +- **Resume interrupted work:** `git log --oneline` → match against doc headings → continue from next unstarted step + +--- + +## Render Throughput Honest Assessment + +The Puppeteer-based rendering ceiling (2–5 fps) cannot be fixed within Remotion. For 1–2 episodes per month, this is acceptable — renders run overnight. If episode volume grows: + +- **Short-term:** Run renders overnight (already the workflow) +- **Medium-term:** Remotion Lambda (serverless distributed rendering) +- **Long-term:** `renderFrames` API with max `--concurrency` on a multi-core machine (8–16 cores = 3–8× faster) + +Do not rewrite the rendering engine. Remotion is correct for this use case. + +--- + +## Agent-Readable Conventions + +Every run log written by the pipeline: +```json +{ + "stage": "transcribe", + "started": "2026-05-08T10:00:00Z", + "finished": "2026-05-08T10:08:32Z", + "inputs": [{ "artifact": "abc123", "role": "audio" }], + "outputs": [{ "artifact": "def456", "role": "transcript_raw" }], + "tool_versions": { "whisper_cpp": "1.5.5", "model_sha256": "..." }, + "params": { "model": "medium.en", "timestamp_offset": 0 } +} +``` + +Every implementation doc step: +```markdown +### Step N — [description] +[what to do] + +**Status check:** `[command]` → [expected output] +``` + +--- + +## Critical Files Reference + +| File | Problem | Phase | +|------|---------|-------| +| [scripts/wizard.js](../scripts/wizard.js) | 60KB procedural; replace with DAG runner | Phase 2 | +| [scripts/sync/AudioSyncer.js](../scripts/sync/AudioSyncer.js) | Non-deterministic FFT peak; lag stored as float | Phase 0 | +| [remotion/components/CameraPlayer.tsx](../remotion/components/CameraPlayer.tsx) | 779 lines; 4 duplicate `hookClipEnd()` | Phase 5+6 | +| [remotion/components/OverlayRenderer.tsx](../remotion/components/OverlayRenderer.tsx) | `React.FC<any>`; hardcoded brand overlay imports | Phase 0.5+5 | +| [remotion/components/HookOverlay.tsx](../remotion/components/HookOverlay.tsx) | Duplicate `buildCaptions()`; 518 lines | Phase 5 | +| [app/editor/page.tsx](../app/editor/page.tsx) | Needs PreviewPlayer and transcript text panel | Phase 8 | +| [app/editor/Timeline.tsx](../app/editor/Timeline.tsx) | 630 lines; needs waveform layer and scroll-sync | Phase 8 | +| [app/editor/timelineCanvas.ts](../app/editor/timelineCanvas.ts)) | Needs waveform draw functions | Phase 8 | +| [vscode-transcript-language/src/extension.js](../vscode-transcript-language/src/extension.js) | Missing `Wrap in cut` command and `> NOTE` support | Phase 0.5 | +| [scripts/edit-transcript.js](../scripts/edit-transcript.js) | Filler detection exists but needs discoverability | Phase 2 | +| [remotion/Composition.tsx](../remotion/Composition.tsx) | No transcript validation; duplicate hook logic | Phase 5 | +| [remotion/types/brand.ts](../remotion/types/brand.ts) | Incomplete — only design tokens | Phase 0.5 | +| [app/components/AutoCarouselForm.tsx](../app/components/AutoCarouselForm.tsx) | 810 lines; no sub-components | Phase 8 | +| [app/context/AuthContext.tsx](../app/context/AuthContext.tsx) | Hardcoded credentials | Phase 7 | +| [scripts/diarize/run_diarize.py](../scripts/diarize/run_diarize.py) | No seed pinning | Phase 0 | diff --git a/docs/REFACTOR_ISSUE_INVENTORY.md b/docs/REFACTOR_ISSUE_INVENTORY.md new file mode 100644 index 0000000..650df2f --- /dev/null +++ b/docs/REFACTOR_ISSUE_INVENTORY.md @@ -0,0 +1,114 @@ +# DeckCreate Engine — Sprint 1–7 Issue Inventory + +## Sprint 1 — Deterministic Foundation + +### 1. Create project file configuration layer +Create the `.ragtech/project.json` foundation and typed helpers for reading/writing project state. This becomes the single source of truth for pipeline parameters, tool versions, and deterministic run metadata. + +### 2. Implement content-addressed artifact storage +Create the artifact storage system that writes outputs into `.ragtech/artifacts/` using SHA-256-derived filenames. Identical content must resolve to identical artifact IDs. + +### 3. Persist pipeline parameters in project file +Move runtime parameters such as diarization seed, timestamp offset, and number of speakers out of CLI flags and into the project file so pipeline runs are reproducible. + +### 4. Add deterministic metadata to generated artifacts +Ensure every JSON artifact includes schema version and tool version metadata so outputs can be traced to exact execution conditions. + +--- + +## Sprint 2 — Hardware Abstraction + +### 5. Implement hardware detection layer +Create a centralized hardware detection module that identifies available execution environments (Apple Silicon / NVIDIA / CPU-only) and exposes a typed hardware profile. + +### 6. Build typed FFmpeg command abstraction +Create a typed FFmpeg command builder that maps encoding/decoding operations to the appropriate hardware acceleration path. + +### 7. Migrate inline FFmpeg calls to shared command builder +Replace scattered inline FFmpeg process invocations with the shared command builder so command construction is centralized. + +--- + +## Sprint 3 — Pipeline DAG Foundation + +### 8. Create DAG node contract +Define the typed node interface for the new pipeline execution model, including node identity, inputs, outputs, and run behavior. + +### 9. Implement DAG runner core +Build the DAG execution engine that resolves dependency order, executes nodes safely, and supports cached artifact reuse. + +### 10. Implement pipeline run logging +Add structured run logs under `.ragtech/runs/` capturing execution metadata, inputs, outputs, parameters, and timing. + +### 11. Rewrite wizard as thin DAG wrapper +Refactor the current wizard so it becomes a thin UX wrapper over the DAG runner instead of owning execution logic directly. + +--- + +## Sprint 4 — Pipeline Stage Migration + +### 12. Implement sync pipeline node +Move sync logic into a DAG node with declared inputs/outputs and artifact-aware execution behavior. + +### 13. Implement transcribe pipeline node +Move transcription into a DAG node that reads synced inputs and writes transcript artifacts. + +### 14. Implement diarize pipeline node +Move diarization into a DAG node with deterministic seed handling and declared artifact outputs. + +### 15. Implement align pipeline node +Move alignment into a DAG node that consumes transcription outputs and produces aligned transcript artifacts. + +--- + +## Sprint 5 — Script Infrastructure + +### 16. Create centralized path resolver +Build shared path helpers so scripts no longer hardcode file locations. + +### 17. Implement shared CLI argument parser +Create a reusable typed CLI parser to replace ad hoc argument parsing logic across scripts. + +### 18. Implement shared script error handling and cleanup +Create shared fatal error handling and cleanup utilities so interrupted runs fail consistently and safely. + +### 19. Define Python interop type contracts +Create shared TypeScript interfaces for JSON emitted by Python scripts so the JS/Python boundary becomes typed and explicit. + +--- + +## Sprint 6 — Script Testability + Determinism Hardening + +### 20. Add dependency injection to script services +Refactor core services so filesystem and subprocess behavior can be injected for testing. + +### 21. Extract pure transcript transformation functions +Extract transcript-building and merge logic into pure functions with no I/O dependencies. + +### 22. Add deterministic FFT tie-breaking in audio sync +Fix non-deterministic FFT peak selection so sync chooses a stable result when multiple peaks are near-equivalent. + +### 23. Store lag as integer frame offset +Normalize sync lag to frame-exact integer offsets instead of floating-point seconds. + +--- + +## Sprint 7 — Engine Correctness + Final Pipeline Completion + +### 24. Implement assign-speakers pipeline node +Create the speaker-assignment stage that combines transcript and diarization outputs into speaker-tagged transcript artifacts. + +### 25. Implement merge-doc pipeline node +Create the merge-doc stage that combines human-edited transcript directives with pipeline outputs into the final transcript artifact. + +### 26. Add pipeline stale-state detection +Build the dependency invalidation mechanism that determines which downstream nodes become stale when upstream artifacts change. + +### 27. Add pipeline status command +Implement a status command that reports cached, stale, and missing pipeline stages. + +### 28. Standardize artifact hashing utilities +Move hashing logic into shared reusable utilities so all artifact identity calculations follow one implementation. + +### 29. Add full pipeline determinism validation command +Create a validation command that re-runs relevant stages and confirms identical outputs across deterministic runs. \ No newline at end of file diff --git a/docs/TESTING_STANDARDS.md b/docs/TESTING_STANDARDS.md new file mode 100644 index 0000000..6ba1f93 --- /dev/null +++ b/docs/TESTING_STANDARDS.md @@ -0,0 +1,362 @@ +# Testing Standards — DeckCreate + +Single reference for what to test, where to put it, and how to write it. Both human and agentic developers must follow this before writing a test or marking a refactor phase complete. + +--- + +## Three test types, three rules + +| Type | Purpose | Scope | Runner | +|------|---------|-------|--------| +| **Unit** | One function or module in isolation | No real I/O, no network | Jest (`node` or `react` project) | +| **Integration** | Multiple modules end-to-end | Real filesystem via temp dirs; mock external services | Jest (`node` project) | +| **E2E** | User flows in the browser | Live Next.js dev server; real DOM | Playwright | + +**Rule 1:** If a function is pure (no I/O, no side effects), write a unit test. No exceptions. +**Rule 2:** If a unit test would require mocking more than two dependencies, write an integration test instead. +**Rule 3:** E2E tests cover user journeys (login → edit → save), not implementation details. + +--- + +## File locations + +``` +scripts/ + someModule.test.ts ← unit test for scripts/ TS modules (node project) + __tests__/ + SomeClass.test.js ← existing JS unit tests (node project) + +tests/ + integration/ + pipeline-dag.test.ts ← multi-stage pipeline integration tests + merge-doc.test.ts ← tests that read/write real temp files + react/ + SomeHook.test.tsx ← isolated hook tests outside app/ or remotion/ + setup.js ← Jest global setup: node project + setup.react.ts ← Jest global setup: react project + __mocks__/ + styleMock.js ← CSS module stub + fileMock.js ← static asset stub + +app/ + components/ + MyComponent.test.tsx ← component unit tests (react project) + editor/ + Timeline.test.tsx + +remotion/ + lib/ + hookTiming.test.ts ← pure logic unit tests (node project) + captions.test.ts + +e2e/ + smoke.test.ts ← routes respond, no server errors + flows/ + editor-flow.test.ts ← full user editing journey + carousel-flow.test.ts +``` + +**Naming:** test file lives next to the file it tests, same name + `.test.{ts,tsx}`. Exception: integration tests always go in `tests/integration/`. + +--- + +## Running tests + +```bash +npm test # all Jest tests (node + react projects) +npm run test:unit # alias for above +npm run test:react # react project only (app/ + remotion/) +npm run test:integration # integration tests only +npm run test:coverage # all Jest tests + coverage report +npm run test:watch # re-run on file change (dev loop) + +npm run test:e2e # Playwright (requires running dev server or auto-starts it) +npm run test:e2e:ui # Playwright interactive UI +npm run test:e2e:debug # Playwright step-through debugger + +npm run test:all # Jest + Playwright (CI equivalent) +``` + +**First-time Playwright setup** (browsers not bundled, run once per machine): +```bash +npx playwright install chromium +``` + +--- + +## Unit tests — scripts and pipeline logic + +Target: `scripts/**/*.{js,ts}`, `remotion/lib/**/*.ts` + +### Pattern: dependency injection + +Scripts receive `fs`, `spawn`, etc. as constructor arguments so tests can inject fakes. + +```typescript +// scripts/pipeline/nodes/transcribe.ts +export class Transcriber { + constructor( + private readonly fs: typeof import('fs-extra') = fse, + private readonly spawn: typeof import('child_process').spawn = cp.spawn, + ) {} + + async run(inputPath: string): Promise<TranscriptRaw> { ... } +} +``` + +```typescript +// scripts/pipeline/nodes/transcribe.test.ts +import { Transcriber } from './transcribe'; +import { createFakeFs } from '../../tests/helpers/fakeFs'; + +describe('Transcriber', () => { + it('writes transcript.raw.json to the artifact store', async () => { + const fakeFs = createFakeFs({ '/input/audio.wav': Buffer.from('') }); + const t = new Transcriber(fakeFs, jest.fn()); + const result = await t.run('/input/audio.wav'); + expect(fakeFs.written['.ragtech/artifacts/']).toBeDefined(); + expect(result.segments).toBeInstanceOf(Array); + }); +}); +``` + +### Pattern: pure function tests + +```typescript +// remotion/lib/hookTiming.test.ts +import { hookClipEnd, buildHookSections } from './hookTiming'; + +describe('hookClipEnd', () => { + it('returns hookTo when bounded', () => { + expect(hookClipEnd({ hookFrom: 10, hookTo: 20 }, 60)).toBe(20); + }); + + it('pads by HOOK_TAIL_PAD_UNBOUNDED_SECONDS when hookTo is absent', () => { + const end = hookClipEnd({ hookFrom: 10 }, 60); + expect(end).toBeCloseTo(10 + 0.16, 5); + }); +}); +``` + +### Coverage thresholds (per refactor phase) + +| Phase | Target files | Minimum coverage | +|-------|-------------|-----------------| +| Phase 4 | `scripts/**/*.ts` | 60% | +| Phase 5 | `remotion/lib/**/*.ts` | 60% | +| Phase 7 | `app/api/**/*.ts` | 70% | +| Phase 8 | `app/components/**/*.tsx` | 50% | + +Run `npm run test:coverage` and check the text summary. The `coverage/lcov-report/index.html` gives per-file line-by-line detail. + +--- + +## Unit tests — React components + +Target: `app/**/*.tsx`, and any React components in `remotion/` that have extractable logic. + +### Pattern: render and assert + +```tsx +// app/components/EpisodePill.test.tsx +import { render, screen } from '@testing-library/react'; +import { EpisodePill } from './EpisodePill'; + +describe('EpisodePill', () => { + it('displays the episode number', () => { + render(<EpisodePill episode={42} />); + expect(screen.getByText('EP 42')).toBeInTheDocument(); + }); +}); +``` + +### Pattern: user interaction + +```tsx +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { FillerWordPanel } from './FillerWordPanel'; + +describe('FillerWordPanel', () => { + it('calls onRestore when restore button is clicked', async () => { + const onRestore = jest.fn(); + render(<FillerWordPanel words={[{ id: '1', text: 'um' }]} onRestore={onRestore} />); + await userEvent.click(screen.getByRole('button', { name: /restore/i })); + expect(onRestore).toHaveBeenCalledWith('1'); + }); +}); +``` + +### What NOT to test in React unit tests + +- Remotion composition output (too complex; requires full Remotion runtime) +- CSS pixel values (test behaviour, not style) +- Implementation details (internal state, private methods) + +### Mocking Remotion hooks + +When a component uses `useCurrentFrame` or `useVideoConfig`, mock the module: + +```tsx +jest.mock('remotion', () => ({ + useCurrentFrame: () => 0, + useVideoConfig: () => ({ fps: 60, durationInFrames: 3600, width: 1920, height: 1080 }), + Sequence: ({ children }: { children: React.ReactNode }) => <>{children}</>, +})); +``` + +--- + +## Integration tests + +Target: `tests/integration/**/*.test.{js,ts}` + +Integration tests are allowed to: +- Read and write real files in `os.tmpdir()` temp directories +- Spawn real child processes if the binary is available (FFmpeg, Python) +- Test multiple modules interacting + +Integration tests must NOT: +- Hit the internet +- Depend on `public/` content that may not be present in CI +- Leave files behind (always clean up in `afterAll`) + +### Pattern: temp directory + real pipeline stage + +```typescript +// tests/integration/merge-doc.test.ts +import os from 'os'; +import path from 'path'; +import fse from 'fs-extra'; +import { mergeDoc } from '../../scripts/edit-transcript'; + +describe('mergeDoc (integration)', () => { + let tmpDir: string; + + beforeAll(async () => { + tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'deckcreate-test-')); + }); + + afterAll(async () => { + await fse.remove(tmpDir); + }); + + it('applies cut markers from doc to transcript', async () => { + const transcriptPath = path.join(tmpDir, 'transcript.json'); + await fse.writeJson(transcriptPath, sampleTranscript); + const result = await mergeDoc(transcriptPath, sampleDoc); + expect(result.segments.filter((s) => s.cut)).toHaveLength(2); + }); +}); +``` + +--- + +## E2E tests + +Target: `e2e/**/*.test.ts` + +### Structure + +``` +e2e/ + smoke.test.ts ← route availability (< 10 s total) + flows/ + auth-flow.test.ts ← login / logout + editor-flow.test.ts ← open editor, make a cut, save + carousel-flow.test.ts +``` + +### Pattern: page object model + +For flows with more than 5 interactions, extract a page object: + +```typescript +// e2e/pages/EditorPage.ts +import { Page } from '@playwright/test'; + +export class EditorPage { + constructor(private page: Page) {} + + async goto() { + await this.page.goto('/editor'); + } + + async markCut(wordText: string) { + await this.page.getByText(wordText).click({ button: 'right' }); + await this.page.getByRole('menuitem', { name: 'Cut word' }).click(); + } + + async save() { + await this.page.getByRole('button', { name: 'Save' }).click(); + await this.page.waitForResponse('/api/transcript'); + } +} +``` + +```typescript +// e2e/flows/editor-flow.test.ts +import { test, expect } from '@playwright/test'; +import { EditorPage } from '../pages/EditorPage'; + +test('user can mark a word as cut', async ({ page }) => { + const editor = new EditorPage(page); + await editor.goto(); + await editor.markCut('um'); + await editor.save(); + await expect(page.getByText('Saved')).toBeVisible(); +}); +``` + +### E2E conventions + +- Each test is independent — no shared state between tests +- Use `page.goto()` at the start of every test (not `beforeEach`) +- Prefer role-based locators (`getByRole`, `getByLabel`) over CSS selectors +- Assert on visible user-facing text, not internal DOM structure +- Keep smoke tests under 10 seconds total; flow tests under 60 seconds each + +--- + +## Mocking philosophy + +| What | How | +|------|-----| +| File system (unit) | Inject fake `fs` via constructor | +| File system (integration) | Real `os.tmpdir()` + cleanup | +| External HTTP (YouTube, Claude API) | `jest.fn()` on fetch or module mock | +| FFmpeg / Python processes | `jest.fn()` on `spawn` (unit); real binary (integration, skip if missing) | +| Sharp, Puppeteer | Global mock in `tests/setup.js` | +| Next.js router / image | Global mock in `tests/setup.react.ts` | +| Remotion hooks | Per-test `jest.mock('remotion', ...)` | +| Browser / DOM | jsdom via `react` Jest project; Playwright for real browser | + +**Never** mock the module being tested. Only mock its dependencies. + +--- + +## Pre-commit behaviour + +The pre-commit hook runs `jest --findRelatedTests` on staged `.js/.ts/.tsx` files. This means: + +- Editing `scripts/edit-transcript.ts` → automatically runs `edit-transcript.test.ts` +- Editing `app/components/Timeline.tsx` → automatically runs `Timeline.test.tsx` (once it exists) +- Tests for unstaged files are NOT run (use `npm test` for full suite) + +The hook uses `--passWithNoTests` — committing a file with no test does not fail. This is intentional: test coverage gaps are caught by the coverage threshold in CI, not the commit hook. + +--- + +## Agent implementation checklist + +When implementing a refactor phase step that involves new logic: + +1. Write the test file **before** or **alongside** the implementation (not after) +2. Test file location follows the conventions above +3. Every new pure function gets at least one happy-path and one edge-case test +4. Every new React component gets a smoke render test (`render(...)` does not throw) +5. Run `npm test` before committing — all projects must pass +6. Check coverage with `npm run test:coverage` if the phase has a coverage target +7. E2E tests are only required for Phase 8 user-facing features + +When in doubt: a failing test is better than no test. diff --git a/docs/PROXY_CONFORM_WORKFLOW.md b/docs/implementation-guides/PROXY_CONFORM_WORKFLOW.md similarity index 100% rename from docs/PROXY_CONFORM_WORKFLOW.md rename to docs/implementation-guides/PROXY_CONFORM_WORKFLOW.md diff --git a/docs/SHORT_FORM_WIZARD.md b/docs/implementation-guides/SHORT_FORM_WIZARD.md similarity index 100% rename from docs/SHORT_FORM_WIZARD.md rename to docs/implementation-guides/SHORT_FORM_WIZARD.md diff --git a/docs/TRANSCRIPTION_RENDER_PERF.md b/docs/implementation-guides/TRANSCRIPTION_RENDER_PERF.md similarity index 100% rename from docs/TRANSCRIPTION_RENDER_PERF.md rename to docs/implementation-guides/TRANSCRIPTION_RENDER_PERF.md diff --git a/docs/research/DAYDREAM_SCOPE_RESEARCH.md b/docs/research/DAYDREAM_SCOPE_RESEARCH.md new file mode 100644 index 0000000..fcf0993 --- /dev/null +++ b/docs/research/DAYDREAM_SCOPE_RESEARCH.md @@ -0,0 +1,642 @@ +# Daydream Scope — Research Reference + +> Written: 2026-05-08. Open-source; all detail sourced from `github.com/daydreamlive/scope`. No need to re-visit source URLs. +> +> **Note:** Daydream Scope (`daydream.live`) is a separate product from Daydream Video (`daydreamvideo.com`) — they share a brand name but are architecturally unrelated. See [DAYDREAM_VIDEO_RESEARCH.md](DAYDREAM_VIDEO_RESEARCH.md) for the closed-source post-production editor. + +--- + +## What It Does + +Daydream Scope transforms live video inputs into AI-generated outputs in real time using autoregressive video diffusion models. Primary use cases: live performance (VJing), immersive installations, music-reactive visuals, AI-assisted live streaming. + +**Core user-facing features:** +- Real-time video-to-video and text-to-video generation at interactive framerates +- Node-based visual workflow editor (graph editor) +- LoRA style customization with runtime hot-swapping +- VACE (Video All-in-One Creation and Editing) for reference-image guidance, inpainting, depth/pose control +- Beat-synchronized parameter changes (Ableton Link, MIDI clock) +- Multi-output: Spout (Windows), Syphon (macOS), NDI, WebRTC, MPEG-TS, MP4 recording +- Plugin system for third-party pipeline extensions +- Cloud GPU backend via Livepeer +- OSC control from external tools (TouchDesigner, Resolume, MaxMSP) +- MCP server interface for AI agent control +- Electron desktop app wrapping Python backend + React frontend + +--- + +## Tech Stack + +### Backend (Python) + +| Category | Library | Version | +|----------|---------|---------| +| HTTP framework | FastAPI | 0.116.1+ | +| ASGI server | uvicorn | 0.35.0+ | +| WebRTC | aiortc | 1.13.0+ | +| Data models | Pydantic | — | +| Deep learning | torch | 2.9.1 | +| Vision | torchvision | 0.24.1 | +| Transformers | transformers | 4.49.0 | +| Diffusers | diffusers | 0.31.0 | +| Training utils | accelerate | 1.1.1 | +| Attention kernel | flash-attn | 2.8.3 | +| Sage attention | sageattention | 2.2.0 | +| Triton kernels | triton | 3.5.1 | +| LoRA | peft | 0.18.1 | +| Quantization | torchao | 0.15.0 | +| Async HTTP | aiohttp | 3.9.0+ | +| WebRTC signaling | Twilio | 9.8.0+ | +| Config | omegaconf | 2.3.0+ | +| Safetensors | safetensors | 0.6.2+ | +| MCP | mcp | 1.0.0+ | +| OSC | python-osc | 1.9.0+ | +| CLI | click | 8.3.1+ | +| Package manager | uv | — | +| Plugin hooks | pluggy | — | +| Video encoding | PyAV | — | +| Kafka telemetry | aiokafka | 0.10.0+ (optional) | +| Ableton Link | aalink | 0.1.1+ (optional) | +| MIDI | mido + python-rtmidi | optional | + +Python requirement: ≥3.12 + +### Frontend (TypeScript/React) + +| Category | Library | +|----------|---------| +| Framework | React 19 | +| Build tool | Vite 7.x | +| Styling | Tailwind CSS + Radix UI | +| UI components | Radix primitives | +| Node graph | React Flow | +| Notifications | Sonner | +| HTTP client | Custom `useApi` hook | +| WebRTC client | Custom `useUnifiedWebRTC` hook | + +### Desktop (Electron) + +| Category | Library | +|----------|---------| +| Electron | 32.2.1 | +| electron-builder | 25.1.8 | +| electron-updater | 6.3.9 | +| electron-log | 5.2.4 | +| Build | Vite (multi-target: main + preload + renderer) | + +### Browser SDK (`@daydreamlive/browser`) + +| Category | Detail | +|----------|--------| +| Language | TypeScript (100%) | +| Streaming protocol | WebRTC via WHIP (broadcast) + WHEP (playback) | +| Packaging | ES module + CJS dual export | +| Testing | Vitest | +| Bundler | tsup | + +--- + +## Repository Structure + +``` +daydreamlive/scope +├── src/scope/ +│ ├── server/ # FastAPI app + all server logic +│ └── core/ # Pipelines, nodes, plugins, config +├── frontend/src/ # React/TypeScript SPA +│ ├── pages/ # StreamPage.tsx (single-page app) +│ ├── components/ # All UI components +│ │ ├── graph/ # Node graph editor (React Flow) +│ │ └── ... +│ ├── hooks/ # 22 custom React hooks +│ ├── contexts/ # 8 React contexts +│ └── types/ +├── app/ # Electron desktop wrapper +│ └── src/ +│ ├── main.ts # Electron main process +│ ├── preload.ts # IPC bridge +│ └── renderer.tsx +├── docs/ # 15+ markdown documentation files +│ ├── architecture/ # pipelines.md, plugins.md, livepeer.md +│ └── ... # osc.md, vace.md, workflows.md, etc. +├── pyproject.toml # Python deps (uv) +├── .mcp.json # MCP server config +├── CLAUDE.md # Agent instructions +└── Dockerfile / Dockerfile.cloud +``` + +--- + +## Architecture + +### Layer Model + +``` +[Desktop App (Electron)] + ├── main.ts: spawns Python backend on dynamic port + ├── health polls until backend ready + └── loads frontend (backend-served SPA) + +[Python Backend (FastAPI / uvicorn)] + ├── REST API (pipeline, WebRTC, OSC, plugins, assets) + ├── WebRTC sessions (aiortc) + ├── MCP server (stdio, --mcp flag) + └── OSC UDP server (port 8000) + +[Frame Processing Pipeline] + FrameProcessor + ├── SourceManager → NDI / Syphon / WebRTC input sources + ├── GraphExecutor → DAG of pipeline nodes + │ └── PipelineProcessor (per node) + │ └── Pipeline.__call__(frame) → tensor THWC + ├── SinkManager → Spout / NDI / Syphon / recording fan-out + └── ParameterScheduler + ModulationEngine → beat-synced param updates + +[Frontend (React SPA)] + StreamPage.tsx + ├── InputAndControlsPanel (prompts, parameters) + ├── VideoOutput (WebRTC playback via WHEP) + ├── GraphEditor (React Flow node graph) + ├── PromptTimeline (temporal prompt sequencing) + ├── SettingsPanel / OutputsPanel + └── StatusBar / LogPanel + +[Browser SDK (@daydreamlive/browser)] + Broadcast (WHIP) → server ingests video + Player (WHEP) → client plays processed video +``` + +**Import rule (critical):** `scope.server` can import from `scope.core`, but `scope.core` must **never** import from `scope.server`. Enforces one-way dependency. + +--- + +## Pipeline Architecture + +### Base Class Pattern + +```python +# All pipelines are Nodes (unified post-refactor) +Pipeline = Node # src/scope/core/pipelines/interface.py + +class MyPipeline(Pipeline): + def get_config_class(self) -> type[BasePipelineConfig]: + return MyPipelineConfig # Pydantic model + + def prepare(self) -> Requirements: + # Declares how many input frames needed before processing + return Requirements(input_size=4) + + def __call__(self, **kwargs) -> dict: + # Returns {"video": tensor} # THWC format, values [0,1] + ... +``` + +### BasePipelineConfig Schema + +```python +class BasePipelineConfig(BaseModel): + model_config = ConfigDict(extra="forbid") + + # Identity + pipeline_id: ClassVar[str] + pipeline_name: ClassVar[str] + pipeline_description: ClassVar[str] + estimated_vram_gb: ClassVar[float] + + # Capability flags + supports_lora: ClassVar[bool] + supports_vace: ClassVar[bool] + supports_cache_management: ClassVar[bool] + supports_quantization: ClassVar[bool] + produces_video: ClassVar[bool] + produces_audio: ClassVar[bool] + + # I/O ports + inputs: ClassVar[list[str]] # e.g. ["video", "vace_input_frames"] + outputs: ClassVar[list[str]] # e.g. ["video"] + + # Modes + modes: ClassVar[dict[str, ModeDefaults]] + + # Fields (runtime-configurable) + height: int + width: int + base_seed: int = 42 + manage_cache: bool = True + denoising_steps: list[int] | None + noise_scale: float # 0.0–1.0 + lora_merge_strategy: Literal["permanent_merge", "runtime_peft"] +``` + +### ModeDefaults + +```python +class ModeDefaults(BaseModel): + default: bool = False + height: int | None = None + width: int | None = None + denoising_steps: list[int] | None = None + noise_scale: float | None = None + noise_controller: bool | None = None + input_size: int | None = None + default_temporal_interpolation_steps: int | None = None +``` + +### Built-in Pipelines (13 registered) + +| Pipeline ID | Description | VRAM | Res | +|-------------|-------------|------|-----| +| `streamdiffusionv2` | StreamDiffusion v2 autoregressive | 20 GB | 512×512 | +| `longlive` | LongLive text/video-to-video | — | 576×320 | +| `krea_realtime_video` | Krea Realtime Video (Wan2.1 14B) | ~55 GB with VACE | 512×512 | +| `reward_forcing` | RewardForcing | — | 576×320 | +| `memflow` | MemFlow optical flow | — | 576×320 | +| `passthrough` | No-op passthrough | minimal | 512×512 | +| `rife` | RIFE frame interpolation | — | — | +| `optical_flow` | Optical flow preprocessor | — | — | +| `video_depth_anything` | Depth estimation | ~1 GB | — | +| `scribble` | Edge/scribble extraction | — | — | +| `gray` | Grayscale conversion | — | — | +| `controller_viz` | Controller visualization | — | — | +| `wan2_1` | Wan2.1 base | — | — | + +--- + +## Graph / Workflow Schema + +### GraphConfig (core data structure) + +```python +class GraphNode(BaseModel): + id: str # e.g. "input", "yolo_plugin" + type: Literal["source","pipeline","sink","record","node"] + pipeline_id: str | None # Registry key for pipeline nodes + node_type_id: str | None # NodeRegistry key for custom nodes + params: dict # Per-node configuration + # Source-specific + mode: str | None # e.g. "camera", "file", "ndi" + name: str | None + flip_vertical: bool + # Sink-specific + output_mode: str | None # "spout", "ndi", "syphon" + +class GraphEdge(BaseModel): + from_node: str + from_port: str + to_node: str + to_port: str + kind: Literal["stream","parameter"] # frame queue vs chunk-level data + +class GraphConfig(BaseModel): + nodes: list[GraphNode] + edges: list[GraphEdge] +``` + +**Execution model:** Queue-based DAG. Node-to-node connections use `maxsize=1` (backpressure). Pipeline-to-pipeline connections use larger queues. Each sink gets its own independent copy of frames. + +### Workflow File Schema (`.scope-workflow.json`) + +```json +{ + "format": "scope-workflow", + "format_version": "1.x", + "metadata": { ... }, + "pipelines": [ { "pipeline_id": "...", "load_params": {...} } ], + "timeline": [ ... ], + "prompts": [ ... ] +} +``` + +--- + +## WebRTC Architecture + +**Protocol flow:** +1. Frontend fetches ICE server config: `GET /api/v1/webrtc/ice-servers` +2. Frontend creates SDP offer with video/audio transceivers (VP8 codec enforced) +3. POST offer to `POST /api/v1/webrtc/offer` → receives session ID + SDP answer +4. Trickle ICE via `PATCH /api/v1/webrtc/offer/{session_id}` +5. Data channel `"parameters"` handles real-time parameter updates (JSON) +6. Backend signals stop via data channel + +**Server-side WebRTC (aiortc):** +``` +Browser → WebRTC → VideoProcessingTrack → MediaRelay + ├── WebRTC output (to browser) + ├── RecordingManager (MP4) + └── SinkOutputTrack (Spout/NDI/Syphon) +``` + +**Cloud relay mode:** `CloudTrack` forwards browser video to Livepeer cloud, receives processed frames, relays back to browser. + +**Browser SDK reconnection:** +- Broadcast: exponential backoff (`delay = baseDelay × 2^attempts`) +- Player: 10 fixed retries then exponential, capped at 60s + +--- + +## API Endpoints (FastAPI) + +**Base URL:** `http://localhost:8000` (dynamic port when Electron-managed) + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/` | Serve SPA index.html | +| GET | `/health` | Status, version, git commit, uptime | +| POST | `/api/v1/restart` | Restart server (exits code 42 in managed mode) | +| POST | `/api/v1/pipeline/load` | Load pipeline(s) | +| GET | `/api/v1/pipeline/status` | Pipeline load status | +| GET | `/api/v1/pipelines/schemas` | Legacy pipeline schemas (cached) | +| GET | `/api/v1/nodes/definitions` | Unified node catalog (memoized) | +| GET | `/api/v1/webrtc/ice-servers` | ICE server config | +| POST | `/api/v1/webrtc/offer` | SDP offer → answer + session ID | +| PATCH | `/api/v1/webrtc/offer/{id}` | Trickle ICE candidates | +| GET | `/api/v1/osc/status` | OSC server state | +| GET | `/api/v1/osc/paths` | Active OSC control paths | +| GET | `/api/v1/osc/stream` | SSE stream of OSC commands | +| PUT | `/api/v1/osc/settings` | Update OSC settings | +| GET | `/api/v1/hardware/info` | GPU VRAM, Spout/NDI/Syphon availability | +| GET/POST | `/api/v1/models/status` | Model download status | +| POST | `/api/v1/models/download` | Download pipeline models | +| GET | `/api/v1/assets` | List assets (images/videos) | +| POST | `/api/v1/assets` | Upload asset | +| GET | `/api/v1/lora/list` | List installed LoRAs | +| GET | `/api/v1/tempo/status` | Tempo sync state | +| POST | `/api/v1/tempo/enable` | Enable Ableton Link or MIDI clock | +| GET/POST | `/api/v1/plugins` | List / install plugins | +| DELETE | `/api/v1/plugins/{name}` | Uninstall plugin | +| POST | `/api/v1/plugins/{name}/reload` | Hot-reload editable plugin | +| GET | `/api/v1/stream` | MPEG-TS streaming endpoint | +| GET | `/docs` | Swagger UI | + +**Cache invalidation:** Three server-level caches (`_pipeline_schemas_cache`, `_node_definitions_cache`, `_plugins_list_cache`) cleared on plugin install/uninstall/cloud connect. + +**Cloud proxy pattern:** Routes decorated with `@cloud_proxy()` automatically redirect to cloud backend URL when a cloud connection is active — zero changes to individual route handlers needed. + +--- + +## MCP Server (AI Agent Interface) + +**Config (`.mcp.json`):** +```json +{ "mcpServers": { "scope": { "type": "stdio", "command": "uv", "args": ["run", "daydream-scope", "--mcp"] } } } +``` + +**All MCP tools (35 total):** + +| Category | Tools | +|----------|-------| +| Connection | `connect_to_scope`, `connect_to_cloud`, `disconnect_from_cloud`, `get_cloud_status` | +| Pipelines | `list_pipelines`, `get_pipeline_status`, `load_pipeline`, `get_models_status`, `download_models` | +| Runtime | `update_parameters`, `get_parameters` | +| Sessions | `start_stream`, `stop_stream`, `get_stream_url` | +| Capture | `capture_frame(quality, sink_node_id)`, `get_session_metrics` | +| Recording | `start_recording`, `stop_recording`, `download_recording` | +| Assets | `list_assets` | +| LoRAs | `list_loras`, `install_lora`, `download_lora`, `delete_lora` | +| Plugins | `list_plugins`, `install_plugin`, `uninstall_plugin`, `reload_plugin` | +| System | `get_health`, `get_hardware_info`, `get_logs` | +| Inputs | `list_input_source_types`, `list_input_sources` | +| OSC | `get_osc_status`, `get_osc_paths` | +| Workflow | `resolve_workflow` | +| API keys | `list_api_keys`, `set_api_key`, `delete_api_key` | +| Resources | `current_log_file` (URI: `logs://current`) | + +--- + +## Plugin Architecture + +**Plugin discovery:** Python entry points under `[project.entry-points."scope"]` in `pyproject.toml`. + +**Hook system:** `pluggy` — plugins implement `@hookimpl` on `register_pipelines(register)` callback. + +**Minimum plugin structure:** +``` +my-scope-plugin/ +├── pyproject.toml # entry-points."scope" = {"my-plugin": "my_scope_plugin.plugin"} +└── my_scope_plugin/ + ├── plugin.py # @hookimpl def register_pipelines(register): register(MyPipeline) + └── pipelines/ + ├── schema.py # BasePipelineConfig subclass + └── pipeline.py # Pipeline subclass with __call__ returning THWC tensor +``` + +**Lifecycle:** +1. Pre-validation: entry points loaded tentatively; broken ones recorded as `FailedPluginInfo` (never crash server) +2. GPU-aware registration: pipelines with `estimated_vram_gb` exceeding available VRAM are skipped +3. Installation: uv resolves deps against `uv.lock` constraints; rollback on failure +4. Restart protocol: managed mode exits with code 42; Electron respawns; standalone uses `os.execv` + +--- + +## Frontend Architecture + +**Single-page app** — one route: `StreamPage.tsx` is the entire app. + +**Provider stack (outermost → innermost):** +``` +TelemetryProvider + CloudStatusProvider + BillingProvider + PipelinesProvider + LoRAsProvider + PluginsProvider + ServerInfoProvider + OnboardingProvider + StreamPage +``` + +**Main layout sections of StreamPage:** +- `Header` — branding, connection status +- `InputAndControlsPanel` — prompt input, parameter sliders +- `VideoOutput` — WebRTC live preview +- `GraphEditor` — React Flow node graph (27+ node types) +- `PromptTimeline` — horizontal timeline for prompt sequencing +- `SettingsPanel` / `OutputsPanel` — global config, output destinations +- `StatusBar` — FPS, session info +- `LogPanel` — backend log stream (SSE) + +**Two rendering modes:** +- **Perform Mode**: Traditional controls panel layout +- **Graph Mode**: Node-based editor replaces pipeline controls (`nonLinearGraph` flag disables pipeline selector and load controls) + +### State Hooks (22 total) + +| Hook | Purpose | +|------|---------| +| `useUnifiedWebRTC` | WebRTC peer connection, SDP, data channel | +| `usePipeline` | Pipeline loading, status | +| `usePipelines` | All pipeline metadata | +| `useVideoSource` | Camera/file input management | +| `useStreamState` | Persisted user preferences | +| `useTempoSync` | Beat-sync coordination | +| `useTimelinePlayback` | Prompt timeline playback | +| `usePromptManager` | Prompt CRUD | +| `useNodeDefinitions` | Backend node catalog | +| `useApi` | Typed API client | +| `useLoRAFiles` | LoRA list | +| `usePlugins` | Plugin management | +| `useServerInfo` | Backend version/health | +| `useCloudStatus` | Cloud connection | +| `useLogStream` | SSE log tail | +| `useMIDIController` | MIDI device input | +| `useControllerInput` | General controller input | +| `useWebRTCStats` | FPS/frame metrics | +| `useWorkflowDependencies` | Workflow import dependency check | +| `useDependencyTracker` | Version dependency resolution | +| `useLocalVideo` | Local video file player | +| `useLocalSliderValue` | Debounced slider state | + +### Graph Node Types (27+) + +| Category | Nodes | +|----------|-------| +| Source | `SourceNode`, `ImageNode`, `AudioNode` | +| Processing | `PipelineNode`, `VaceNode`, `LoraNode`, `PromptListNode`, `PromptBlendNode`, `SchedulerNode` | +| Control | `SliderNode`, `KnobsNode`, `XYPadNode`, `BoolNode`, `TriggerNode`, `MidiNode`, `TempoNode`, `TupleNode` | +| Output | `SinkNode`, `OutputNode`, `RecordNode` | +| Utility | `NoteNode`, `RerouteNode`, `PrimitiveNode`, `ControlNode`, `MathNode` | +| Subgraph | `SubgraphNode`, `SubgraphInputNode`, `SubgraphOutputNode`, `CustomNode` | + +--- + +## Frame Processing Data Flow + +``` +Input + └── SourceManager (NDI/Syphon/WebRTC/file) + ↓ numpy frames via _on_frame callback + ↓ convert to GPU tensor (per-thread pinned buffers) + └── GraphExecutor (DAG) + ↓ queue-based streaming, maxsize=1 for node-to-node + └── PipelineProcessor (per node) + ↓ Pipeline.__call__(**kwargs) → {"video": THWC tensor [0,1]} + └── SinkManager fan-out + ├── WebRTC (aiortc MediaRelay → browser) + ├── RecordingCoordinator (MP4 via PyAV, libx264 + AAC) + ├── HeadlessSession (MPEG-TS or direct MP4) + ├── Spout/Syphon/NDI (platform-specific) + └── Kafka (telemetry heartbeats every 10s) +``` + +**Output tensor format:** THWC (time/frames, height, width, channels), values normalized `[0, 1]`. + +--- + +## Recording Implementation + +- Uses `PyAV` (not aiortc's built-in recorder) +- Container: MP4 with flags `{"use_editlist": "0", "movflags": "+faststart"}` +- Video codec: libx264 at 30 FPS +- Audio codec: AAC +- Subscribes to same `MediaRelay` as WebRTC output (non-destructive) +- Thread-safe state via `recording_lock` +- Handles odd-dimension frames via padding + +--- + +## OSC Control System + +- UDP port 8000 +- Address format: `/scope/<node-slug>/<param>` +- Node slugs derived from display titles (kebab-case) +- Three layers: runtime globals, pipeline params, graph nodes +- Validation: type constraints (float, int, bool, string, integer_list), min/max, enum +- SSE endpoint for observing received OSC commands +- Auto-generated HTML docs at `/api/v1/osc/docs` + +--- + +## Tempo Sync / Beat-Synchronized Parameters + +**Sources:** Ableton Link (`aalink`) or MIDI clock (`mido + python-rtmidi`) + +**Quantization options:** Immediate, Beat, Bar, 2 Bars, 4 Bars + +**Modulation waveforms:** sine, cosine, triangle, saw, square, exp_decay + +**Architecture (5 layers):** +``` +Tempo sources (Link/MIDI) → TempoManager → ParameterScheduler → ModulationEngine → Pipeline injection +``` + +**ParameterScheduler logic:** Schedules parameter changes ahead of beat boundaries by a configurable lookahead (0–1000ms) to compensate for pipeline processing latency. Uses `threading.Timer`. Merges concurrent updates without re-calculating boundaries. + +--- + +## Electron Desktop App + +**Main process (`app/src/main.ts`):** +- Spawns Python backend (`uv run daydream-scope`) on a dynamically allocated port +- Polls `/health` endpoint until backend is ready +- Opens `BrowserWindow` pointing to `http://localhost:<port>` +- Detects backend restart signals (exit code 42) and respawns + +**Restart protocol:** +- Managed mode (Electron): backend exits code 42 → Electron respawns → frontend polls `/health` +- Standalone mode: `os.execv()` (Unix) or `subprocess.Popen` (Windows) + +**Build targets:** `dist:win`, `dist:mac`, `dist:linux` via electron-builder + +--- + +## Environment Variables + +| Variable | Purpose | +|----------|---------| +| `PIPELINE` | Pre-warm pipeline on startup | +| `HF_TOKEN` | Hugging Face token (model downloads + Cloudflare TURN) | +| `CIVITAI_API_TOKEN` | CivitAI LoRA downloads | +| `VERBOSE_LOGGING` | Debug log output | + +--- + +## Development Conventions + +- **Import style:** Relative imports for 1–2 levels; absolute otherwise +- **Code quality:** ruff (Python), prettier + eslint (frontend) +- **Commit sign-off:** DCO required on all commits +- **Testing:** `uv run pytest` (backend), `vitest` (frontend) +- **MCP testing:** Use HTTP API directly rather than MCP tools when testing server behavior +- **Pre-commit hooks:** `uv run pre-commit install` + +--- + +## Key Architectural Patterns Relevant to deckcreate Refactor + +### 1. Pipeline = Node Unification +After their refactor, "a pipeline is just a config-driven node." This simplifies the registry and enables composing pipelines in graphs interchangeably with custom nodes. Applied to deckcreate: transcript processing stages (transcribe, align, diarize, merge) could become unified `Node` types with a shared config interface. + +### 2. Pydantic-Driven UI +Pipeline schemas auto-generate frontend forms via `/api/v1/nodes/definitions`. The `ui_field_config()` helper attaches rendering metadata (order, category, mode-visibility, is_load_param) to Pydantic `Field` definitions. Applied to deckcreate: `transcript.json` schema changes could drive UI updates automatically. + +### 3. Queue-Based DAG with Backpressure +Node-to-node edges use `maxsize=1` queues, creating natural backpressure. Sink nodes get independent queue copies to decouple recording from display. Applied to deckcreate: the multi-step pipeline (sync → transcribe → diarize → align → merge) maps naturally to this pattern. + +### 4. Two Rendering Modes (Perform / Graph) +"Perform Mode" (traditional panel layout) and "Graph Mode" (node editor) coexist behind a `nonLinearGraph` flag that disables pipeline selector and load controls. Applied to deckcreate: could distinguish between "simple timeline editing" vs. "advanced composition graph" modes. + +### 5. MCP as Primary Agent Interface +The MCP server is a thin wrapper over the HTTP API. All editing operations are accessible to AI agents via `--mcp` flag. Applied to deckcreate: exposing transcript edits, cut operations, and render triggers as MCP tools would enable AI-driven editing (the same model as Daydream Video). + +### 6. Plugin Restart Protocol via Exit Code 42 +A deliberate restart signal via exit code is a clean pattern for coordinating desktop app ↔ backend restarts across platforms. Electron respawns on code 42; standalone uses `os.execv`. + +### 7. Cloud Proxy Decorator +`@cloud_proxy()` transparently redirects requests to cloud backend when connected — zero changes to individual route handlers needed. Applied to deckcreate: could route heavy processing (whisper, alignment) to cloud when available. + +### 8. Per-Thread Pinned GPU Buffers +Prevents race conditions when multiple input sources upload frames concurrently to GPU. Applied to deckcreate: relevant if adding parallel video track processing. + +### 9. SSE for Live Log Streaming +`GET /api/v1/osc/stream` and log endpoints use Server-Sent Events for real-time backend output in the frontend. Applied to deckcreate: transcription/alignment progress could stream to UI via SSE instead of polling. + +### 10. Dynamic Port Allocation + Health Polling +Electron spawns backend on a dynamic port and polls `/health` until ready. This is cleaner than hardcoding a port, avoids conflicts, and gives a clean startup signal. Applied to deckcreate if adding a local backend service. + +--- + +## Sources + +- [https://daydream.live/](https://daydream.live/) — Daydream Scope landing page +- [https://github.com/daydreamlive/scope](https://github.com/daydreamlive/scope) — Full open-source codebase diff --git a/docs/research/DAYDREAM_VIDEO_RESEARCH.md b/docs/research/DAYDREAM_VIDEO_RESEARCH.md new file mode 100644 index 0000000..ce07686 --- /dev/null +++ b/docs/research/DAYDREAM_VIDEO_RESEARCH.md @@ -0,0 +1,62 @@ +# Daydream Video — Research Reference + +> Written: 2026-05-08. Closed-source product; all detail is inferred from the landing page. No need to re-visit source URLs. +> +> **Note:** The GitHub org `daydreamlive` belongs to the separate open-source product *Daydream Scope* — see [DAYDREAM_SCOPE_RESEARCH.md](DAYDREAM_SCOPE_RESEARCH.md). The two share the Daydream brand but are architecturally unrelated. + +--- + +## What It Does + +Daydream Video is an AI-native post-production video editor that operates as an **MCP (Model Context Protocol) server**, allowing AI agents (Claude Code, ChatGPT, Codex, Cursor) to control editing operations programmatically. It also has its own native chat interface. + +**Core user-facing features:** +- **Transcript-based editing**: Cut bad takes, filler words, and silences by editing the transcript. AI can do this automatically on instruction. +- **Natural language editing**: Trim clips, add b-roll, generate graphics by describing what you want. +- **Timeline editing with contextual precision**: AI understands artistic intent, not just mechanical cuts. +- **Export to professional tools**: MP4, Final Cut Pro, DaVinci Resolve, Premiere Pro. +- **Platform optimization**: Output formatted for TikTok, YouTube, Instagram. +- **Local-first privacy**: All video stays on-device, never uploaded to cloud. +- **No watermarks.** + +--- + +## Pricing + +| Plan | Price | Processing | Transcription | MCP calls | +|------|-------|------------|---------------|-----------| +| Free | $0/mo | 1 hr/mo | 1 hr/mo | 100/mo | +| Pro | $16/mo (annual) | 20 hrs/mo | 10 hrs/mo | 1M/mo | +| Business | Custom | Extended | Extended | Extended | + +--- + +## Tech Stack (Inferred — closed-source) + +- Runs as a local MCP server (Model Context Protocol) +- Desktop app (local-first, no cloud video upload) +- Exports to FCPXML, XML, MP4 +- Integrates with AI agent interfaces via MCP stdio or local socket +- Team: ex-Cisco, Meta, Workday, Twitch, Amazon, The Athletic + +--- + +## Architecture Pattern + +Daydream Video exposes its editing capabilities as MCP tools. An AI agent running in Claude Code (or similar) calls these tools to perform editing operations. The video never leaves the local machine — the MCP server runs locally and AI agents interact with it via stdio or local socket. + +This is the same MCP-as-primary-agent-interface pattern used by Daydream Scope (open-source). See [DAYDREAM_SCOPE_RESEARCH.md § MCP Server](DAYDREAM_SCOPE_RESEARCH.md) for a fully inspectable implementation of that pattern. + +--- + +## Relevance to deckcreate Refactor + +- **MCP as primary agent interface**: Exposing transcript edits, cut operations, and render triggers as MCP tools would enable AI-driven editing in deckcreate, mirroring how Daydream Video works. +- **Transcript-based editing as first-class UX**: Daydream Video treats the transcript as the primary editing surface, not the timeline. deckcreate's existing transcript pipeline already has this foundation. +- **Local-first**: No cloud video upload is a strong user trust signal; deckcreate's current architecture already matches this. + +--- + +## Source + +- [https://www.daydreamvideo.com/](https://www.daydreamvideo.com/) — landing page diff --git a/docs/research/TRANSCRIPT_EDITOR_GAP_ANALYSIS.md b/docs/research/TRANSCRIPT_EDITOR_GAP_ANALYSIS.md new file mode 100644 index 0000000..dcff5d7 --- /dev/null +++ b/docs/research/TRANSCRIPT_EDITOR_GAP_ANALYSIS.md @@ -0,0 +1,334 @@ +# Transcript Editor Gap Analysis: Current Implementation vs. Descript + +> **Context for agents:** This doc compares DeckCreate's transcript-based video editing workflow against Descript (the industry benchmark for browser-based transcript-driven video editing). The "code editor" column refers to the VSCode + `transcript.doc.txt` + custom language extension workflow. The "web editor" refers to `app/editor/` (the Next.js timeline UI). Feasibility ratings apply to implementing the feature *within* the existing approach — not from scratch. + +--- + +## How the Current Editor Works (Baseline) + +Two parallel editing surfaces: + +**Surface A — Code editor (VSCode + `transcript.doc.txt`)** +- Human edits a plain-text markup file with a custom grammar +- VSCode extension provides syntax highlighting, directive autocomplete (`> [TAB]`), and real-time linting +- Supports: text corrections, `{word cuts}`, segment cuts (`-[n]`), hooks (`> HOOK`), camera directives (`> CAM`), speaker splits (`> SPEAKER`), graphics (`> LowerThird`, `> ImageWindow`), trim bounds (`> START`/`> END`), silence cuts (`> CUT`) +- No video, no audio, no visual feedback — pure text markup + +**Surface B — Web timeline editor (`app/editor/`)** +- Canvas-based timeline: colored speaker tracks, cut overlays, camera cue track +- Video player above timeline (separate element, not synced to word positions) +- Mark In / Mark Out (I/O keys) to add visual cuts; saved as `> CUT` lines back to the doc +- Camera cue drag-and-drop on the camera track +- Cuts list panel with seek-to-cut links +- No waveform, no audio scrubbing, no real-time render preview + +**Round-trip:** Doc edits → `npm run merge-doc` → `transcript.json` → Remotion render. Web editor cuts → save → `transcript.json` + `> CUT` lines appended to doc. + +--- + +## Gap Analysis + +### 1. Word-Level Text Editing as Video Cutting + +**Descript:** Clicking on a word and pressing Delete removes it from both the transcript and the video. The transcript IS the edit decision list. + +**Current:** Words are cut by wrapping in `{braces}` in the doc. No clicking. No visual connection between transcript text and video frames. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Partially feasible — the doc format already supports `{word}` cuts; a `Wrap in cut` command could wrap any VSCode selection | Not feasible — web editor has no transcript text view at all | +| What's missing | A VSCode command (e.g. Cmd+D) that wraps selected text in `{}`; no further infrastructure needed | A transcript text panel synced to video time; word-level click-to-cut | +| **Recommendation** | **Extend the VSCode extension**: add a `Wrap in cut` command. Closes ~70% of the Descript word-cutting experience with minimal effort. | + +--- + +### 2. Transcript Scroll-Sync During Playback + +**Descript:** As video plays, the transcript scrolls and highlights the current word in real time. + +**Current:** No connection between video playback and the doc in VSCode. Web editor shows a moving playhead but no transcript text. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Not feasible — VSCode cannot receive live video playback position from a separate browser window | Feasible — the video playback time is already in state; mapping time → token → doc line is a data problem, not a new system | +| What's missing | A transcript text panel in the web editor; `currentWordIndex` derived from playback time + token `t_dtw` timestamps; scroll-into-view on word change | +| **Recommendation** | **Web editor task.** All required data exists in `transcript.json`. This is a UI-only problem. | + +--- + +### 3. Filler Word Detection and One-Click Removal + +**Descript:** "Remove filler words" detects all "um", "uh", "like", "you know" and marks them for removal with human review. + +**Current:** `edit-transcript.js` already auto-detects fillers and wraps them in `{}` during doc generation. No UI to review or selectively restore them. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | **Already implemented** in the pipeline — fillers appear as `{word}` cuts in the generated doc; the VSCode diff view shows what was auto-cut | Feasible — a "Filler words" panel listing all `{word}` cuts with per-word restore buttons | +| What's missing | Discoverability: users don't know the auto-detection ran. A summary in the generated doc header would help. Web editor gap: no filler review panel. | +| **Recommendation** | **Short-term:** Add a filler summary comment block to the top of the generated doc. **Medium-term:** Filler review panel in the web editor. | + +--- + +### 4. Silence Detection and Removal + +**Descript:** "Remove silences" with configurable threshold; previews proposed cuts before applying. + +**Current:** `npm run merge-doc:cut-pauses` adds `> CUT` lines for detected pauses. No preview. No configurable threshold in the UI. No undo. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Functional — the command exists; threshold could be a `> CONFIG silence_threshold=0.5` doc directive | Feasible — show proposed silence cuts as pending yellow bands in the timeline before the user confirms | +| What's missing | Per-episode threshold config; preview mode showing proposed cuts without committing; confirm/discard all | +| **Recommendation** | **Web editor task.** Detection logic already exists. Add a "Detect silences" button → show pending cuts → "Apply all" or dismiss. | + +--- + +### 5. Waveform Visualization + +**Descript:** Audio waveform in the timeline makes silence, breath, and filler audio visible without listening. + +**Current:** No waveform. Timeline shows colored segment blocks only. Audio content is invisible. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Not applicable | Feasible — pre-compute peak amplitude per frame during the `sync` pipeline step; store as a sidecar JSON in the artifact store; render in `timelineCanvas.ts` as a waveform layer | +| What's missing | Waveform extraction in the pipeline (`ffmpeg -af astats` or similar); waveform JSON stored as a pipeline artifact; waveform layer in `timelineCanvas.ts` | +| **Recommendation** | **Pipeline + web editor task.** Add waveform extraction to the `sync` stage. Render in canvas behind speaker tracks. Moderate effort, high daily-use value. | + +--- + +### 6. Real-Time Video Preview of Edits + +**Descript:** As you mark cuts, the video player immediately skips cut sections during playback. + +**Current:** The video player shows the **raw uncut source video**. Edits are not previewed until a full Remotion render (hours). + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Not feasible | Feasible — the jump-cut logic in `SegmentPlayer.tsx` can be ported to a browser `PreviewPlayer` using `HTMLVideoElement` seek + `timeupdate` events | +| What's missing | A `PreviewPlayer` React component that reads `transcript.json` cuts and plays back the video with sections skipped using native browser video APIs — no Remotion involved | +| **Recommendation** | **The single highest-ROI missing feature.** Without cut preview, the editor is blind. Implement `PreviewPlayer` using the same section calculation logic as `SegmentPlayer`. All the cut data already exists. | + +--- + +### 7. Audio Scrubbing + +**Descript:** Dragging the playhead plays audio at high speed so you can hear context while seeking. + +**Current:** Dragging the playhead is silent. Only click-to-seek works. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Not applicable | Feasible — connect timeline drag events to `videoElement.currentTime` updates in real time | +| What's missing | Forward drag position from canvas mouse events to the video element's `currentTime` property | +| **Recommendation** | **Low effort, high usability improvement.** The drag event handler is already in `Timeline.tsx`; it just needs to call `video.currentTime = dragPosition`. | + +--- + +### 8. Speaker Assignment / Correction UI + +**Descript:** Click on any segment to change its speaker. Drag to merge adjacent speaker turns. + +**Current:** Speaker names can only be changed via `> SPEAKER` directives in the doc. No UI for this in the web editor. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | **Already supported** — `> SPEAKER Alice at="word"` works; VSCode extension could add a snippet for it | Feasible — right-click on a segment track → "Change speaker" dropdown; writes `> SPEAKER` directive back to doc | +| What's missing | VSCode snippet for `> SPEAKER` (trivial to add); web editor context menu on segment blocks | +| **Recommendation** | **Extend VSCode extension** with `> SPEAKER` snippet (immediate). Add context menu to web editor (medium-term). | + +--- + +### 9. Inline Transcript Text Correction + +**Descript:** Click on a misrecognized word in the transcript, retype the correct word. Used for captions; audio is unchanged. + +**Current:** Text corrections are made by editing `transcript.doc.txt` in VSCode. The web editor has no text view. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | **Already the intended workflow** — editing doc text IS the text correction mechanism | Feasible only after scroll-sync (gap #2) is implemented — requires a transcript text panel | +| What's missing | The code editor already handles this. The gap is that new users don't know to edit the doc text. | +| **Recommendation** | **Code editor is correct for text correction.** Improve discoverability via documentation and wizard prompts. Web editor text editing is a stretch goal dependent on scroll-sync. | + +--- + +### 10. Undo / Redo + +**Descript:** Full undo/redo stack; every edit is reversible. + +**Current:** No undo/redo in the web editor. The doc has VSCode's built-in text undo (works well). No snapshots taken before destructive pipeline operations. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | **VSCode undo/redo works natively** for doc text edits | Not implemented; requires command pattern or `useReducer` with history stack | +| What's missing | Pre-operation snapshots of `transcript.doc.txt` and `transcript.json` before destructive pipeline runs; web editor history stack | +| **Recommendation** | **Phase 0 of the production refactor delivers pipeline snapshots** via content-addressed artifacts. Web editor undo is a medium-term task. | + +--- + +### 11. Timestamped Comments / Annotations + +**Descript:** Leave timestamped comments for async team review. + +**Current:** No comment system. The doc has no comment syntax for review notes. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Feasible — add `> NOTE "comment text"` as a doc directive; parsed but not rendered in video; shown as markers in web editor | Feasible — note markers on timeline; tooltip on hover | +| What's missing | `> NOTE` directive in VSCode extension and doc parser; notes layer in `timelineCanvas.ts` | +| **Recommendation** | **Low priority for solo workflow.** Trivial to implement as a `> NOTE` directive when multi-person async review is needed. | + +--- + +### 12. AI Hook / Chapter / Highlight Suggestions + +**Descript:** AI identifies key moments, suggests chapter titles, highlights quotable quotes. + +**Current:** Hooks are manually marked with `> HOOK` in the doc. No AI assistance. `hook-qa.js` reviews quality but doesn't suggest locations. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | **LLM-ready** — the transcript is structured text; a Claude API call with the full transcript can identify hooks, suggest chapter names, and flag quotable moments; output as suggested doc directives | Feasible as a "Suggestions" panel in the web editor showing AI-proposed hooks for human approval | +| What's missing | `scripts/suggest-hooks.ts` calling Claude API with `transcript.json`; output as commented `> HOOK` and `> ChapterMarker` suggestions in the doc or a `suggestions.json` sidecar | +| **Recommendation** | **High value, moderate effort.** Claude API is already used in this project. A structured prompt → suggested hook timestamps + chapter titles is a straightforward addition. | + +--- + +### 13. Version History / Project Snapshots + +**Descript:** Every save creates a named version. Roll back to any point. + +**Current:** No versioning. The doc is overwritten on every save. Git provides history only if the user commits manually. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | **Feasible with the Phase 0 refactor** — the `.ragtech/artifacts/` content-addressed store makes pre-save snapshots automatic | The web editor save endpoint can write a snapshot before overwriting | +| What's missing | Snapshot logic in the pipeline runner; a `npm run transcript:history` command to list and restore snapshots | +| **Recommendation** | **Implement as part of Phase 0 (project file refactor).** Every `merge-doc` and web editor save snapshots the transcript to `.ragtech/artifacts/`. Restoring is a one-line copy command. | + +--- + +### 14. Captions Export (SRT / VTT) + +**Descript:** Export standalone captions in SRT, VTT, or burned-in formats. + +**Current:** Captions are burned-in by Remotion (`CaptionOverlay.tsx`). No standalone SRT/VTT export. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | **Feasible as a pipeline script** — `transcript.final.json` has all token timestamps; generating SRT/VTT is straightforward | Web editor "Export captions" button calling the script | +| What's missing | `scripts/export-captions.ts` generating SRT/VTT from token timestamps with cut sections excluded; timing must skip cut regions (same logic as `SegmentPlayer`) | +| **Recommendation** | **Short-term pipeline task.** SRT format is trivial. Main complexity is applying cut logic to caption timing — reuse `SegmentPlayer` section math. | + +--- + +### 15. Direct Publish to Platforms + +**Descript:** Export and upload directly to YouTube, Spotify, Apple Podcasts from within the app. + +**Current:** Render produces a video file. Upload is manual. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Not applicable | Feasible — YouTube Data API v3 for video upload; podcast RSS for audio; render output path is known from `project.json` | +| What's missing | OAuth token management per platform; upload progress UI; metadata entry (title, description, tags, thumbnail) | +| **Recommendation** | **Medium-term web UI task.** Add a "Publish" step to the wizard after render. Lower priority than preview and waveform. | + +--- + +### 16. AI Voice Synthesis (Overdub) + +**Descript:** Fix a word by typing — the app synthesizes replacement audio in the speaker's voice. + +**Current:** Not implemented anywhere. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Feasible in theory with ElevenLabs/Play.ht API — but requires audio replacement pipeline and re-sync with video | Not feasible without significant audio mixing infrastructure | +| What's missing | Voice model cloning per speaker; audio replacement pipeline; re-sync synthesized clip with video | +| **Recommendation** | **Out of scope.** Descript's moat feature. Complex and expensive to build. Skip unless there is a specific demand for correcting audio without re-recording. | + +--- + +### 17. Eye Contact Correction + +**Descript:** AI makes speakers appear to look directly at the camera. + +**Current:** Not implemented. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Requires per-frame video ML inference (NVIDIA Maxine or equivalent) | Not applicable | +| **Recommendation** | **Out of scope.** Infrastructure does not support per-frame video ML today. | + +--- + +### 18. Video B-Roll Insertion + +**Descript:** Drag a video clip from the media library onto the timeline to insert B-roll at a timecode. + +**Current:** Image B-roll (`> ImageWindow`) and GIF B-roll (`> GifWindow`) work via doc directives. No video B-roll. No media library browser. + +| | Code editor | Web editor | +|--|------------|------------| +| Feasibility | Image/GIF: **already implemented** via doc directives | Video B-roll: requires a new `> VideoWindow` Remotion overlay component + media upload UI | +| What's missing | `> VideoWindow` overlay in Remotion; media library browser in web editor; timeline representation of video B-roll clips | +| **Recommendation** | **Image/GIF B-roll: already done.** Video B-roll: add `> VideoWindow` Remotion component as a medium-term task. | + +--- + +## Summary Table + +| Feature | Code editor | Web editor | Priority | +|---------|------------|------------|---------| +| Word-level cut (select → `{}`) | **Add `Wrap in cut` command** | Requires transcript text panel | High | +| Transcript scroll-sync | Not feasible | Requires text panel + token mapping | High | +| Filler word review | Auto-detected; add summary comment | Add filler review panel | Medium | +| Silence removal preview | Add `> CONFIG` threshold | Preview layer before commit | Medium | +| Waveform visualization | Not applicable | Pipeline step + canvas layer | Medium | +| **Real-time cut preview** | Not feasible | **`PreviewPlayer` — highest ROI** | **Critical** | +| Audio scrubbing | Not applicable | Connect drag → `video.currentTime` | Low | +| Speaker assignment UI | Add `> SPEAKER` snippet | Context menu on segment | Medium | +| Inline text correction | Already works in doc | Depends on scroll-sync | Low | +| Undo / redo | Built into VSCode | `useReducer` history stack | Medium | +| Timestamped comments | Add `> NOTE` directive | Notes layer on canvas | Low | +| AI hook/chapter suggestions | Claude API script → doc directives | Suggestions panel | High | +| Version history | **Phase 0 refactor delivers this** | Snapshot on save | High | +| Captions export (SRT/VTT) | Pipeline script | Export button | Medium | +| Direct publish | Not applicable | YouTube/podcast API | Low | +| Voice synthesis (Overdub) | Out of scope | Out of scope | Skip | +| Eye contact correction | Out of scope | Out of scope | Skip | +| Video B-roll | Not applicable | `> VideoWindow` component | Low | + +--- + +## Key Conclusion: Is Staying in the Code Editor Feasible? + +**Yes, for content decisions.** The code editor (VSCode + `transcript.doc.txt`) is correct for: text corrections, hook placement, camera directives, speaker corrections, graphics, and any edit that maps to a named doc directive. The custom language extension already provides autocomplete, linting, and syntax highlighting. Most Descript "smart" editing features (filler detection, silence removal) are already implemented in the pipeline — the gap is UI discoverability, not capability. + +**The code editor is not the right surface for:** real-time video preview, waveform visualization, transcript scroll-sync, or audio scrubbing. Those require a browser. + +**The right strategy:** Keep the code editor as the source-of-truth for content decisions. Invest web editor effort in exactly three things, in order: + +1. **`PreviewPlayer`** — a browser video player that reads `transcript.json` cuts and plays back with sections skipped. This alone makes the editor feel like Descript's core experience. All required data exists; it is a pure frontend implementation using `HTMLVideoElement` seek + `timeupdate`. The section calculation logic to port already exists in `remotion/components/SegmentPlayer.tsx`. + +2. **Waveform layer** — pre-computed during the sync pipeline step, rendered in `timelineCanvas.ts`. Makes silence and breath visible without listening. + +3. **Transcript text panel with scroll-sync** — a read-only transcript view that highlights the current word during playback. Once this exists, inline text correction and filler word review in the web editor become possible. + +Everything else on the gap list can wait. + +--- + +## Critical Files for Implementation + +| File | Relevance | +|------|-----------| +| [app/editor/page.tsx](../../app/editor/page.tsx) | `PreviewPlayer` and transcript text panel would be added here | +| [app/editor/Timeline.tsx](../../app/editor/Timeline.tsx) | Waveform layer and scroll-sync highlighting added here | +| [app/editor/timelineCanvas.ts](../../app/editor/timelineCanvas.ts) | Waveform draw functions go here | +| [scripts/edit-transcript.js](../../scripts/edit-transcript.js) | Filler detection + silence detection already implemented here | +| [vscode-transcript-language/src/extension.js](../../vscode-transcript-language/src/extension.js) | `Wrap in cut` command + `> NOTE` directive support added here | +| [remotion/components/SegmentPlayer.tsx](../../remotion/components/SegmentPlayer.tsx) | Jump-cut section logic to port to `PreviewPlayer` | diff --git a/e2e/smoke.test.ts b/e2e/smoke.test.ts new file mode 100644 index 0000000..a6ef2dd --- /dev/null +++ b/e2e/smoke.test.ts @@ -0,0 +1,24 @@ +/** + * Smoke tests — verify the app boots and critical routes respond. + * + * These run against a live Next.js dev server (started automatically by + * playwright.config.ts webServer). Keep them fast: no heavy user flows here. + * Full user flows belong in e2e/flows/*.test.ts. + */ +import { test, expect } from '@playwright/test'; + +test('root redirects to login or renders app shell', async ({ page }) => { + const response = await page.goto('/'); + expect(response?.status()).toBeLessThan(500); +}); + +test('editor route is reachable', async ({ page }) => { + const response = await page.goto('/editor'); + // Allow redirect to login (302) or page render (200) but not server errors + expect(response?.status()).toBeLessThan(500); +}); + +test('camera route is reachable', async ({ page }) => { + const response = await page.goto('/camera'); + expect(response?.status()).toBeLessThan(500); +}); diff --git a/jest.config.js b/jest.config.js index 0e8df2d..6ed88f3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,38 +1,61 @@ /** @type {import('jest').Config} */ -export default { - // Use Node environment for server-side scripts - testEnvironment: 'node', - - // Test file locations - testMatch: [ - '**/__tests__/**/*.js', - '**/?(*.)+(spec|test).js' - ], - - // Coverage configuration - collectCoverage: true, +const config = { + verbose: true, + testTimeout: 30000, coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], collectCoverageFrom: [ - 'scripts/**/*.js', - '!scripts/**/*.test.js', - '!scripts/**/node_modules/**' + 'scripts/**/*.{js,ts}', + 'app/**/*.{ts,tsx}', + 'remotion/**/*.{ts,tsx}', + '!**/*.test.{js,ts,tsx}', + '!**/__tests__/**', + '!**/__mocks__/**', + '!**/node_modules/**', ], - coverageReporters: ['text', 'lcov', 'html'], - - // Setup files - setupFilesAfterEnv: ['<rootDir>/tests/setup.js'], - - // Module transformations for ES modules - transform: { - '^.+\\.js$': 'babel-jest' - }, - transformIgnorePatterns: [ - 'node_modules/(?!(sharp)/)' + + projects: [ + // ── Node project: scripts + pipeline integration tests ───────────────── + { + displayName: 'node', + testEnvironment: 'node', + testMatch: [ + '<rootDir>/scripts/**/*.test.{js,ts}', + '<rootDir>/scripts/__tests__/**/*.{js,ts}', + '<rootDir>/tests/integration/**/*.test.{js,ts}', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, + transformIgnorePatterns: ['node_modules/(?!(sharp)/)'], + setupFilesAfterEnv: ['<rootDir>/tests/setup.js'], + moduleNameMapper: { + '^@/(.*)$': '<rootDir>/$1', + }, + }, + + // ── React project: app + remotion component tests ────────────────────── + { + displayName: 'react', + testEnvironment: 'jsdom', + testMatch: [ + '<rootDir>/app/**/*.test.{ts,tsx}', + '<rootDir>/remotion/**/*.test.{ts,tsx}', + '<rootDir>/tests/react/**/*.test.{ts,tsx}', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, + transformIgnorePatterns: ['node_modules/'], + setupFilesAfterEnv: ['<rootDir>/tests/setup.react.ts'], + moduleNameMapper: { + '^@/(.*)$': '<rootDir>/$1', + '\\.(css|less|scss|sass)$': '<rootDir>/tests/__mocks__/styleMock.js', + '\\.(gif|ttf|eot|svg|png|jpg|jpeg|webp|mp4|mp3|wav|ogg)$': + '<rootDir>/tests/__mocks__/fileMock.js', + }, + }, ], - - // Timeout for async operations - testTimeout: 30000, - - // Verbose output - verbose: true }; + +export default config; diff --git a/package-lock.json b/package-lock.json index 016c565..d854ce0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "file-saver": "^2.0.5", "fs-extra": "^11.2.0", "jszip": "^3.10.1", - "next": "16.1.6", + "next": "16.2.6", "pdfkit": "^0.14.0", "puppeteer": "^23.0.0", "react": "19.2.3", @@ -43,18 +43,27 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/preset-env": "^7.29.0", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@playwright/test": "^1.59.1", "@remotion/eslint-plugin": "^4.0.451", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/jest": "^30.0.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "babel-jest": "^30.2.0", "eslint": "^9", - "eslint-config-next": "16.1.6", + "eslint-config-next": "16.2.6", "husky": "^9.1.7", "jest": "^29.7.0", + "jest-environment-jsdom": "^30.4.1", "jest-environment-node": "^29.7.0", - "postcss": "^8.5.6", + "lint-staged": "^16.4.0", + "postcss": "^8.5.10", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "tailwindcss": "^4", @@ -70,6 +79,13 @@ "lightningcss-linux-x64-gnu": "1.30.2" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "dev": true, @@ -81,6 +97,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "license": "MIT", @@ -1294,6 +1331,75 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.29.0", "dev": true, @@ -1408,6 +1514,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", "dev": true, @@ -1563,6 +1689,47 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.6", "license": "MIT", @@ -1617,6 +1784,121 @@ "dev": true, "license": "MIT" }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -3018,6 +3300,16 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { "version": "29.7.0", "dev": true, @@ -3032,86 +3324,344 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect": { - "version": "29.7.0", + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", + "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3" + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/globals": { - "version": "29.7.0", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "@types/node": "*", + "jest-regex-util": "30.4.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/pattern": { - "version": "30.0.1", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" + "@sinclair/typebox": "^0.34.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/pattern/node_modules/jest-regex-util": { - "version": "30.0.1", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/reporters": { - "version": "29.7.0", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", "@jest/test-result": "^29.7.0", "@jest/transform": "^29.7.0", "@jest/types": "^29.6.3", @@ -3369,9 +3919,9 @@ } }, "node_modules/@mediabunny/aac-encoder": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.39.2.tgz", - "integrity": "sha512-KD6KADVzAnW7tqhRFGBOX4uaiHbd0Yxvg0lfthj3wJLAEEgEBAvi43w+ZXWeEn54X/jpabrLe4bW/eYFFvlbUA==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.42.0.tgz", + "integrity": "sha512-Wcxn/Ez5oYNnHtNDjSV1kxrGeHm9Xq9SycdyXBLwbyd3VJvsagR9r/XwtHYOp2CGYKo+biN/NYhJuEORSXkkGQ==", "license": "MPL-2.0", "funding": { "type": "individual", @@ -3382,9 +3932,9 @@ } }, "node_modules/@mediabunny/flac-encoder": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.39.2.tgz", - "integrity": "sha512-VwBr3AzZTPEEPvt4aladZiXwOf3W293eq213zDupGQi/taS8WWNqDd3eBdf8FfvlbXATfbRiycXDKyQ0HlOZaQ==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.42.0.tgz", + "integrity": "sha512-yguntZRxJ9ghVVxV1okbehi5jUgmlGbvjknNAgTzkM0m6F+IGZn9hG6ztW4Pvfdmv8YH1m7WbcB8wj87UHVsyg==", "license": "MPL-2.0", "funding": { "type": "individual", @@ -3395,9 +3945,9 @@ } }, "node_modules/@mediabunny/mp3-encoder": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.39.2.tgz", - "integrity": "sha512-3rrodrGnUpUP8F2d1aRUl8IvjqK3jegkupbOzvOokooSAO5rXk2Lr5jZe7TnPeiVGiXfmnoJ7s9uyUOHlCd8qw==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.42.0.tgz", + "integrity": "sha512-J6TwGa6rbVpXxcOETL6NVTi8UFYkd/W+DcV0CsQEEEjjMnL9Y5SN1h36Vc1CDSGVDSq62E8PxbG8C8eOIDqbFg==", "license": "MPL-2.0", "funding": { "type": "individual", @@ -3473,11 +4023,15 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.1.6", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", + "integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==", "dev": true, "license": "MIT", "dependencies": { @@ -3485,7 +4039,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", "cpu": [ "arm64" ], @@ -3499,12 +4055,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3514,12 +4071,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -3529,12 +4087,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -3544,12 +4103,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -3559,12 +4119,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -3574,12 +4135,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -3589,12 +4151,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -3643,6 +4206,22 @@ "node": ">=12.4.0" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@puppeteer/browsers": { "version": "2.6.1", "license": "Apache-2.0", @@ -3674,22 +4253,22 @@ } }, "node_modules/@remotion/bundler": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.451.tgz", - "integrity": "sha512-/E4lXcFWR1uuv9qmnkV7cqTRH39jSpTpfzk56JOhhvLiWwdaFXGVaoEKJD0XO9/M5IqNYXuqVgDXcEqlTpeFaQ==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.459.tgz", + "integrity": "sha512-lMEhmsCafWe7yjpCCYCVKZlmo+Vu/GqSKftWBDDQnxUiMrA9a3JBHIRiuMovIoi+5sx9Agwi9JKEcXbgYB0MKQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@remotion/media-parser": "4.0.451", - "@remotion/studio": "4.0.451", - "@remotion/studio-shared": "4.0.451", + "@remotion/media-parser": "4.0.459", + "@remotion/studio": "4.0.459", + "@remotion/studio-shared": "4.0.459", "@rspack/core": "1.7.6", "@rspack/plugin-react-refresh": "1.6.1", "esbuild": "0.25.0", "loader-utils": "2.0.4", - "postcss": "8.5.1", + "postcss": "8.5.10", "postcss-value-parser": "4.2.0", "react-refresh": "0.18.0", - "remotion": "4.0.451", + "remotion": "4.0.459", "source-map": "0.7.3", "style-loader": "4.0.0", "webpack": "5.105.0" @@ -3699,32 +4278,14 @@ "react-dom": ">=16.8.0" } }, - "node_modules/@remotion/bundler/node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" + "node_modules/@remotion/bundler/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, "node_modules/@remotion/bundler/node_modules/source-map": { @@ -3743,22 +4304,22 @@ "license": "MIT" }, "node_modules/@remotion/cli": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.451.tgz", - "integrity": "sha512-c89ogR7+gOAR64G/skFk4D5BNXIwPzAtBCx+FPOKz6N589e0+MTEmGGlJMN+9fKnT1GdNWiwwlNCU5Jx5aqG6Q==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.459.tgz", + "integrity": "sha512-pTELzNvRfSSpH9YkNqQ0ghgeldODDbuBaAEbTHfHnMtD0J7iP3V1lVUGACRyy3g/BEcytPfN2a1A36HrTrvZdg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@remotion/bundler": "4.0.451", - "@remotion/media-utils": "4.0.451", - "@remotion/player": "4.0.451", - "@remotion/renderer": "4.0.451", - "@remotion/studio": "4.0.451", - "@remotion/studio-server": "4.0.451", - "@remotion/studio-shared": "4.0.451", + "@remotion/bundler": "4.0.459", + "@remotion/media-utils": "4.0.459", + "@remotion/player": "4.0.459", + "@remotion/renderer": "4.0.459", + "@remotion/studio": "4.0.459", + "@remotion/studio-server": "4.0.459", + "@remotion/studio-shared": "4.0.459", "dotenv": "17.3.1", "minimist": "1.2.6", "prompts": "2.4.2", - "remotion": "4.0.451" + "remotion": "4.0.459" }, "bin": { "remotion": "remotion-cli.js", @@ -3786,10 +4347,20 @@ "version": "1.2.6", "license": "MIT" }, + "node_modules/@remotion/cli/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remotion/compositor-darwin-arm64": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.451.tgz", - "integrity": "sha512-8Fl7oP3Bv8jqbkTljgBbcV6T8TtGQ6CQ8N4Z/yS66OvNiSILVu3g8R9hMq/pgSp+B+a1YvucO1HW3V3Mv0t5ew==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.459.tgz", + "integrity": "sha512-PAFL4p/mXu1o9Gq98pBUVSdkQ89cm2md13Ej7NdWB+NhSroPNXKNh91vdMf3cUa/sc0mjq8+gacOU7W9e47QPw==", "cpu": [ "arm64" ], @@ -3799,9 +4370,9 @@ ] }, "node_modules/@remotion/compositor-darwin-x64": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.451.tgz", - "integrity": "sha512-p5dVw2FnVhbGfK8z6C9EmbitJGyu+Yi3N5ujkrXX/76QxDnekEhYuDSR+pcBLPiYWiudpPc79iI/kLzzBKReKQ==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.459.tgz", + "integrity": "sha512-CBtAEiVzcLr75zKQNfMbPKkPJXcLWidtryUXlhmJeGv4N52qL/lycQ6o2/olLTnUeuxjjnop8RWEEVp9mxu1TA==", "cpu": [ "x64" ], @@ -3811,9 +4382,9 @@ ] }, "node_modules/@remotion/compositor-linux-arm64-gnu": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.451.tgz", - "integrity": "sha512-H0tELxkx0kc1eEoQBah63T6/h/2dtSIwkR+ABgbhByD0fLmKKBT6mU9TX62u1a1zRNpeWmYvcuVT80u42PKQIw==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.459.tgz", + "integrity": "sha512-uiBxn7vICcu7yoN1LW+m7em7/HStyAaQYYII0XSwWFf/QSsfeEESxVJTUftzAeqVEGU4lM37L6/Qob+DmLvHLA==", "cpu": [ "arm64" ], @@ -3823,9 +4394,9 @@ ] }, "node_modules/@remotion/compositor-linux-arm64-musl": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.451.tgz", - "integrity": "sha512-aV8yea+ot3wZ1EZDn0g+HPiEphXZGP8tc9xu37HnqwwsrRJAdecZdwC0u+5omO4lBCNiAuVcjraS+eMu2LllLQ==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.459.tgz", + "integrity": "sha512-W3F03qxAK8WAqvD5ZvxfRpm8mHcfSXAdq0pIdQZHhLPYAn2VwLYm7VxT0a/Pkg244XdNJRhEAxncOqSLcbYEMw==", "cpu": [ "arm64" ], @@ -3835,9 +4406,9 @@ ] }, "node_modules/@remotion/compositor-linux-x64-gnu": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.451.tgz", - "integrity": "sha512-LMOu8uOGMRDlDGlUfP13fben8l33nCjx8lFq8GKhzrg5UrfTfY0DXL1VNBohhVN3QI+e+NphaSeskco1Jw0UXQ==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.459.tgz", + "integrity": "sha512-ktH8fSVEjxUu+Oo4xwSZ1ZaCNZ1tAhBoFp/LHPfSdN5Mcl4WY9/DL53m1ejSIBDJfe883/lOcfw5XcIkCDJVRw==", "cpu": [ "x64" ], @@ -3847,9 +4418,9 @@ ] }, "node_modules/@remotion/compositor-linux-x64-musl": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.451.tgz", - "integrity": "sha512-xJUJlGhPBqwCOi4cIbmQ6z715BoUPOTTlcStzFIfrPtH0s6ieFCCy888c3M3X5xIXYbjbKgWjG/M0Tv2t6LpcQ==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.459.tgz", + "integrity": "sha512-ECPqxb3xs0LUdWD4w23tseBhquehCNvSeD0pDaYxk+oBJPJ9QwqBhosKYgGmm9p3ar6YvT8ig+Y044IRStqHcw==", "cpu": [ "x64" ], @@ -3859,9 +4430,9 @@ ] }, "node_modules/@remotion/compositor-win32-x64-msvc": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.451.tgz", - "integrity": "sha512-ZelPVRyVu6ABamoE+ETCbA9QlVRGFTMy/EwrtxQzzf2qmieZL2sCu2s4mXfg3qi3EAJYzs8oLZiQWY3RxRz7rQ==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.459.tgz", + "integrity": "sha512-0Nj75T9O8mk8UiEucJZDW7v8xByTYhN7UeA6RlMM08s/RTreOlpP12JP9xSH7028vIErBBV0ZEbX6FpKxAI8SQ==", "cpu": [ "x64" ], @@ -4041,75 +4612,105 @@ } }, "node_modules/@remotion/licensing": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.451.tgz", - "integrity": "sha512-LQKL1gKZEWY71ueW+cSTFP3pR/kbNfzoOgu+pplYMPXLf/jtNrlkDW5cr6Bnl5xtSUQczrP3L8J18gwCi2kH7Q==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.459.tgz", + "integrity": "sha512-7/e1RXXp7aGTt5nMSlgEnvCLjy604aiBDI9pweT+kLZoCEcKeDHMX01CXN3HNtESWp7H5L2X6z8wYCGUOq5brA==", "license": "MIT" }, "node_modules/@remotion/media-parser": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.451.tgz", - "integrity": "sha512-GeGHDxV2ljUNZc+TAFjBGYM8tLFKToln34Xn9ymu7R25lO79DKATqSwu4nnLlKx3qsWvQ2Yy2lVj7x9pXIShQg==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.459.tgz", + "integrity": "sha512-W4IrDTnGDY3gFfb4MpuQocvbXuqFfjM1gm8hxpUZ1LgVK2pDWypAzsskuwtOU2k34ykLW/5DsjvmVZFj0Pcj8g==", "license": "Remotion License https://remotion.dev/license" }, "node_modules/@remotion/media-utils": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.451.tgz", - "integrity": "sha512-vi1CvRxaRX7MMR+fm7FBqJZZlxoYvrvGcgMwEQbfG0kGFmFUxJyHr4gcpdPoqlowJLywuDVhs2SR5RuzAQoonw==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.459.tgz", + "integrity": "sha512-HglobwSGK3VbBpDVcWzEJg3Wq8js426HVOEl4tE8mdLB0klXoVWVHrlVSQp4hP3eZhsYugvt6p2+qh0BpFJ2JQ==", "license": "MIT", "dependencies": { - "mediabunny": "1.39.2", - "remotion": "4.0.451" + "mediabunny": "1.42.0", + "remotion": "4.0.459" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, + "node_modules/@remotion/media-utils/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remotion/player": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.451.tgz", - "integrity": "sha512-mJSaWHsMQuDXVVVh6UVBySBzzzqeSI+MHUuS+6hk/CrqZwBz1CQnsrtFZcjcnBpY40ao+ZqMA6uTEgs9n34YlQ==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.459.tgz", + "integrity": "sha512-K8CpB0M/VTUBJ1G+KxxKw/jyqbneI+h2rXUANnty+skZKkv7c56VXEjnduQMOn8PPRdLctKyEl/KCZQDHG0HrQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "remotion": "4.0.451" + "remotion": "4.0.459" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, + "node_modules/@remotion/player/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remotion/renderer": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.451.tgz", - "integrity": "sha512-bIYrbI0eIs5tk+BFnx/a4XAb1MXEHyjh6/lh69KWYp5AmFpsCA/bqc/ju0yx7ucaFuR5KDwh+MWJb5rHef54TQ==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.459.tgz", + "integrity": "sha512-dgJTAAxb0Cheb6qWpqN3oW0ZmgzmQqNcI1dGVxSMcwges+WGIpxamyvolgweokEBPcDJ/aK2xiJrhgyTxaDPSQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@remotion/licensing": "4.0.451", - "@remotion/streaming": "4.0.451", + "@remotion/licensing": "4.0.459", + "@remotion/streaming": "4.0.459", "execa": "5.1.1", "extract-zip": "2.0.1", - "remotion": "4.0.451", + "remotion": "4.0.459", "source-map": "^0.8.0-beta.0", "ws": "8.17.1" }, "optionalDependencies": { - "@remotion/compositor-darwin-arm64": "4.0.451", - "@remotion/compositor-darwin-x64": "4.0.451", - "@remotion/compositor-linux-arm64-gnu": "4.0.451", - "@remotion/compositor-linux-arm64-musl": "4.0.451", - "@remotion/compositor-linux-x64-gnu": "4.0.451", - "@remotion/compositor-linux-x64-musl": "4.0.451", - "@remotion/compositor-win32-x64-msvc": "4.0.451" + "@remotion/compositor-darwin-arm64": "4.0.459", + "@remotion/compositor-darwin-x64": "4.0.459", + "@remotion/compositor-linux-arm64-gnu": "4.0.459", + "@remotion/compositor-linux-arm64-musl": "4.0.459", + "@remotion/compositor-linux-x64-gnu": "4.0.459", + "@remotion/compositor-linux-x64-musl": "4.0.459", + "@remotion/compositor-win32-x64-msvc": "4.0.459" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, - "node_modules/@remotion/renderer/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "node_modules/@remotion/renderer/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", "deprecated": "The work that was done in this beta branch won't be included in future versions", "license": "BSD-3-Clause", @@ -4148,27 +4749,27 @@ "license": "MIT" }, "node_modules/@remotion/streaming": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.451.tgz", - "integrity": "sha512-XWPNqLXEeP5+SCU2renegG0tDExwSEY7FXXaymwO2OJ/zzYu6ilAkFMi3T/JfXyNxbVivYdIB295P1GnHGHp4g==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.459.tgz", + "integrity": "sha512-waQjPDGmMOBEdEDlqm3v5GGQ6H8T1y7ayCWvGBZmw3gWcjlE0bTKHmTr37yZN7QxUyGYuaaZiFjzyDWSGblPDQ==", "license": "MIT" }, "node_modules/@remotion/studio": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.451.tgz", - "integrity": "sha512-u+4Kdoy9Yszftk5aycG+taCGPPOZjk44P5tjgjt4qoEKEc+UpzJEd7phhb3IbPSkJvMDUGMUJw4AgPuY0jpuFg==", - "license": "MIT", - "dependencies": { - "@remotion/media-utils": "4.0.451", - "@remotion/player": "4.0.451", - "@remotion/renderer": "4.0.451", - "@remotion/studio-shared": "4.0.451", - "@remotion/web-renderer": "4.0.451", - "@remotion/zod-types": "4.0.451", - "mediabunny": "1.39.2", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.459.tgz", + "integrity": "sha512-re5JewMvp9p3kZ0wAmHdO4/lYGEOB1QFx2YfNqZmWlCKy3qLGVTjIkwAhY8NQ9evHrKEm72tYedGe9VMqukU8g==", + "license": "MIT", + "dependencies": { + "@remotion/media-utils": "4.0.459", + "@remotion/player": "4.0.459", + "@remotion/renderer": "4.0.459", + "@remotion/studio-shared": "4.0.459", + "@remotion/web-renderer": "4.0.459", + "@remotion/zod-types": "4.0.459", + "mediabunny": "1.42.0", "memfs": "3.4.3", "open": "^8.4.2", - "remotion": "4.0.451", + "remotion": "4.0.459", "semver": "7.5.3", "source-map": "0.7.3", "zod": "4.3.6" @@ -4179,21 +4780,21 @@ } }, "node_modules/@remotion/studio-server": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.451.tgz", - "integrity": "sha512-MXoaKNYcDzZYO2RlL/DXod+4HKiD5HDQQ1JmCpCQbz45kd3HJtZfvQId2eLt4dV4LkfNHxInNYmmSpc9Rz44xA==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.459.tgz", + "integrity": "sha512-2TrslnE4GzgqwE+vjMO1xlFV7eilw73ut577bK+UZR3DEoX2nTZGKvVkt/0q8Lpxx1naZ4xJOnEur/fBN0NNuQ==", "license": "MIT", "dependencies": { "@babel/parser": "7.24.1", "@babel/types": "7.24.0", - "@remotion/bundler": "4.0.451", - "@remotion/renderer": "4.0.451", - "@remotion/studio-shared": "4.0.451", + "@remotion/bundler": "4.0.459", + "@remotion/renderer": "4.0.459", + "@remotion/studio-shared": "4.0.459", "memfs": "3.4.3", "open": "^8.4.2", "prettier": "3.8.1", "recast": "0.23.11", - "remotion": "4.0.451", + "remotion": "4.0.459", "semver": "7.5.3", "source-map": "0.7.3" } @@ -4236,6 +4837,16 @@ "node": ">=10" } }, + "node_modules/@remotion/studio-server/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remotion/studio-server/node_modules/semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", @@ -4267,12 +4878,22 @@ "license": "ISC" }, "node_modules/@remotion/studio-shared": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.451.tgz", - "integrity": "sha512-+04L8Tf9MDklfDDZR9hxaxxNeni0haeQIzYeCqUn+esS13YJqrUnqKzlvrhT8/c8K5cdU24x4XkW/EYXYdDL8A==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.459.tgz", + "integrity": "sha512-YvJXf19EAxNT8Qz1OqzZCQnbnb9wQc3IjIEdxWrLI3D6vL6JQCbLtH7s+Y65c5xx1dZH/fOuX25nBl911hFJTQ==", "license": "MIT", "dependencies": { - "remotion": "4.0.451" + "remotion": "4.0.459" + } + }, + "node_modules/@remotion/studio-shared/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, "node_modules/@remotion/studio/node_modules/lru-cache": { @@ -4287,6 +4908,16 @@ "node": ">=10" } }, + "node_modules/@remotion/studio/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remotion/studio/node_modules/semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", @@ -4318,35 +4949,55 @@ "license": "ISC" }, "node_modules/@remotion/web-renderer": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.451.tgz", - "integrity": "sha512-ORnRL2Cg5coh040XqWTzwuAROLaerKgYrNui+O8903odMWHAKlPhTY4RB1neLZ/rkDSColnOHWFW5mXu9fLdCg==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.459.tgz", + "integrity": "sha512-ynyxL+Tt4DngSAzuBMoqTha0ARG96S5QwF+fdSAJvJgCIVbfbferMXKxu1ZrZZ61UktwpTI9AE5fDQGTsiOFZg==", "license": "UNLICENSED", "dependencies": { - "@mediabunny/aac-encoder": "1.39.2", - "@mediabunny/flac-encoder": "1.39.2", - "@mediabunny/mp3-encoder": "1.39.2", - "@remotion/licensing": "4.0.451", - "mediabunny": "1.39.2", - "remotion": "4.0.451" + "@mediabunny/aac-encoder": "1.42.0", + "@mediabunny/flac-encoder": "1.42.0", + "@mediabunny/mp3-encoder": "1.42.0", + "@remotion/licensing": "4.0.459", + "mediabunny": "1.42.0", + "remotion": "4.0.459" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, + "node_modules/@remotion/web-renderer/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@remotion/zod-types": { - "version": "4.0.451", - "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.451.tgz", - "integrity": "sha512-BHYXjfZqN+E+1TrgQfv8WjUlzC8VgD2Ra8pxTG2jxXxdedUJTnUUqvuZi7nm5qqXfhh5dKRpzHfRbm1PibzA4A==", + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.459.tgz", + "integrity": "sha512-j/fNgjKO9ATpaCBbWxJK07FoVIUPoGae2lqrws1MoSD4tZkoFssgkl5dLt/HimNKi6zTz+FrcUZ4c4tIZsUq1A==", "license": "MIT", "dependencies": { - "remotion": "4.0.451" + "remotion": "4.0.459" }, "peerDependencies": { "zod": "4.3.6" } }, + "node_modules/@remotion/zod-types/node_modules/remotion": { + "version": "4.0.459", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.459.tgz", + "integrity": "sha512-IRhfFepGJ/TPmN07rF2yS2YpFfHK61yKUKa2T35HwGvaYn4MVyOvl28YTByR4wqRfqB4R6Zn2aCYNJBfA8B4zA==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@rspack/binding": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.6.tgz", @@ -4692,20 +5343,167 @@ "tailwindcss": "4.1.18" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -4745,6 +5543,8 @@ }, "node_modules/@types/dom-mediacapture-transform": { "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", + "integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==", "license": "MIT", "dependencies": { "@types/dom-webcodecs": "*" @@ -4752,6 +5552,8 @@ }, "node_modules/@types/dom-webcodecs": { "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", "license": "MIT" }, "node_modules/@types/eslint": { @@ -4811,6 +5613,268 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/jest/node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "license": "MIT" @@ -4848,6 +5912,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "dev": true, @@ -6172,9 +7243,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.0.tgz", - "integrity": "sha512-5K9eNNn7ywHPsYnFwjKgYH8Hf8B5emh7JKcPaVjjrMJFQQwGpwowEnZNEtHs7DfR7hCZsmaK3VA4HUK0YarT+w==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6486,6 +7557,85 @@ "dev": true, "license": "MIT" }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/client-only": { "version": "0.0.1", "license": "MIT" @@ -6563,6 +7713,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -6748,6 +7905,13 @@ "version": "4.2.0", "license": "MIT" }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "dev": true, @@ -6756,23 +7920,88 @@ "cssesc": "bin/cssesc" }, "engines": { - "node": ">=4" + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" } }, - "node_modules/csstype": { - "version": "3.2.3", - "license": "MIT" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, "engines": { - "node": ">= 14" + "node": ">=18" } }, "node_modules/data-view-buffer": { @@ -6842,6 +8071,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -6973,6 +8209,17 @@ "node": ">= 14" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "license": "Apache-2.0", @@ -7030,6 +8277,14 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-helpers": { "version": "5.2.1", "license": "MIT", @@ -7131,6 +8386,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "license": "MIT", @@ -7138,6 +8406,19 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.4", "license": "MIT", @@ -7280,9 +8561,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "license": "MIT" }, "node_modules/es-object-atoms": { @@ -7856,11 +9137,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.1.6", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz", + "integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.1.6", + "@next/eslint-plugin-next": "16.2.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -8212,6 +9495,13 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -8353,9 +9643,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -8587,6 +9877,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "license": "MIT", @@ -8875,6 +10178,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -8941,6 +10257,19 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "funding": [ @@ -9011,6 +10340,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "dev": true, @@ -9043,7 +10382,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -9324,6 +10665,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "license": "MIT", @@ -9785,14 +11133,257 @@ "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.4.1.tgz", + "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/environment-jsdom-abstract": "30.4.1", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { @@ -10179,6 +11770,83 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.1.0", "dev": true, @@ -10428,6 +12096,156 @@ "version": "1.2.4", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", @@ -10485,6 +12303,160 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -10503,6 +12475,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "dev": true, @@ -10552,9 +12535,9 @@ } }, "node_modules/mediabunny": { - "version": "1.39.2", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.39.2.tgz", - "integrity": "sha512-VcrisGRt+OI7tTPrziucJoCIPYIS/DEWY37TqzQVLWSUUHiyvsiRizEypQ3FOlhfIZ4ytAG/Mw4zxfetCTyKUg==", + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.42.0.tgz", + "integrity": "sha512-s9ypTqLi6kbh95gC+YaJlG0PkLvMxu37Q/wO/pFZx0fUCA5Ym5mp+2dWoa83mKQ3Uo18aNlgev5iJ5ESZqWwgQ==", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -10632,6 +12615,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -10644,6 +12640,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.5", "dev": true, @@ -10731,12 +12737,14 @@ } }, "node_modules/next": { - "version": "16.1.6", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.2.6", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -10748,15 +12756,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -10853,32 +12861,6 @@ "@img/sharp-libvips-linux-x64": "1.2.4" } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/next/node_modules/semver": { "version": "7.7.4", "license": "ISC", @@ -10990,6 +12972,13 @@ "node": ">=8" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -11270,6 +13259,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -11404,6 +13406,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/png-js": { "version": "1.0.0" }, @@ -11415,10 +13464,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", - "dev": true, + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", @@ -11871,6 +13919,22 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "dev": true, + "license": "MIT" + }, "node_modules/react-number-format": { "version": "5.4.4", "license": "MIT", @@ -12025,6 +14089,20 @@ "node": ">=4" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "dev": true, @@ -12199,6 +14277,52 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/restructure": { "version": "2.0.1", "license": "MIT" @@ -12212,6 +14336,20 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "dev": true, @@ -12286,6 +14424,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "license": "MIT" @@ -12881,6 +15039,52 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "license": "MIT", @@ -12998,6 +15202,16 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "dev": true, @@ -13152,6 +15366,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "dev": true, @@ -13243,6 +15470,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tabbable": { "version": "6.4.0", "license": "MIT" @@ -13285,9 +15519,9 @@ } }, "node_modules/terser": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", - "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz", + "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -13408,6 +15642,16 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "dev": true, @@ -13452,6 +15696,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "dev": true, @@ -13477,6 +15741,19 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -14026,6 +16303,19 @@ "node": ">=10.12.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/walker": { "version": "1.0.8", "dev": true, @@ -14114,9 +16404,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz", - "integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz", + "integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -14144,6 +16434,30 @@ "node": ">=4.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", @@ -14304,6 +16618,23 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "license": "ISC", @@ -14316,6 +16647,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "license": "MIT", diff --git a/package.json b/package.json index cacd214..9594827 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,15 @@ "start": "next start", "sync": "node scripts/sync/sync-audio-video.js", "test": "jest", + "test:unit": "jest --selectProjects node react", + "test:integration": "jest --selectProjects node --testPathPattern='tests/integration'", + "test:react": "jest --selectProjects react", "test:coverage": "jest --coverage", "test:watch": "jest --watch", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug", + "test:all": "npm run test && npm run test:e2e", "thumbnail:candidates:extract": "python3 scripts/thumbnail/extract-speaker-candidates.py --transcript public/edit/transcript.json --camera-profiles public/camera/camera-profiles.json --video public/sync/output/synced-output-1.mp4 --output-dir public/thumbnail/candidates --num-candidates 6", "thumbnail:candidates:extract:docker": "docker compose run --rm thumbnail python3 scripts/thumbnail/extract-speaker-candidates.py --transcript public/edit/transcript.json --camera-profiles public/camera/camera-profiles.json --video public/sync/output/synced-output-1.mp4 --output-dir public/thumbnail/candidates --num-candidates 6", "thumbnail:cutouts:generate": "node scripts/thumbnail/generate-cutouts-from-selection.js", @@ -79,15 +86,15 @@ "@remotion/sfx": "^4.0.451", "@tabler/icons-react": "^3.29.0", "@types/file-saver": "^2.0.7", + "canvas": "^3.2.3", "dayjs": "^1.11.19", "dotenv": "^16.0.0", "embla-carousel-react": "^8.6.0", "fft.js": "^4.0.4", "file-saver": "^2.0.5", - "canvas": "^3.2.3", "fs-extra": "^11.2.0", "jszip": "^3.10.1", - "next": "16.1.6", + "next": "16.2.6", "pdfkit": "^0.14.0", "puppeteer": "^23.0.0", "react": "19.2.3", @@ -100,26 +107,44 @@ "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-linux-x64": "0.33.5", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "lightningcss-linux-x64-gnu": "1.30.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "lightningcss-linux-arm64-gnu": "1.30.2" + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2" + }, + "overrides": { + "postcss": "^8.5.10" + }, + "lint-staged": { + "*.{ts,tsx}": "eslint --max-warnings=0", + "*.{js,mjs}": "eslint --max-warnings=0", + "*.json": "node .husky/validate-json.cjs", + "*.py": "python3 -m py_compile" }, "devDependencies": { "@babel/core": "^7.29.0", "@babel/preset-env": "^7.29.0", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@playwright/test": "^1.59.1", "@remotion/eslint-plugin": "^4.0.451", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/jest": "^30.0.0", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "babel-jest": "^30.2.0", "eslint": "^9", - "eslint-config-next": "16.1.6", + "eslint-config-next": "16.2.6", "husky": "^9.1.7", "jest": "^29.7.0", + "jest-environment-jsdom": "^30.4.1", "jest-environment-node": "^29.7.0", - "postcss": "^8.5.6", + "lint-staged": "^16.4.0", + "postcss": "^8.5.10", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "tailwindcss": "^4", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..a435b3e --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + // Fail the build on CI if test.only() was accidentally left in + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? 'github' : 'html', + + use: { + baseURL: 'http://localhost:3000', + // Capture trace on the first retry to help debug failures + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + // Start the Next.js dev server before running tests + webServer: { + command: 'npm run dev', + url: 'http://localhost:3000', + // Reuse the already-running dev server if present; in CI always start fresh + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/tests/__mocks__/fileMock.js b/tests/__mocks__/fileMock.js new file mode 100644 index 0000000..a1633ba --- /dev/null +++ b/tests/__mocks__/fileMock.js @@ -0,0 +1,3 @@ +// Replaces static asset imports (images, fonts, media) in Jest +const fileStub = 'test-file-stub'; +export default fileStub; diff --git a/tests/__mocks__/styleMock.js b/tests/__mocks__/styleMock.js new file mode 100644 index 0000000..1792d9a --- /dev/null +++ b/tests/__mocks__/styleMock.js @@ -0,0 +1,3 @@ +// Replaces CSS/SCSS module imports in Jest (jsdom can't process them) +const styleMock = {}; +export default styleMock; diff --git a/tests/setup.react.ts b/tests/setup.react.ts new file mode 100644 index 0000000..8ae30d0 --- /dev/null +++ b/tests/setup.react.ts @@ -0,0 +1,29 @@ +import '@testing-library/jest-dom'; + +// Silence non-error console output in component tests. +// Use console.error freely in tests to debug — it still prints. +beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +// Mock Next.js router (used by many app/ components) +jest.mock('next/navigation', () => ({ + useRouter: () => ({ push: jest.fn(), replace: jest.fn(), back: jest.fn() }), + usePathname: () => '/', + useSearchParams: () => new URLSearchParams(), +})); + +// Mock next/image so components render without Next.js image optimisation +jest.mock('next/image', () => ({ + __esModule: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default: ({ src, alt, ...rest }: any) => + // biome-ignore lint: test mock intentionally uses img + Object.assign(document.createElement('img'), { src, alt, ...rest }), +}));