[
  {
    "path": ".agents/skills/add-tts-engine/SKILL.md",
    "content": "---\nname: add-tts-engine\ndescription: Use this skill to add a new TTS engine to Voicebox. It walks through dependency research, backend implementation, frontend wiring, PyInstaller bundling, and frozen-build testing. Always start with Phase 0 (dependency audit) before writing any code.\n---\n\n# Add TTS Engine\n\n## Goal\n\nIntegrate a new text-to-speech engine into Voicebox end-to-end: dependency research, backend protocol implementation, frontend UI wiring, PyInstaller bundling, and frozen-build verification. The user should only need to test the final build locally.\n\n## Reference Doc\n\nThe full phased guide lives at `docs/content/docs/developer/tts-engines.mdx`. **Read this file in its entirety before starting.** It contains:\n\n- Phase 0: Dependency research (mandatory before writing code)\n- Phase 1: Backend implementation (`TTSBackend` protocol)\n- Phase 2: Route and service integration (usually zero changes)\n- Phase 3: Frontend integration (5 files)\n- Phase 4: Dependencies (`requirements.txt`, justfile, CI, Docker)\n- Phase 5: PyInstaller bundling (`build_binary.py` + `server.py`)\n- Phase 6: Common upstream workarounds\n- Implementation checklist (gate between phases)\n\n## Workflow\n\n### 1. Read the guide\n\n```bash\n# Read the full TTS engines doc\ncat docs/content/docs/developer/tts-engines.mdx\n```\n\nInternalize all phases, especially Phase 0 and Phase 5. The v0.2.3 release was three patch releases because Phase 0 was skipped.\n\n### 2. Dependency research (Phase 0)\n\nClone the model library into a temporary directory and audit it. Do NOT skip this.\n\n```bash\nmkdir /tmp/engine-research && cd /tmp/engine-research\ngit clone <model-library-url>\n```\n\nRun the grep searches from Phase 0.2 in the guide against the cloned source and its transitive dependencies. Produce a written dependency audit covering:\n\n1. PyPI vs non-PyPI packages\n2. PyInstaller directives needed (`--collect-all`, `--copy-metadata`, `--hidden-import`)\n3. Runtime data files that must be bundled\n4. Native library paths that need env var overrides in frozen builds\n5. Monkey-patches needed (`torch.load`, float64, MPS, HF token)\n6. Sample rate\n7. Model download method (`from_pretrained` vs `snapshot_download` + `from_local`)\n\nTest model loading and generation on CPU in the throwaway venv before proceeding.\n\n### 3. Implement (Phases 1–4)\n\nFollow the guide's phases in order. Key files to modify:\n\n**Backend (Phase 1):**\n- Create `backend/backends/<engine>_backend.py`\n- Register in `backend/backends/__init__.py` (ModelConfig + TTS_ENGINES + factory)\n- Update regex in `backend/models.py`\n\n**Frontend (Phase 3):**\n- `app/src/lib/api/types.ts` — engine union type\n- `app/src/lib/constants/languages.ts` — ENGINE_LANGUAGES\n- `app/src/components/Generation/EngineModelSelector.tsx` — ENGINE_OPTIONS, ENGINE_DESCRIPTIONS\n- `app/src/lib/hooks/useGenerationForm.ts` — Zod schema, model-name mapping\n- `app/src/components/ServerSettings/ModelManagement.tsx` — MODEL_DESCRIPTIONS\n\n**Dependencies (Phase 4):**\n- `backend/requirements.txt`\n- `justfile` (setup-python, setup-python-release targets)\n- `.github/workflows/release.yml`\n- `Dockerfile` (if applicable)\n\n### 4. PyInstaller bundling (Phase 5)\n\nRegister the engine in `backend/build_binary.py`:\n- `--hidden-import` for the backend module and model package\n- `--collect-all` for packages using `inspect.getsource`, shipping data files, or native libraries\n- `--copy-metadata` for packages using `importlib.metadata`\n\nIf the engine has native data paths, add `os.environ.setdefault()` in `backend/server.py` inside the `if getattr(sys, 'frozen', False):` block.\n\n### 5. Verify in dev mode\n\n```bash\njust dev\n```\n\nTest the full chain: model download → load → generate → voice cloning.\n\n### 6. Use the checklist\n\nWalk through the Implementation Checklist at the bottom of `tts-engines.mdx`. Every item must be checked before handing the build to the user.\n\n## Key Lessons (from v0.2.3)\n\nThese are the most common failure modes. Phase 0 research catches all of them:\n\n| Pattern | Symptom in Frozen Build | Fix |\n|---------|------------------------|-----|\n| `@typechecked` / `inspect.getsource()` | \"could not get source code\" | `--collect-all <package>` |\n| Package ships pretrained model files | `FileNotFoundError` for `.pth.tar`, `.yaml` | `--collect-all <package>` |\n| C library with hardcoded system paths | `FileNotFoundError` for `/usr/share/...` | `--collect-all` + env var in `server.py` |\n| `importlib.metadata.version()` | \"No package metadata found\" | `--copy-metadata <package>` |\n| `torch.load` without `map_location` | CUDA device not available on CPU build | Monkey-patch `torch.load` |\n| `torch.from_numpy` on float64 data | dtype mismatch RuntimeError | Cast to `.float()` |\n| `token=True` in HF download calls | Auth failure without stored HF token | Use `snapshot_download(token=None)` + `from_local()` |\n\n## Notes\n\n- The route and service layers have zero per-engine dispatch points. `main.py` requires zero changes.\n- The model config registry in `backends/__init__.py` handles all dispatch automatically.\n- Use `get_torch_device()` and `model_load_progress()` from `backends/base.py` — don't reimplement device detection or progress tracking.\n- Always test with a **clean HuggingFace cache** (no pre-downloaded models from dev).\n- Do NOT push or create a release. Hand the build to the user for local testing.\n"
  },
  {
    "path": ".agents/skills/draft-release-notes/SKILL.md",
    "content": "---\nname: draft-release-notes\ndescription: Use this skill to draft or update the [Unreleased] section of CHANGELOG.md from the actual changes since the last tag. Run this at any point during development to keep a working copy of the release narrative. Does NOT bump versions or create tags.\n---\n\n# Draft Release Notes\n\n## Goal\n\nUpdate the `[Unreleased]` section at the top of `CHANGELOG.md` with a narrative release story based on the real changes since the last tag. This is a **non-destructive working copy** — run it as many times as you want during development.\n\n## Workflow\n\n1. **Identify the last release tag and gather changes.**\n\n   ```bash\n   LAST_TAG=$(git tag --list \"v*\" --sort=-v:refname | head -n 1)\n   echo \"Last tag: $LAST_TAG\"\n   ```\n\n   Then collect raw material from three sources:\n\n   a. **Commit log since last tag:**\n   ```bash\n   git log --oneline \"$LAST_TAG\"..HEAD\n   ```\n\n   b. **GitHub-generated release notes preview** (PR titles, new contributors):\n   ```bash\n   gh api repos/:owner/:repo/releases/generate-notes \\\n     -f tag_name=\"vNEXT\" \\\n     -f target_commitish=\"$(git rev-parse HEAD)\" \\\n     -f previous_tag_name=\"$LAST_TAG\" \\\n     --jq '.body'\n   ```\n\n   c. **Diff stat for theme analysis:**\n   ```bash\n   git diff --stat \"$LAST_TAG\"..HEAD\n   ```\n\n2. **Draft the release narrative.**\n\n   Write markdown for the `[Unreleased]` section following the format below. Do not include the `## [Unreleased]` heading itself — just the body content.\n\n3. **Update CHANGELOG.md.**\n\n   Replace everything between `## [Unreleased]` and the next `## [` heading with the new draft. Preserve the HTML comment header and all existing release sections below.\n\n   The `[Unreleased]` section must always exist and always be the first section after the header comments.\n\n4. **Do NOT commit, tag, or bump versions.** Just leave the file modified in the working tree.\n\n## Release Story Format\n\nStructure the `[Unreleased]` section like this:\n\n```markdown\n## [Unreleased]\n\n<One strong opening paragraph: what this release is about and why it matters.\nTie it to concrete shipped changes. No vague hype.>\n\n<One paragraph on major technical shifts, if applicable.>\n\n### <Feature/Theme Group>\n- Bullet points with specifics\n- Reference PRs where available: ([#123](https://github.com/jamiepine/voicebox/pull/123))\n\n### <Another Group>\n- ...\n\n### Bug Fixes\n- ...\n```\n\n### Style Guidelines\n\n- **Factual and specific.** Every claim should trace to a real commit or PR.\n- **Narrative over list.** Lead with paragraphs that tell the story, then support with bullets.\n- **Group by theme, not by commit.** Cluster related changes under descriptive headings.\n- **Reference PRs** where they exist, but don't fabricate them.\n- **Skip trivial chores** (typo fixes, CI tweaks) unless they're the bulk of the release.\n- **Match the voice of existing releases** — look at the v0.2.1 and v0.2.3 entries in CHANGELOG.md for tone reference.\n\n## When There Are No Changes\n\nIf `git log \"$LAST_TAG\"..HEAD` is empty, leave the `[Unreleased]` section empty (just the heading) and tell the user there's nothing to draft.\n\n## Notes\n\n- This skill only touches the `[Unreleased]` section. It never modifies stamped release sections.\n- The agent can be asked to run this skill at any point — mid-feature, before a PR, or right before cutting a release.\n- The `release-bump` skill depends on this draft being up to date before it finalizes.\n"
  },
  {
    "path": ".agents/skills/release-bump/SKILL.md",
    "content": "---\nname: release-bump\ndescription: Use this skill to finalize a release. It stamps the [Unreleased] changelog section with a version and date, runs bumpversion to update all version files, and creates the release commit and tag. Only run this when you're ready to ship.\n---\n\n# Release Bump\n\n## Goal\n\nFinalize the changelog draft, bump the version across all tracked files, and create a tagged release commit. After this skill runs, the repo has a clean release commit and tag ready to push.\n\n## Prerequisites\n\n- `gh` CLI installed and authenticated (`gh auth status`).\n- `bumpversion` installed (`pip install bumpversion` or available in the project venv).\n- The `[Unreleased]` section of `CHANGELOG.md` should already contain the release narrative. If it's empty or stale, run the `draft-release-notes` skill first.\n\n## Workflow\n\n1. **Verify the working tree is clean** (except `CHANGELOG.md` which may have the draft).\n\n   ```bash\n   git status --porcelain\n   ```\n\n   Only `CHANGELOG.md` (and optionally `.agents/` files) should be modified. If there are other uncommitted changes, stop and ask the user to commit or stash them first.\n\n2. **Determine the bump level.**\n\n   Ask the user if not specified: `patch`, `minor`, or `major`. Check the current version:\n\n   ```bash\n   grep '^current_version' .bumpversion.cfg\n   ```\n\n3. **Stamp the changelog.**\n\n   Read the current `[Unreleased]` content from `CHANGELOG.md`. Compute the new version (based on bump level and current version). Then:\n\n   a. Replace the `## [Unreleased]` section body with an empty placeholder.\n   b. Insert a new stamped section immediately after `## [Unreleased]`:\n\n   ```markdown\n   ## [Unreleased]\n\n   ## [X.Y.Z] - YYYY-MM-DD\n\n   <the content that was in [Unreleased]>\n   ```\n\n   c. Update the reference links at the bottom of the file:\n   - Change the `[Unreleased]` link to compare against the new tag\n   - Add a new link for the new version\n\n   ```markdown\n   [Unreleased]: https://github.com/jamiepine/voicebox/compare/vX.Y.Z...HEAD\n   [X.Y.Z]: https://github.com/jamiepine/voicebox/compare/vPREVIOUS...vX.Y.Z\n   ```\n\n4. **Stage the changelog.**\n\n   ```bash\n   git add CHANGELOG.md\n   ```\n\n5. **Run bumpversion.**\n\n   ```bash\n   bumpversion --allow-dirty <patch|minor|major>\n   ```\n\n   The `--allow-dirty` flag is needed because `CHANGELOG.md` is already staged. bumpversion will:\n   - Update version strings in all tracked files (see `.bumpversion.cfg`)\n   - Create a commit with message `Bump version: X.Y.Z -> A.B.C`\n   - Create a tag `vA.B.C`\n\n   The staged `CHANGELOG.md` will be included in this commit automatically.\n\n6. **Verify results.**\n\n   ```bash\n   git show --name-only --stat HEAD\n   git tag --list \"v*\" --sort=-v:refname | head -n 5\n   ```\n\n   Confirm the commit contains:\n   - `CHANGELOG.md`\n   - `.bumpversion.cfg`\n   - `tauri/src-tauri/tauri.conf.json`\n   - `tauri/src-tauri/Cargo.toml`\n   - `package.json`\n   - `app/package.json`\n   - `tauri/package.json`\n   - `landing/package.json`\n   - `web/package.json`\n   - `backend/__init__.py`\n\n   Confirm the new tag exists.\n\n7. **Do NOT push** unless the user explicitly asks. Report the tag name and suggest:\n\n   ```\n   Ready to push. When you're ready:\n     git push origin main --follow-tags\n   ```\n\n## Version Calculation Reference\n\nGiven current version `X.Y.Z`:\n- `patch` -> `X.Y.(Z+1)`\n- `minor` -> `X.(Y+1).0`\n- `major` -> `(X+1).0.0`\n\n## Error Recovery\n\n- If bumpversion fails, the tag won't exist. Fix the issue and re-run — bumpversion is idempotent as long as the tag doesn't already exist.\n- If you need to undo a release commit (before pushing): `git tag -d vX.Y.Z && git reset --soft HEAD~1`\n- Never amend a release commit that has been pushed.\n\n## Notes\n\n- When the tag is pushed, the release CI (`.github/workflows/release.yml`) automatically extracts the matching version section from `CHANGELOG.md` and uses it as the GitHub Release body. No manual copy-paste needed.\n- The release commit message is controlled by `.bumpversion.cfg` (`Bump version: X.Y.Z -> A.B.C`). Do not override it.\n- If you need to manually update the GitHub Release body after the fact: `gh release edit vX.Y.Z --notes-file <(sed -n '/## \\[X.Y.Z\\]/,/## \\[/p' CHANGELOG.md | head -n -1)`\n"
  },
  {
    "path": ".biomeignore",
    "content": "# Dependencies\nnode_modules\nbun.lockb\n\n# Build outputs\ndist\ntarget\n.tauri\n\n# Generated files\napp/src/lib/api\n\n# Config files (don't lint/format)\n*.config.js\n*.config.ts\n\n# Tailwind CSS files (contains @tailwind directives)\n**/index.css\n"
  },
  {
    "path": ".bumpversion.cfg",
    "content": "[bumpversion]\ncurrent_version = 0.3.1\ncommit = True\ntag = True\ntag_name = v{new_version}\ntag_message = Release v{new_version}\nmessage = Bump version: {current_version} → {new_version}\n\n[bumpversion:file:tauri/src-tauri/tauri.conf.json]\nsearch = \"version\": \"{current_version}\"\nreplace = \"version\": \"{new_version}\"\n\n[bumpversion:file:tauri/src-tauri/Cargo.toml]\nsearch = version = \"{current_version}\"\nreplace = version = \"{new_version}\"\n\n[bumpversion:file:package.json]\nsearch = \"version\": \"{current_version}\"\nreplace = \"version\": \"{new_version}\"\n\n[bumpversion:file:app/package.json]\nsearch = \"version\": \"{current_version}\"\nreplace = \"version\": \"{new_version}\"\n\n[bumpversion:file:tauri/package.json]\nsearch = \"version\": \"{current_version}\"\nreplace = \"version\": \"{new_version}\"\n\n[bumpversion:file:landing/package.json]\nsearch = \"version\": \"{current_version}\"\nreplace = \"version\": \"{new_version}\"\n\n[bumpversion:file:web/package.json]\nsearch = \"version\": \"{current_version}\"\nreplace = \"version\": \"{new_version}\"\n\n[bumpversion:file:backend/__init__.py]\nsearch = __version__ = \"{current_version}\"\nreplace = __version__ = \"{new_version}\"\n"
  },
  {
    "path": ".dockerignore",
    "content": "# Version control\n.git\n.github\n.gitignore\n\n# Desktop-only (not needed in web container)\ntauri/\nlanding/\ndocs/\nmlx-test/\nscripts/\n\n# Dependencies & build artifacts (rebuilt in Docker)\nnode_modules/\n__pycache__/\n*.pyc\n*.pyo\n*.egg-info/\ndist/\nbuild/\n*.spec\n\n# Data (will be bind-mounted)\ndata/\nbackend/data/\n\n# IDE & OS\n.vscode/\n.idea/\n*.swp\n*.swo\n.DS_Store\nThumbs.db\n\n# Config files not needed in container\nbiome.json\n.biomeignore\n.bumpversion.cfg\n.npmrc\nMakefile\nCHANGELOG.md\nCONTRIBUTING.md\nSECURITY.md\nLICENSE\nREADME.md\nbackend/README.md\n"
  },
  {
    "path": ".github/workflows/build-windows.yml",
    "content": "name: Build Windows\n\non:\n  workflow_dispatch:\n\njobs:\n  build-windows:\n    permissions:\n      contents: write\n    runs-on: windows-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n          cache: \"pip\"\n\n      - name: Install Python dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install pyinstaller\n          pip install -r backend/requirements.txt\n\n      - name: Build Python server\n        shell: bash\n        run: |\n          cd backend\n          python build_binary.py\n\n          PLATFORM=$(rustc --print host-tuple)\n          mkdir -p ../tauri/src-tauri/binaries\n          cp dist/voicebox-server.exe ../tauri/src-tauri/binaries/voicebox-server-${PLATFORM}.exe\n          echo \"Built voicebox-server-${PLATFORM}.exe\"\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n\n      - name: Install Rust stable\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Rust cache\n        uses: swatinem/rust-cache@v2\n        with:\n          workspaces: \"./tauri/src-tauri -> target\"\n\n      - name: Install dependencies\n        run: bun install\n\n      - uses: tauri-apps/tauri-action@v0\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          projectPath: tauri\n          tagName: v__VERSION__\n          releaseName: \"voicebox v__VERSION__ (test build)\"\n          releaseBody: \"Test build for audio export fix\"\n          releaseDraft: true\n          prerelease: true\n          args: \"\"\n          includeUpdaterJson: false\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - \"v*\"\n\njobs:\n  release:\n    permissions:\n      contents: write\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - platform: \"macos-latest\"\n            args: \"--target aarch64-apple-darwin\"\n            python-version: \"3.12\"\n            backend: \"mlx\"\n          - platform: \"macos-15-intel\"\n            args: \"--target x86_64-apple-darwin\"\n            python-version: \"3.12\"\n            backend: \"pytorch\"\n          - platform: \"windows-latest\"\n            args: \"\"\n            python-version: \"3.12\"\n            backend: \"pytorch\"\n\n    runs-on: ${{ matrix.platform }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install dependencies (ubuntu only)\n        if: contains(matrix.platform, 'ubuntu') || contains(matrix.platform, 'namespace')\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf llvm-dev libasound2-dev\n\n      - name: Install LLVM (macOS)\n        if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-15-intel'\n        run: |\n          brew install llvm@20\n          echo \"$(brew --prefix llvm@20)/bin\" >> $GITHUB_PATH\n          echo \"LLVM_CONFIG=$(brew --prefix llvm@20)/bin/llvm-config\" >> $GITHUB_ENV\n\n      - name: Setup Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n          cache: \"pip\"\n\n      - name: Install CPU-only PyTorch (Linux)\n        if: contains(matrix.platform, 'ubuntu') || contains(matrix.platform, 'namespace')\n        run: |\n          pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu\n\n      - name: Install Python dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install pyinstaller\n          pip install -r backend/requirements.txt\n          pip install --no-deps chatterbox-tts\n          pip install --no-deps hume-tada\n\n      - name: Install MLX dependencies (Apple Silicon only)\n        if: matrix.backend == 'mlx'\n        run: |\n          pip install -r backend/requirements-mlx.txt\n\n      - name: Build Python server (Linux/macOS)\n        if: matrix.platform != 'windows-latest'\n        run: |\n          chmod +x scripts/build-server.sh\n          ./scripts/build-server.sh\n\n      - name: Build Python server (Windows)\n        if: matrix.platform == 'windows-latest'\n        shell: bash\n        run: |\n          cd backend\n          python build_binary.py\n\n          # Get platform tuple\n          PLATFORM=$(rustc --print host-tuple)\n\n          # Create binaries directory\n          mkdir -p ../tauri/src-tauri/binaries\n\n          # Copy with platform suffix\n          cp dist/voicebox-server.exe ../tauri/src-tauri/binaries/voicebox-server-${PLATFORM}.exe\n          echo \"Built voicebox-server-${PLATFORM}.exe\"\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n\n      - name: Install Rust stable\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: ${{ (matrix.platform == 'macos-latest' && 'aarch64-apple-darwin') || (matrix.platform == 'macos-15-intel' && 'x86_64-apple-darwin') || '' }}\n\n      - name: Rust cache\n        uses: swatinem/rust-cache@v2\n        with:\n          workspaces: \"./tauri/src-tauri -> target\"\n\n      - name: Install dependencies\n        run: bun install\n\n      - name: Install Apple API key\n        if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-15-intel'\n        run: |\n          mkdir -p ~/.appstoreconnect/private_keys/\n          cd ~/.appstoreconnect/private_keys/\n          echo ${{ secrets.APPLE_API_KEY_BASE64 }} >> AuthKey_${{ secrets.APPLE_API_KEY }}.p8.base64\n          base64 --decode -i AuthKey_${{ secrets.APPLE_API_KEY }}.p8.base64 -o AuthKey_${{ secrets.APPLE_API_KEY }}.p8\n          rm AuthKey_${{ secrets.APPLE_API_KEY }}.p8.base64\n\n      - name: Install Codesigning Certificate\n        if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-15-intel'\n        uses: apple-actions/import-codesign-certs@v3\n        with:\n          p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }}\n          p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}\n\n      - name: Extract release notes from CHANGELOG.md\n        id: changelog\n        shell: bash\n        run: |\n          # Get the version from the tag (strip leading 'v')\n          VERSION=\"${GITHUB_REF_NAME#v}\"\n\n          # Extract the section for this version from CHANGELOG.md\n          # Matches from \"## [X.Y.Z]\" until the next \"## [\" heading\n          NOTES=$(sed -n \"/^## \\[${VERSION}\\]/,/^## \\[/{/^## \\[${VERSION}\\]/d;/^## \\[/d;p;}\" CHANGELOG.md)\n\n          # Fall back to a placeholder if the version isn't in the changelog\n          if [ -z \"$(echo \"$NOTES\" | tr -d '[:space:]')\" ]; then\n            NOTES=\"See the assets below to download and install this version.\"\n          fi\n\n          # Use multiline output syntax\n          {\n            echo \"notes<<CHANGELOG_EOF\"\n            echo \"$NOTES\"\n            echo \"CHANGELOG_EOF\"\n          } >> \"$GITHUB_OUTPUT\"\n\n      - uses: tauri-apps/tauri-action@v0.6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}\n          ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}\n          APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}\n          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}\n          APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}\n          APPLE_PROVIDER_SHORT_NAME: ${{ secrets.APPLE_PROVIDER_SHORT_NAME }}\n          APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}\n          APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}\n        with:\n          projectPath: tauri\n          tagName: v__VERSION__\n          releaseName: \"voicebox v__VERSION__\"\n          releaseBody: ${{ steps.changelog.outputs.notes }}\n          releaseDraft: true\n          prerelease: false\n          args: ${{ matrix.args }}\n          includeUpdaterJson: true\n\n  build-cuda-windows:\n    runs-on: windows-latest\n    permissions:\n      contents: write\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n          cache: \"pip\"\n\n      - name: Install Python dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install pyinstaller\n          pip install -r backend/requirements.txt\n          pip install --no-deps chatterbox-tts\n          pip install --no-deps hume-tada\n\n      - name: Install PyTorch with CUDA 12.8\n        run: |\n          pip install torch --index-url https://download.pytorch.org/whl/cu128 --force-reinstall --no-deps\n          pip install torchaudio --index-url https://download.pytorch.org/whl/cu128 --force-reinstall --no-deps\n\n      - name: Verify CUDA support in torch\n        run: |\n          python -c \"import torch; print(f'CUDA available in build: {torch.cuda.is_available()}'); print(f'CUDA version: {torch.version.cuda}')\"\n\n      - name: Build CUDA server binary (onedir)\n        shell: bash\n        working-directory: backend\n        run: python build_binary.py --cuda\n\n      - name: Package into server core + CUDA libs archives\n        shell: bash\n        run: |\n          python scripts/package_cuda.py \\\n            backend/dist/voicebox-server-cuda/ \\\n            --output release-assets/ \\\n            --cuda-libs-version cu128-v1 \\\n            --torch-compat \">=2.7.0,<2.11.0\"\n\n      - name: Upload archives to GitHub Release\n        if: startsWith(github.ref, 'refs/tags/')\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            release-assets/voicebox-server-cuda.tar.gz\n            release-assets/voicebox-server-cuda.tar.gz.sha256\n            release-assets/cuda-libs-cu128-v1.tar.gz\n            release-assets/cuda-libs-cu128-v1.tar.gz.sha256\n            release-assets/cuda-libs.json\n          draft: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Upload onedir as workflow artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: voicebox-server-cuda-windows\n          path: backend/dist/voicebox-server-cuda/\n          retention-days: 7\n"
  },
  {
    "path": ".gitignore",
    "content": "# Dependencies\nnode_modules/\nbun.lockb\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\nvenv/\nenv/\nENV/\n*.prompt\n# Build outputs\ndist/\nbuild/\n*.egg-info/\n*.egg\ntarget/\n*.app\n*.dmg\n*.exe\n*.msi\n*.deb\n*.AppImage\n\n# IDE\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n\n# OS\n.DS_Store\nThumbs.db\n\n# Data (user-generated)\ndata/\n!data/.gitkeep\n\n# Logs\n*.log\nlogs/\n\n# Environment\n.env\n.env.local\n\n# Generated files\napp/openapi.json\ntauri/src-tauri/binaries/*\ntauri/src-tauri/gen/Assets.car\ntauri/src-tauri/gen/voicebox.icns\ntauri/src-tauri/gen/partial.plist\n\n# PyInstaller\n*.spec\n\n# Windows artifacts\nnul\n\n# Temporary\ntmp/\ntemp/\n*.tmp\n"
  },
  {
    "path": ".npmrc",
    "content": "# Force bun usage\nengine-strict=true\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "<!-- This file is compiled automatically during the release workflow. -->\n<!-- Do not edit manually — your changes will be overwritten. -->\n<!-- To update the draft: ask the agent to use the draft-release-notes skill. -->\n<!-- To finalize a release: ask the agent to use the release-bump skill. -->\n\n# Changelog\n\n## [Unreleased]\n\n## [0.3.0] - 2026-03-17\n\nThis release rewrites the backend into a modular architecture, overhauls the settings UI into routed sub-pages, fixes audio player freezing, migrates documentation to Fumadocs, and ships a batch of bug fixes targeting the most-reported issues from the tracker.\n\nThe backend's 3,000-line monolith `main.py` has been decomposed into domain routers, a services layer, and a proper database package. A style guide and ruff configuration now enforce consistency. On the frontend, settings have been split into dedicated routed pages with server logs, a changelog viewer, and an about page. The audio player no longer freezes mid-playback, and model loading status is now visible in the UI. Seven user-reported bugs have been fixed, including server crashes during sample uploads, generation list staleness, cryptic error messages, and CUDA support for RTX 50-series GPUs.\n\n### Settings Overhaul ([#294](https://github.com/jamiepine/voicebox/pull/294))\n- Split settings into routed sub-tabs: General, Generation, GPU, Logs, Changelog, About\n- Added live server log viewer with auto-scroll\n- Added in-app changelog page that parses `CHANGELOG.md` at build time\n- Added About page with version info, license, and generation folder quick-open\n- Extracted reusable `SettingRow` component for consistent setting layouts\n\n### Audio Player Fix ([#293](https://github.com/jamiepine/voicebox/pull/293))\n- Fixed audio player freezing during playback\n- Improved playback UX with better state management and listener cleanup\n- Fixed restart race condition during regeneration\n- Added stable keys for audio element re-rendering\n- Improved accessibility across player controls\n\n### Backend Refactor ([#285](https://github.com/jamiepine/voicebox/pull/285))\n- Extracted all routes from `main.py` into 13 domain routers under `backend/routes/` — `main.py` dropped from ~3,100 lines to ~10\n- Moved CRUD and service modules into `backend/services/`, platform detection into `backend/utils/`\n- Split monolithic `database.py` into a `database/` package with separate `models`, `session`, `migrations`, and `seed` modules\n- Added `backend/STYLE_GUIDE.md` and `pyproject.toml` with ruff linting config\n- Removed dead code: unused `_get_cuda_dll_excludes`, stale `studio.py`, `example_usage.py`, old `Makefile`\n- Deduplicated shared logic across TTS backends into `backends/base.py`\n- Improved startup logging with version, platform, data directory, and database stats\n- Fixed startup database session leak — sessions now rollback and close in `finally` block\n- Isolated shutdown unload calls so one backend failure doesn't block the others\n- Handled null duration in `story_items` migration\n- Reject model migration when target is a subdirectory of source cache\n\n### Documentation Rewrite ([#288](https://github.com/jamiepine/voicebox/pull/288))\n- Migrated docs site from Mintlify to Fumadocs (Next.js-based)\n- Rewrote introduction and root page with content from README\n- Added \"Edit on GitHub\" links and last-updated timestamps on all pages\n- Generated OpenAPI spec and auto-generated API reference pages\n- Removed stale planning docs (`CUDA_BACKEND_SWAP`, `EXTERNAL_PROVIDERS`, `MLX_AUDIO`, `TTS_PROVIDER_ARCHITECTURE`, etc.)\n- Sidebar groups now expand by default; root redirects to `/docs`\n- Added OG image metadata and `/og` preview page\n\n### UI & Frontend\n- Added model loading status indicator and effects preset dropdown ([3187344](https://github.com/jamiepine/voicebox/commit/3187344))\n- Fixed take-label race condition during regeneration\n- Added accessible focus styling to select component\n- Softened select focus indicator opacity\n- Addressed 4 critical and 12 major issues from CodeRabbit review\n\n### Bug Fixes ([#295](https://github.com/jamiepine/voicebox/pull/295))\n- Fixed sample uploads crashing the server — audio decoding now runs in a thread pool instead of blocking the async event loop ([#278](https://github.com/jamiepine/voicebox/issues/278))\n- Fixed generation list not updating when a generation completes — switched to `refetchQueries` for reliable cache busting, added SSE error fallback, and page reset on completion ([#231](https://github.com/jamiepine/voicebox/issues/231))\n- Fixed error toasts showing `[object Object]` instead of the actual error message ([#290](https://github.com/jamiepine/voicebox/issues/290))\n- Added Whisper model selection (`base`, `small`, `medium`, `large`, `turbo`) and expanded language support to the `/transcribe` endpoint ([#233](https://github.com/jamiepine/voicebox/issues/233))\n- Upgraded CUDA backend build from cu121 to cu126 for RTX 50-series (Blackwell) GPU support ([#289](https://github.com/jamiepine/voicebox/issues/289))\n- Handled client disconnects in SSE and streaming endpoints to suppress `[Errno 32] Broken Pipe` errors ([#248](https://github.com/jamiepine/voicebox/issues/248))\n- Fixed Docker build failure from pip hash mismatch on Qwen3-TTS dependencies ([#286](https://github.com/jamiepine/voicebox/issues/286))\n- Added 50 MB upload size limit with chunked reads to prevent unbounded memory allocation on sample uploads\n- Eliminated redundant double audio decode in sample processing pipeline\n\n### Platform Fixes\n- Replaced `netstat` with `TcpStream` + PowerShell for Windows port detection ([#277](https://github.com/jamiepine/voicebox/pull/277))\n- Fixed Docker frontend build and cleaned up Docker docs\n- Fixed macOS download links to use `.dmg` instead of `.app.tar.gz`\n- Added dynamic download redirect routes to landing site\n\n### Release Tooling\n- Added `draft-release-notes` and `release-bump` agent skills\n- Wired CI release workflow to extract notes from `CHANGELOG.md` for GitHub Releases\n- Backfilled changelog with all historical releases\n\n## [0.2.3] - 2026-03-15\n\nThe \"it works in dev but not in prod\" release. This version fixes a series of PyInstaller bundling issues that prevented model downloading, loading, generation, and progress tracking from working in production builds.\n\n### Model Downloads Now Actually Work\n\nThe v0.2.1/v0.2.2 builds could not download or load models that weren't already cached from a dev install. This release fixes the entire chain:\n\n- **Chatterbox, Chatterbox Turbo, and LuxTTS** all download, load, and generate correctly in bundled builds\n- **Real-time download progress** — byte-level progress bars now work in production. The root cause: `huggingface_hub` silently disables tqdm progress bars based on logger level, which prevented our progress tracker from receiving byte updates. We now force-enable the internal counter regardless.\n- **Fixed Python 3.12.0 `code.replace()` bug** — the macOS build was on Python 3.12.0, which has a [known CPython bug](https://github.com/pyinstaller/pyinstaller/issues/7992) that corrupts bytecode when PyInstaller rewrites code objects. This caused `NameError: name 'obj' is not defined` crashes during scipy/torch imports. Upgraded to Python 3.12.13.\n\n### PyInstaller Fixes\n\n- Collect all `inflect` files — `typeguard`'s `@typechecked` decorator calls `inspect.getsource()` at import time, which needs `.py` source files, not just bytecode. Fixes LuxTTS \"could not get source code\" error.\n- Collect all `perth` files — bundles the pretrained watermark model (`hparams.yaml`, `.pth.tar`) needed by Chatterbox at runtime\n- Collect all `piper_phonemize` files — bundles `espeak-ng-data/` (phoneme tables, language dicts) needed by LuxTTS for text-to-phoneme conversion\n- Set `ESPEAK_DATA_PATH` in frozen builds so the espeak-ng C library finds the bundled data instead of looking at `/usr/share/espeak-ng-data/`\n- Collect all `linacodec` files — fixes `inspect.getsource` error in Vocos codec\n- Collect all `zipvoice` files — fixes source code lookup in LuxTTS voice cloning\n- Copy metadata for `requests`, `transformers`, `huggingface-hub`, `tokenizers`, `safetensors`, `tqdm` — fixes `importlib.metadata` lookups in frozen binary\n- Add hidden imports for `chatterbox`, `chatterbox_turbo`, `luxtts`, `zipvoice` backends\n- Add `multiprocessing.freeze_support()` to fix resource_tracker subprocess crash in frozen binary\n- `--noconsole` now only applied on Windows — macOS/Linux need stdout/stderr for Tauri sidecar log capture\n- Hardened `sys.stdout`/`sys.stderr` devnull redirect to test writability, not just `None` check\n\n### Updater\n\n- Fixed updater artifact generation with `v1Compatible` for `tauri-action` signature files\n- Updated `tauri-action` to v0.6 to fix updater JSON and `.sig` generation\n\n### Other Fixes\n\n- Full traceback logging on all backend model loading errors (was just `str(e)` before)\n\n## [0.2.2] - 2026-03-15\n\n- Fix Chatterbox model support in bundled builds\n- Fix LuxTTS/ZipVoice support in bundled builds\n- Auto-update CUDA binary when app version changes\n- CUDA download progress bar\n- Fix server process staying alive on macOS (SIGHUP handling, watchdog grace period)\n- Hide console window when running CUDA binary on Windows\n\n## [0.2.1] - 2026-03-15\n\nVoicebox v0.1.x was a single-engine voice cloning app built around Qwen3-TTS. v0.2.0 is a ground-up rethink: four TTS engines, 23 languages, paralinguistic emotion controls, a post-processing effects pipeline, unlimited generation length, an async generation queue, and support for every major GPU vendor. Plus Docker.\n\n### New TTS Engines\n\n#### Multi-Engine Architecture\n\nVoicebox now runs **four independent TTS engines** behind a thread-safe per-engine backend registry. Switch engines per-generation from a single dropdown — no restart required.\n\n| Engine                      | Languages | Size    | Key Strengths                                 |\n| --------------------------- | --------- | ------- | --------------------------------------------- |\n| **Qwen3-TTS 1.7B**          | 10        | ~3.5 GB | Highest quality, delivery instructions        |\n| **Qwen3-TTS 0.6B**          | 10        | ~1.2 GB | Lighter, faster variant                       |\n| **LuxTTS**                  | English   | ~300 MB | CPU-friendly, 48 kHz output, 150x realtime    |\n| **Chatterbox Multilingual** | 23        | ~3.2 GB | Broadest language coverage, zero-shot cloning |\n| **Chatterbox Turbo**        | English   | ~1.5 GB | 350M params, low latency, paralinguistic tags |\n\n#### Chatterbox Multilingual — 23 Languages ([#257](https://github.com/jamiepine/voicebox/pull/257))\n\nZero-shot voice cloning in Arabic, Chinese, Danish, Dutch, English, Finnish, French, German, Greek, Hebrew, Hindi, Italian, Japanese, Korean, Malay, Norwegian, Polish, Portuguese, Russian, Spanish, Swahili, Swedish, and Turkish.\n\n#### LuxTTS — Lightweight English TTS ([#254](https://github.com/jamiepine/voicebox/pull/254))\n\nA fast, CPU-friendly English engine. ~300 MB download, 48 kHz output, runs at 150x realtime on CPU.\n\n#### Chatterbox Turbo — Expressive English ([#258](https://github.com/jamiepine/voicebox/pull/258))\n\nA fast 350M-parameter English model with inline paralinguistic tags.\n\n#### Paralinguistic Tags Autocomplete ([#265](https://github.com/jamiepine/voicebox/pull/265))\n\nType `/` in the text input with Chatterbox Turbo selected to open an autocomplete for **9 expressive tags**: `[laugh]` `[chuckle]` `[gasp]` `[cough]` `[sigh]` `[groan]` `[sniff]` `[shush]` `[clear throat]`\n\n### Generation\n\n#### Unlimited Generation Length — Auto-Chunking ([#266](https://github.com/jamiepine/voicebox/pull/266))\n\nLong text is now automatically split at sentence boundaries, generated per-chunk, and crossfaded back together. Engine-agnostic.\n\n- Auto-chunking limit slider — 100–5,000 chars (default 800)\n- Crossfade slider — 0–200ms (default 50ms)\n- Max text length raised to 50,000 characters\n- Smart splitting respects abbreviations, CJK punctuation, and `[tags]`\n\n#### Asynchronous Generation Queue ([#269](https://github.com/jamiepine/voicebox/pull/269))\n\nGeneration is now fully non-blocking. Serial execution queue prevents GPU contention. Real-time SSE status streaming.\n\n#### Generation Versions\n\nEvery generation now supports multiple versions with provenance tracking — original, effects versions, takes, source tracking, version pinning in stories, and favorites.\n\n### Post-Processing Effects ([#271](https://github.com/jamiepine/voicebox/pull/271))\n\nA full audio effects system powered by Spotify's `pedalboard` library: Pitch Shift, Reverb, Delay, Chorus/Flanger, Compressor, Gain, High-Pass Filter, Low-Pass Filter. 4 built-in presets, custom presets, per-profile default effects, and live preview.\n\n### Platform Support\n\n- **Windows Support** ([#272](https://github.com/jamiepine/voicebox/pull/272)) — Full Windows support with CUDA GPU detection\n- **Linux** ([#262](https://github.com/jamiepine/voicebox/pull/262)) — AMD ROCm, NVIDIA GBM fix, WebKitGTK mic access (build from source)\n- **NVIDIA CUDA Backend Swap** ([#252](https://github.com/jamiepine/voicebox/pull/252)) — Download and swap in CUDA backend from within the app\n- **Intel Arc (XPU) and DirectML** — PyTorch backend supports Intel Arc and DirectML\n- **Docker + Web Deployment** ([#161](https://github.com/jamiepine/voicebox/pull/161)) — 3-stage build, non-root runtime, health checks\n- **Whisper Turbo** — Added `openai/whisper-large-v3-turbo` as a transcription model option\n\n### Model Management ([#268](https://github.com/jamiepine/voicebox/pull/268))\n\nPer-model unload, custom models directory, model folder migration, download cancel/clear UI ([#238](https://github.com/jamiepine/voicebox/pull/238)), restructured settings UI.\n\n### Security & Reliability\n\n- CORS hardening ([#88](https://github.com/jamiepine/voicebox/pull/88))\n- Network access toggle ([#133](https://github.com/jamiepine/voicebox/pull/133))\n- Offline crash fix ([#152](https://github.com/jamiepine/voicebox/pull/152))\n- Atomic audio saves ([#263](https://github.com/jamiepine/voicebox/pull/263))\n- Filesystem health endpoint\n- Chatterbox float64 dtype fix ([#264](https://github.com/jamiepine/voicebox/pull/264))\n\n### Accessibility ([#243](https://github.com/jamiepine/voicebox/pull/243))\n\nScreen reader support, keyboard navigation, state-aware `aria-label` attributes on all interactive controls.\n\n### UI Polish\n\n- Redesigned landing page ([#274](https://github.com/jamiepine/voicebox/pull/274))\n- Voices tab overhaul with inline inspector\n- Responsive layout improvements\n- Duplicate profile name validation ([#175](https://github.com/jamiepine/voicebox/pull/175))\n\n### Community Contributors\n\n[@haosenwang1018](https://github.com/haosenwang1018), [@Balneario-de-Cofrentes](https://github.com/Balneario-de-Cofrentes), [@ageofalgo](https://github.com/ageofalgo), [@mikeswann](https://github.com/mikeswann), [@rayl15](https://github.com/rayl15), [@mpecanha](https://github.com/mpecanha), [@ways2read](https://github.com/ways2read), [@ieguiguren](https://github.com/ieguiguren), [@Vaibhavee89](https://github.com/Vaibhavee89), [@pandego](https://github.com/pandego), [@luminest-llc](https://github.com/luminest-llc)\n\n## [0.1.13] - 2026-02-23\n\n### Stability and reliability\n\n- [#95](https://github.com/jamiepine/voicebox/pull/95) Fix: selecting 0.6B model still downloads and uses 1.7B\n- [#93](https://github.com/jamiepine/voicebox/pull/93) fix(mlx): bundle native libs and broaden error handling for Apple Silicon\n- [#79](https://github.com/jamiepine/voicebox/pull/79) fix: handle non-ASCII filenames in Content-Disposition headers\n- [#78](https://github.com/jamiepine/voicebox/pull/78) fix: guard getUserMedia call against undefined mediaDevices in non-secure contexts\n- [#77](https://github.com/jamiepine/voicebox/pull/77) fix: await for confirmation before deleting voices and channels\n- [#128](https://github.com/jamiepine/voicebox/pull/128) fix: resolve multiple issues (#96, #119, #111, #108, #121, #125, #127)\n- [#40](https://github.com/jamiepine/voicebox/pull/40) Fix: audio export path resolution\n\n### Build and packaging\n\n- [#122](https://github.com/jamiepine/voicebox/pull/122) fix(web): add @tailwindcss/vite plugin to web config\n- [#126](https://github.com/jamiepine/voicebox/pull/126) Create requirements.txt\n\n### UX and docs\n\n- [#44](https://github.com/jamiepine/voicebox/pull/44) Enhances floating generate box UX\n- [#57](https://github.com/jamiepine/voicebox/pull/57) chore: updates repo URL in README\n- [#146](https://github.com/jamiepine/voicebox/pull/146) Add Spacebot banner to landing page\n- [#1](https://github.com/jamiepine/voicebox/pull/1) Improvements\n\n## [0.1.12] - 2026-01-31\n\n### Model Download UX Overhaul\n\n- Real-time download progress tracking with accurate percentage and speed info\n- No more downloading notifications during generation even when its not downloading\n- Better error handling and status reporting throughout the download process\n\n### Other Improvements\n\n- Enhanced health check endpoint with GPU type information\n- Improved model caching verification\n- More reliable SSE progress updates\n- Actual update notifications — no need to manually check in settings anymore\n\n## [0.1.11] - 2026-01-30\n\n- Fixed transcriptions on MLX\n- Fixed model download progress (finally)\n\n## [0.1.10] - 2026-01-30\n\n### Faster generation on Apple Silicon\n\nMassive speed gains, from around 20s per generation to 2-3s. Added native MLX backend support for Apple Silicon, providing significantly faster TTS and STT generation on M-series macOS machines.\n\n- **MLX Backend** — New backend implementation optimized for Apple Silicon using MLX framework\n- **Dynamic Backend Selection** — Automatically detects platform and selects between MLX (macOS) and PyTorch (other platforms)\n- Refactored TTS and STT logic into modular backend implementations\n- Updated build process to include MLX-specific dependencies for macOS builds\n\n## [0.1.9] - 2026-01-30\n\n### Improved voice profile creation flow\n\n- Voice create drafts: No longer lose work if you close the modal\n- Fixed whisper only transcribing English or Chinese, now has support for all languages\n\n### Improved Stories editor\n\n- Added spacebar for play/pause\n- Timeline now auto-scrolls to follow playhead during playback\n- Fixed misalignment of the items with mouse when picking up\n- Fixed hitbox for selecting an item\n- Fixed playhead jumping forward when pressing play\n\n### Generation box improvements\n\n- Instruct mode no longer wipes prompt text\n- Improved UI cleanliness\n\n### Misc\n\n- Fixed \"Model downloading\" toast during generation when model is already downloaded\n\n## [0.1.8] - 2026-01-29\n\n### Model Download Timeout Issues\n\nFixed critical issue where model downloads would fail with \"Failed to fetch\" errors on Windows. Refactored download endpoints to return immediately and continue downloads in background.\n\n### Cross-Platform Cache Path Issues\n\nFixed hardcoded `~/.cache/huggingface/hub` paths that don't work on Windows. All cache paths now use `hf_constants.HF_HUB_CACHE` for proper cross-platform support.\n\n### Windows Process Management\n\n- Added `/shutdown` endpoint for graceful server shutdown on Windows\n- Added `gpu_type` field to health check response\n\n## [0.1.7] - 2026-01-29\n\n- Trim and split audio clips in Story Editor\n- Auto-activation of stories in Story Editor with visible playhead\n- Conditional auto-play support in AudioPlayer for better user control\n- Refactored audio loading across HistoryTable, SampleList, and generation forms\n- Audio now only auto-plays when explicitly intended, preventing unexpected playback\n\n## [0.1.6] - 2026-01-29\n\n### Introducing Stories\n\nA full voice editor for composing podcasts and generated conversations.\n\n- **Stories Editor** — Create multi-voice narratives, podcasts, or conversations with a timeline-based editor\n- Compose tracks with different voices\n- Edit and arrange audio segments inline\n- Build generated conversations with multiple participants\n- **Improved Voice Generation UI** — Auto-resizing input, default voice selection, better layout\n- **Track Editor Integration** — Inline track editing within story items\n\n## [0.1.5] - 2026-01-28\n\nFixed recording length limit at 0:29 to auto stop instead of passing the limit and getting an error, which would cause users to lose their recording.\n\n## [0.1.4] - 2026-01-28\n\n- Audio channel management system\n- Native audio playback handling in AudioPlayer component\n- Refactored ConnectionForm and Checkbox components\n- Improved layout consistency and responsiveness\n- Added safe area constants for better responsive design\n\n## [0.1.3] - 2026-01-27\n\n- Improved the generate textbox\n- Maybe fixed Windows autoupdate restarting entire computer\n\n## [0.1.2] - 2026-01-27\n\n### Audio Capture & Format Conversion\n\n- Added audio format conversion util\n- Enhanced system audio capture on macOS and Windows\n- Improved audio recording hooks\n- Added audio input entitlement for macOS\n- Added audio capture tests\n\n### Update System\n\n- Enhanced auto-updater functionality and update status display\n\n## [0.1.1] - 2026-01-27\n\n### Platform Support\n\n- **macOS Audio Capture** — Native audio capture support for sample creation\n- **Windows Audio Capture** — WASAPI implementation with improved thread safety\n- **Linux Support** — Temporarily removed builds due to runner disk space constraints\n\n### Audio Features\n\n- Play/pause for audio samples across all components\n- Three new sample components: Recording, System capture, Upload with drag-and-drop\n- Audio validation, error handling, and consistent cleanup\n\n### Voice Profile Management\n\n- Profile import with file size validation (100MB limit)\n- Enhanced profile form with new audio sample components\n- Drag-and-drop support for audio file uploads\n\n### Server Management\n\n- Changed default URL from `localhost:8000` to `127.0.0.1:17493`\n- Server reuse logic, \"keep server running\" preference, orphaned process handling\n\n### Build & Release\n\n- Added `.bumpversion.cfg` for automated version management\n- Enhanced icon generation script for multi-size Windows icons\n\n### Bug Fixes\n\n- Fixed date formatting for timezone-less date strings\n- Fixed getLatestRelease file filtering\n- Improved audio duration metadata on Windows\n\n## [0.1.0] - 2026-01-27\n\nThe first public release of Voicebox — an open-source voice synthesis studio powered by Qwen3-TTS.\n\n### Voice Cloning with Qwen3-TTS\n\n- Automatic model download from HuggingFace\n- Multiple model sizes (1.7B and 0.6B)\n- Voice prompt caching for instant regeneration\n- English and Chinese support\n\n### Voice Profile Management\n\n- Create profiles from audio files or record directly in the app\n- Multiple samples per profile for higher quality cloning\n- Import/Export profiles\n- Automatic transcription via Whisper\n\n### Speech Generation\n\n- Simple text-to-speech with profile selection\n- Seed control for reproducible generations\n- Long-form support up to 5,000 characters\n\n### Generation History\n\n- Full history with metadata\n- Search by text content\n- Inline playback and download\n\n### Flexible Deployment\n\n- Local mode with bundled backend\n- Remote mode for GPU servers on your network\n- One-click server setup\n\n### Desktop Experience\n\n- Built with Tauri v2 (Rust) — native performance, not Electron\n- Cross-platform: macOS and Windows\n- No Python installation required\n\n### Tech Stack\n\nTauri v2, React, TypeScript, Tailwind CSS, FastAPI, Qwen3-TTS, Whisper, SQLite\n\n[Unreleased]: https://github.com/jamiepine/voicebox/compare/v0.2.3...HEAD\n[0.2.3]: https://github.com/jamiepine/voicebox/compare/v0.2.2...v0.2.3\n[0.2.2]: https://github.com/jamiepine/voicebox/compare/v0.2.1...v0.2.2\n[0.2.1]: https://github.com/jamiepine/voicebox/compare/v0.1.13...v0.2.1\n[0.1.13]: https://github.com/jamiepine/voicebox/compare/v0.1.12...v0.1.13\n[0.1.12]: https://github.com/jamiepine/voicebox/compare/v0.1.11...v0.1.12\n[0.1.11]: https://github.com/jamiepine/voicebox/compare/v0.1.10...v0.1.11\n[0.1.10]: https://github.com/jamiepine/voicebox/compare/v0.1.9...v0.1.10\n[0.1.9]: https://github.com/jamiepine/voicebox/compare/v0.1.8...v0.1.9\n[0.1.8]: https://github.com/jamiepine/voicebox/compare/v0.1.7...v0.1.8\n[0.1.7]: https://github.com/jamiepine/voicebox/compare/v0.1.6...v0.1.7\n[0.1.6]: https://github.com/jamiepine/voicebox/compare/v0.1.5...v0.1.6\n[0.1.5]: https://github.com/jamiepine/voicebox/compare/v0.1.4...v0.1.5\n[0.1.4]: https://github.com/jamiepine/voicebox/compare/v0.1.3...v0.1.4\n[0.1.3]: https://github.com/jamiepine/voicebox/compare/v0.1.2...v0.1.3\n[0.1.2]: https://github.com/jamiepine/voicebox/compare/v0.1.1...v0.1.2\n[0.1.1]: https://github.com/jamiepine/voicebox/compare/v0.1.0...v0.1.1\n[0.1.0]: https://github.com/jamiepine/voicebox/releases/tag/v0.1.0\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Voicebox\n\nThank you for your interest in contributing to Voicebox! This document provides guidelines and instructions for contributing.\n\n## Code of Conduct\n\n- Be respectful and inclusive\n- Welcome newcomers and help them learn\n- Focus on constructive feedback\n- Respect different viewpoints and experiences\n\n## Getting Started\n\n### Prerequisites\n\n- **[Bun](https://bun.sh)** - Fast JavaScript runtime and package manager\n  ```bash\n  curl -fsSL https://bun.sh/install | bash\n  ```\n\n- **[Python 3.11+](https://python.org)** - For backend development\n  ```bash\n  python --version  # Should be 3.11 or higher\n  ```\n\n- **[Rust](https://rustup.rs)** - For Tauri desktop app (installed automatically by Tauri CLI)\n  ```bash\n  rustc --version  # Check if installed\n  ```\n- **[Tauri Prerequisites](https://v2.tauri.app/start/prerequisites)** - Tauri-specific system dependencies (varies by OS).\n\n- **Git** - Version control\n\n### Development Setup\n\nInstall [just](https://github.com/casey/just) (`brew install just`, `cargo install just`, or `winget install Casey.Just`), then:\n\n```bash\ngit clone https://github.com/YOUR_USERNAME/voicebox.git\ncd voicebox\n\njust setup   # creates venv, installs Python + JS deps\njust dev     # starts backend + desktop app\n```\n\n`just setup` handles everything automatically, including:\n- Creating a Python virtual environment\n- Installing Python dependencies (with CUDA PyTorch on Windows if an NVIDIA GPU is detected)\n- Installing MLX dependencies on Apple Silicon\n- Installing JavaScript dependencies\n\n`just dev` starts the backend and desktop app together. If a backend is already running (e.g. from `just dev-backend` in another terminal), it detects it and only starts the frontend.\n\nOther useful commands:\n\n```bash\njust dev-web       # backend + web app (no Tauri/Rust build)\njust dev-backend   # backend only\njust dev-frontend  # Tauri app only (backend must be running)\njust kill          # stop all dev processes\njust clean-all     # nuke everything and start fresh\njust --list        # see all available commands\n```\n\n> **Note:** In dev mode, the app connects to a manually-started Python server.\n> The bundled server binary is only used in production builds.\n\n#### Windows Notes\n\nThe justfile works natively on Windows via PowerShell. No WSL or Git Bash required. On Windows with an NVIDIA GPU, `just setup` automatically installs CUDA-enabled PyTorch for GPU acceleration.\n\n### Model Downloads\n\nModels are automatically downloaded from HuggingFace Hub on first use:\n- **Whisper** (transcription): Auto-downloads on first transcription\n- **Qwen3-TTS** (voice cloning): Auto-downloads on first generation (~2-4GB)\n\nFirst-time usage will be slower due to model downloads, but subsequent runs will use cached models.\n\n### Building\n\n**Build production app:**\n\n```bash\njust build        # Build CPU server binary + Tauri installer\n```\n\nOn Windows, to build with CUDA support for local testing:\n\n```bash\njust build-local  # Build CPU + CUDA server binaries + Tauri installer\n```\n\nThis builds the CPU sidecar (bundled with the app), the CUDA binary (placed in `%APPDATA%/com.voicebox.app/backends/` for runtime GPU switching), and the installable Tauri app.\n\nCreates platform-specific installers (`.dmg`, `.msi`, `.AppImage`) in `tauri/src-tauri/target/release/bundle/`.\n\n**Individual build targets:**\n\n```bash\njust build-server       # CPU server binary only\njust build-server-cuda  # CUDA server binary only (Windows)\njust build-tauri        # Tauri desktop app only\njust build-web          # Web app only\n```\n\n**Building with local Qwen3-TTS development version:**\n\nIf you're actively developing or modifying the Qwen3-TTS library, set the `QWEN_TTS_PATH` environment variable to point to your local clone:\n\n```bash\nexport QWEN_TTS_PATH=~/path/to/your/Qwen3-TTS\njust build-server\n```\n\nThis makes PyInstaller use your local qwen-tts version instead of the pip-installed package.\n\n### Generate OpenAPI Client\n\nAfter starting the backend server:\n```bash\n./scripts/generate-api.sh\n```\nThis downloads the OpenAPI schema and generates the TypeScript client in `app/src/lib/api/`\n\n### Convert Assets to Web Formats\n\nTo optimize images and videos for the web, run:\n```bash\nbun run convert:assets\n```\n\nThis script:\n- Converts PNG → WebP (better compression, same quality)\n- Converts MOV → WebM (VP9 codec, smaller file size)\n- Processes files in `landing/public/` and `docs/public/`\n- **Deletes original files** after successful conversion\n\n**Requirements:** Install `webp` and `ffmpeg`:\n```bash\nbrew install webp ffmpeg\n```\n\n> **Note:** Run this before committing new images or videos to keep the repository size small.\n\n## Development Workflow\n\n### 1. Create a Branch\n\n```bash\ngit checkout -b feature/your-feature-name\n# or\ngit checkout -b fix/your-bug-fix\n```\n\n### 2. Make Your Changes\n\n- Write clean, readable code\n- Follow existing code style\n- Add comments for complex logic\n- Update documentation as needed\n\n### 3. Test Your Changes\n\n- Test manually in the app\n- Ensure backend API endpoints work\n- Check for TypeScript/Python errors\n- Verify UI components render correctly\n\n### 4. Commit Your Changes\n\nWrite clear, descriptive commit messages:\n\n```bash\ngit commit -m \"Add feature: voice profile export\"\ngit commit -m \"Fix: audio playback stops after 30 seconds\"\n```\n\n### 5. Push and Create Pull Request\n\n```bash\ngit push origin feature/your-feature-name\n```\n\nThen create a pull request on GitHub with:\n- Clear description of changes\n- Screenshots (for UI changes)\n- Reference to related issues\n\n## Code Style\n\n### TypeScript/React\n\n- Use TypeScript strict mode\n- Follow React best practices\n- Use functional components with hooks\n- Prefer named exports\n- Format with Biome (runs automatically)\n\n```typescript\n// Good\nexport function ProfileCard({ profile }: { profile: Profile }) {\n  return <div>{profile.name}</div>;\n}\n\n// Avoid\nexport const ProfileCard = (props) => { ... }\n```\n\n### Python\n\n- Follow PEP 8 style guide\n- Use type hints\n- Use async/await for I/O operations\n- Format with Black (if configured)\n\n```python\n# Good\nasync def create_profile(name: str, language: str) -> Profile:\n    \"\"\"Create a new voice profile.\"\"\"\n    ...\n\n# Avoid\ndef create_profile(name, language):\n    ...\n```\n\n### Rust\n\n- Follow Rust conventions\n- Use meaningful variable names\n- Handle errors explicitly\n- Format with `rustfmt`\n\n## Project Structure\n\n```\nvoicebox/\n├── app/              # Shared React frontend\n│   └── src/\n│       ├── components/   # UI components\n│       ├── lib/          # Utilities and API client\n│       └── hooks/        # React hooks\n├── backend/          # Python FastAPI server\n│   ├── main.py       # API routes\n│   ├── tts.py        # Voice synthesis\n│   └── ...\n├── tauri/            # Desktop app wrapper\n│   └── src-tauri/    # Rust backend\n└── scripts/          # Build scripts\n```\n\n## Areas for Contribution\n\n### 🐛 Bug Fixes\n\n- Check existing issues for bugs to fix\n- Test your fix thoroughly\n- Add tests if possible\n\n### ✨ New Features\n\n- Check the roadmap in README.md\n- Discuss major features in an issue first\n- Keep features focused and well-scoped\n\n### 📚 Documentation\n\n- Improve README clarity\n- Add code comments\n- Write API documentation\n- Create tutorials or guides\n\n### 🎨 UI/UX Improvements\n\n- Improve accessibility\n- Enhance visual design\n- Optimize performance\n- Add animations/transitions\n\n### 🔧 Infrastructure\n\n- Improve build process\n- Add CI/CD improvements\n- Optimize bundle size\n- Add testing infrastructure\n\n## API Development\n\nWhen adding new API endpoints:\n\n1. **Add route in `backend/main.py`**\n2. **Create Pydantic models in `backend/models.py`**\n3. **Implement business logic in appropriate module**\n4. **Update OpenAPI schema** (automatic with FastAPI)\n5. **Regenerate TypeScript client:**\n   ```bash\n   bun run generate:api\n   ```\n6. **Update `backend/README.md`** with endpoint documentation\n\n## Testing\n\nCurrently, testing is primarily manual. When adding tests:\n\n- **Backend**: Use pytest for Python tests\n- **Frontend**: Use Vitest for React component tests\n- **E2E**: Use Playwright for end-to-end tests (future)\n\n## Pull Request Process\n\n1. **Update documentation** if needed\n2. **Ensure code follows style guidelines**\n3. **Test your changes thoroughly**\n4. **Update CHANGELOG.md** with your changes\n5. **Request review** from maintainers\n\n### PR Checklist\n\n- [ ] Code follows style guidelines\n- [ ] Documentation updated\n- [ ] Changes tested\n- [ ] No breaking changes (or documented)\n- [ ] CHANGELOG.md updated\n\n## Release Process\n\nReleases are managed by maintainers:\n\n1. **Bump version using bumpversion:**\n   ```bash\n   # Install bumpversion (if not already installed)\n   pip install bumpversion\n   \n   # Bump patch version (0.1.0 -> 0.1.1)\n   bumpversion patch\n   \n   # Or bump minor version (0.1.0 -> 0.2.0)\n   bumpversion minor\n   \n   # Or bump major version (0.1.0 -> 1.0.0)\n   bumpversion major\n   ```\n   \n   This automatically:\n   - Updates version numbers in all files (`tauri.conf.json`, `Cargo.toml`, all `package.json` files, `backend/main.py`)\n   - Creates a git commit with the version bump\n   - Creates a git tag (e.g., `v0.1.1`, `v0.2.0`)\n\n2. **Update CHANGELOG.md** with release notes\n\n3. **Push commits and tags:**\n   ```bash\n   git push\n   git push --tags\n   ```\n\n4. **GitHub Actions builds and releases** automatically when tags are pushed\n\n## Troubleshooting\n\nSee [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for common issues and solutions.\n\n**Quick fixes:**\n\n- **Backend won't start:** Check Python version (3.11+), ensure venv is activated, install dependencies\n- **Tauri build fails:** Ensure Rust is installed, clean build with `cd tauri/src-tauri && cargo clean`\n- **OpenAPI client generation fails:** Ensure backend is running, check `curl http://localhost:17493/openapi.json`\n\n## Questions?\n\n- Open an issue for bugs or feature requests\n- Check existing issues and discussions\n- Review the codebase to understand patterns\n- See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for common issues\n\n## Additional Resources\n\n- [README.md](README.md) - Project overview\n- [backend/README.md](backend/README.md) - API documentation\n- [docs/AUTOUPDATER_QUICKSTART.md](docs/AUTOUPDATER_QUICKSTART.md) - Auto-updater setup\n- [SECURITY.md](SECURITY.md) - Security policy\n- [CHANGELOG.md](CHANGELOG.md) - Version history\n\n## License\n\nBy contributing, you agree that your contributions will be licensed under the MIT License.\n\n---\n\nThank you for contributing to Voicebox! 🎉\n"
  },
  {
    "path": "Dockerfile",
    "content": "# ============================================================\n# Voicebox — Local TTS Server with Web UI (CPU)\n# 3-stage build: Frontend → Python deps → Runtime\n# ============================================================\n\n# === Stage 1: Build frontend ===\nFROM oven/bun:1 AS frontend\n\nWORKDIR /build\n\n# Copy workspace config and frontend source\nCOPY package.json bun.lock ./\nCOPY app/ ./app/\nCOPY web/ ./web/\n\n# Strip workspaces not needed for web build, and fix trailing comma\nRUN sed -i '/\"tauri\"/d; /\"landing\"/d' package.json && \\\n    sed -i -z 's/,\\n  ]/\\n  ]/' package.json\nRUN bun install --no-save\n# Build frontend (skip tsc — upstream has pre-existing type errors)\nRUN cd web && bunx --bun vite build\n\n\n# === Stage 2: Build Python dependencies ===\nFROM python:3.11-slim AS backend-builder\n\nWORKDIR /build\n\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    git \\\n    build-essential \\\n    && rm -rf /var/lib/apt/lists/*\n\nRUN pip install --no-cache-dir --upgrade pip\n\nCOPY backend/requirements.txt .\nRUN pip install --no-cache-dir --prefix=/install -r requirements.txt\nRUN pip install --no-cache-dir --prefix=/install --no-deps chatterbox-tts\nRUN pip install --no-cache-dir --prefix=/install --no-deps hume-tada\nRUN pip install --no-cache-dir --prefix=/install \\\n    git+https://github.com/QwenLM/Qwen3-TTS.git\n\n\n# === Stage 3: Runtime ===\nFROM python:3.11-slim\n\n# Create non-root user for security\nRUN groupadd -r voicebox && \\\n    useradd -r -g voicebox -m -s /bin/bash voicebox\n\nWORKDIR /app\n\n# Install only runtime system dependencies\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    ffmpeg \\\n    curl \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy installed Python packages from builder stage\nCOPY --from=backend-builder /install /usr/local\n\n# Copy backend application code\nCOPY --chown=voicebox:voicebox backend/ /app/backend/\n\n# Copy built frontend from frontend stage\nCOPY --from=frontend --chown=voicebox:voicebox /build/web/dist /app/frontend/\n\n# Create data directories owned by non-root user\nRUN mkdir -p /app/data/generations /app/data/profiles /app/data/cache \\\n    && chown -R voicebox:voicebox /app/data\n\n# Switch to non-root user\nUSER voicebox\n\n# Expose the API port\nEXPOSE 17493\n\n# Health check — auto-restart if the server hangs\nHEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=60s \\\n    CMD curl -f http://localhost:17493/health || exit 1\n\n# Start the FastAPI server\nCMD [\"uvicorn\", \"backend.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"17493\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2026 Voicebox Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\".github/assets/icon-dark.webp\" alt=\"Voicebox\" width=\"120\" height=\"120\" />\n</p>\n\n<h1 align=\"center\">Voicebox</h1>\n\n<p align=\"center\">\n  <strong>The open-source voice synthesis studio.</strong><br/>\n  Clone voices. Generate speech. Apply effects. Build voice-powered apps.<br/>\n  All running locally on your machine.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/jamiepine/voicebox/releases\">\n    <img src=\"https://img.shields.io/github/downloads/jamiepine/voicebox/total?style=flat&color=blue\" alt=\"Downloads\" />\n  </a>\n  <a href=\"https://github.com/jamiepine/voicebox/releases/latest\">\n    <img src=\"https://img.shields.io/github/v/release/jamiepine/voicebox?style=flat\" alt=\"Release\" />\n  </a>\n  <a href=\"https://github.com/jamiepine/voicebox/stargazers\">\n    <img src=\"https://img.shields.io/github/stars/jamiepine/voicebox?style=flat\" alt=\"Stars\" />\n  </a>\n  <a href=\"https://github.com/jamiepine/voicebox/blob/main/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/jamiepine/voicebox?style=flat\" alt=\"License\" />\n  </a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://voicebox.sh\">voicebox.sh</a> •\n  <a href=\"https://docs.voicebox.sh\">Docs</a> •\n  <a href=\"#download\">Download</a> •\n  <a href=\"#features\">Features</a> •\n  <a href=\"#api\">API</a>\n</p>\n\n<br/>\n\n<p align=\"center\">\n  <a href=\"https://voicebox.sh\">\n    <img src=\"landing/public/assets/app-screenshot-1.webp\" alt=\"Voicebox App Screenshot\" width=\"800\" />\n  </a>\n</p>\n\n<p align=\"center\">\n  <em>Click the image above to watch the demo video on <a href=\"https://voicebox.sh\">voicebox.sh</a></em>\n</p>\n\n<br/>\n\n<p align=\"center\">\n  <img src=\"landing/public/assets/app-screenshot-2.webp\" alt=\"Voicebox Screenshot 2\" width=\"800\" />\n</p>\n\n<p align=\"center\">\n  <img src=\"landing/public/assets/app-screenshot-3.webp\" alt=\"Voicebox Screenshot 3\" width=\"800\" />\n</p>\n\n<br/>\n\n## What is Voicebox?\n\nVoicebox is a **local-first voice cloning studio** — a free and open-source alternative to ElevenLabs. Clone voices from a few seconds of audio, generate speech in 23 languages across 5 TTS engines, apply post-processing effects, and compose multi-voice projects with a timeline editor.\n\n- **Complete privacy** — models and voice data stay on your machine\n- **5 TTS engines** — Qwen3-TTS, LuxTTS, Chatterbox Multilingual, Chatterbox Turbo, and HumeAI TADA\n- **23 languages** — from English to Arabic, Japanese, Hindi, Swahili, and more\n- **Post-processing effects** — pitch shift, reverb, delay, chorus, compression, and filters\n- **Expressive speech** — paralinguistic tags like `[laugh]`, `[sigh]`, `[gasp]` via Chatterbox Turbo\n- **Unlimited length** — auto-chunking with crossfade for scripts, articles, and chapters\n- **Stories editor** — multi-track timeline for conversations, podcasts, and narratives\n- **API-first** — REST API for integrating voice synthesis into your own projects\n- **Native performance** — built with Tauri (Rust), not Electron\n- **Runs everywhere** — macOS (MLX/Metal), Windows (CUDA), Linux, AMD ROCm, Intel Arc, Docker\n\n---\n\n## Download\n\n| Platform              | Download                                               |\n| --------------------- | ------------------------------------------------------ |\n| macOS (Apple Silicon) | [Download DMG](https://voicebox.sh/download/mac-arm)   |\n| macOS (Intel)         | [Download DMG](https://voicebox.sh/download/mac-intel) |\n| Windows               | [Download MSI](https://voicebox.sh/download/windows)   |\n| Docker                | `docker compose up`                                    |\n\n> **[View all binaries →](https://github.com/jamiepine/voicebox/releases/latest)**\n\n> **Linux** — Pre-built binaries are not yet available. See [voicebox.sh/linux-install](https://voicebox.sh/linux-install) for build-from-source instructions.\n\n---\n\n## Features\n\n### Multi-Engine Voice Cloning\n\nFive TTS engines with different strengths, switchable per-generation:\n\n| Engine                      | Languages | Strengths                                                                                                                                |\n| --------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- |\n| **Qwen3-TTS** (0.6B / 1.7B) | 10        | High-quality multilingual cloning, delivery instructions (\"speak slowly\", \"whisper\")                                                     |\n| **LuxTTS**                  | English   | Lightweight (~1GB VRAM), 48kHz output, 150x realtime on CPU                                                                              |\n| **Chatterbox Multilingual** | 23        | Broadest language coverage — Arabic, Danish, Finnish, Greek, Hebrew, Hindi, Malay, Norwegian, Polish, Swahili, Swedish, Turkish and more |\n| **Chatterbox Turbo**        | English   | Fast 350M model with paralinguistic emotion/sound tags                                                                                   |\n| **TADA** (1B / 3B)          | 10        | HumeAI speech-language model — 700s+ coherent audio, text-acoustic dual alignment                                                        |\n\n### Emotions & Paralinguistic Tags\n\nType `/` in the text input to insert expressive tags that the model synthesizes inline with speech (Chatterbox Turbo):\n\n`[laugh]` `[chuckle]` `[gasp]` `[cough]` `[sigh]` `[groan]` `[sniff]` `[shush]` `[clear throat]`\n\n### Post-Processing Effects\n\n8 audio effects powered by Spotify's `pedalboard` library. Apply after generation, preview in real time, build reusable presets.\n\n| Effect           | Description                                   |\n| ---------------- | --------------------------------------------- |\n| Pitch Shift      | Up or down by up to 12 semitones              |\n| Reverb           | Configurable room size, damping, wet/dry mix  |\n| Delay            | Echo with adjustable time, feedback, and mix  |\n| Chorus / Flanger | Modulated delay for metallic or lush textures |\n| Compressor       | Dynamic range compression                     |\n| Gain             | Volume adjustment (-40 to +40 dB)             |\n| High-Pass Filter | Remove low frequencies                        |\n| Low-Pass Filter  | Remove high frequencies                       |\n\nShips with 4 built-in presets (Robotic, Radio, Echo Chamber, Deep Voice) and supports custom presets. Effects can be assigned per-profile as defaults.\n\n### Unlimited Generation Length\n\nText is automatically split at sentence boundaries and each chunk is generated independently, then crossfaded together. Works with all engines.\n\n- Configurable auto-chunking limit (100–5,000 chars)\n- Crossfade slider (0–200ms) for smooth transitions\n- Max text length: 50,000 characters\n- Smart splitting respects abbreviations, CJK punctuation, and `[tags]`\n\n### Generation Versions\n\nEvery generation supports multiple versions with provenance tracking:\n\n- **Original** — clean TTS output, always preserved\n- **Effects versions** — apply different effects chains from any source version\n- **Takes** — regenerate with a new seed for variation\n- **Source tracking** — each version records its lineage\n- **Favorites** — star generations for quick access\n\n### Async Generation Queue\n\nGeneration is non-blocking. Submit and immediately start typing the next one.\n\n- Serial execution queue prevents GPU contention\n- Real-time SSE status streaming\n- Failed generations can be retried\n- Stale generations from crashes auto-recover on startup\n\n### Voice Profile Management\n\n- Create profiles from audio files or record directly in-app\n- Import/export profiles to share or back up\n- Multi-sample support for higher quality cloning\n- Per-profile default effects chains\n- Organize with descriptions and language tags\n\n### Stories Editor\n\nMulti-voice timeline editor for conversations, podcasts, and narratives.\n\n- Multi-track composition with drag-and-drop\n- Inline audio trimming and splitting\n- Auto-playback with synchronized playhead\n- Version pinning per track clip\n\n### Recording & Transcription\n\n- In-app recording with waveform visualization\n- System audio capture (macOS and Windows)\n- Automatic transcription powered by Whisper (including Whisper Turbo)\n- Export recordings in multiple formats\n\n### Model Management\n\n- Per-model unload to free GPU memory without deleting downloads\n- Custom models directory via `VOICEBOX_MODELS_DIR`\n- Model folder migration with progress tracking\n- Download cancel/clear UI\n\n### GPU Support\n\n| Platform                 | Backend        | Notes                                          |\n| ------------------------ | -------------- | ---------------------------------------------- |\n| macOS (Apple Silicon)    | MLX (Metal)    | 4-5x faster via Neural Engine                  |\n| Windows / Linux (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app |\n| Linux (AMD)              | PyTorch (ROCm) | Auto-configures HSA_OVERRIDE_GFX_VERSION       |\n| Windows (any GPU)        | DirectML       | Universal Windows GPU support                  |\n| Intel Arc                | IPEX/XPU       | Intel discrete GPU acceleration                |\n| Any                      | CPU            | Works everywhere, just slower                  |\n\n---\n\n## API\n\nVoicebox exposes a full REST API for integrating voice synthesis into your own apps.\n\n```bash\n# Generate speech\ncurl -X POST http://localhost:17493/generate \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"text\": \"Hello world\", \"profile_id\": \"abc123\", \"language\": \"en\"}'\n\n# List voice profiles\ncurl http://localhost:17493/profiles\n\n# Create a profile\ncurl -X POST http://localhost:17493/profiles \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"My Voice\", \"language\": \"en\"}'\n```\n\n**Use cases:** game dialogue, podcast production, accessibility tools, voice assistants, content automation.\n\nFull API documentation available at `http://localhost:17493/docs`.\n\n---\n\n## Tech Stack\n\n| Layer         | Technology                                        |\n| ------------- | ------------------------------------------------- |\n| Desktop App   | Tauri (Rust)                                      |\n| Frontend      | React, TypeScript, Tailwind CSS                   |\n| State         | Zustand, React Query                              |\n| Backend       | FastAPI (Python)                                  |\n| TTS Engines   | Qwen3-TTS, LuxTTS, Chatterbox, Chatterbox Turbo, TADA |\n| Effects       | Pedalboard (Spotify)                              |\n| Transcription | Whisper / Whisper Turbo (PyTorch or MLX)          |\n| Inference     | MLX (Apple Silicon) / PyTorch (CUDA/ROCm/XPU/CPU) |\n| Database      | SQLite                                            |\n| Audio         | WaveSurfer.js, librosa                            |\n\n---\n\n## Roadmap\n\n| Feature                 | Description                                    |\n| ----------------------- | ---------------------------------------------- |\n| **Real-time Streaming** | Stream audio as it generates, word by word     |\n| **Voice Design**        | Create new voices from text descriptions       |\n| **More Models**         | XTTS, Bark, and other open-source voice models  |\n| **Plugin Architecture** | Extend with custom models and effects          |\n| **Mobile Companion**    | Control Voicebox from your phone               |\n\n---\n\n## Development\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for detailed setup and contribution guidelines.\n\n### Quick Start\n\n```bash\ngit clone https://github.com/jamiepine/voicebox.git\ncd voicebox\n\njust setup   # creates Python venv, installs all deps\njust dev     # starts backend + desktop app\n```\n\nInstall [just](https://github.com/casey/just): `brew install just` or `cargo install just`. Run `just --list` to see all commands.\n\n**Prerequisites:** [Bun](https://bun.sh), [Rust](https://rustup.rs), [Python 3.11+](https://python.org), [Tauri Prerequisites](https://v2.tauri.app/start/prerequisites/), and [Xcode](https://developer.apple.com/xcode/) on macOS.\n\n### Building Locally\n\n```bash\njust build          # Build CPU server binary + Tauri app\njust build-local    # (Windows) Build CPU + CUDA server binaries + Tauri app\n```\n\n### Adding New Voice Models\n\nThe multi-engine architecture makes adding new TTS engines straightforward. A [step-by-step guide](docs/content/docs/developer/tts-engines.mdx) covers the full process: dependency research, backend protocol implementation, frontend wiring, and PyInstaller bundling.\n\nThe guide is optimized for AI coding agents. An [agent skill](.agents/skills/add-tts-engine/SKILL.md) can pick up a model name and handle the entire integration autonomously — you just test the build locally.\n\n### Project Structure\n\n```\nvoicebox/\n├── app/              # Shared React frontend\n├── tauri/            # Desktop app (Tauri + Rust)\n├── web/              # Web deployment\n├── backend/          # Python FastAPI server\n├── landing/          # Marketing website\n└── scripts/          # Build & release scripts\n```\n\n---\n\n## Contributing\n\nContributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n1. Fork the repo\n2. Create a feature branch\n3. Make your changes\n4. Submit a PR\n\n## Security\n\nFound a security vulnerability? Please report it responsibly. See [SECURITY.md](SECURITY.md) for details.\n\n---\n\n## License\n\nMIT License — see [LICENSE](LICENSE) for details.\n\n---\n\n<p align=\"center\">\n  <a href=\"https://voicebox.sh\">voicebox.sh</a>\n</p>\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWe release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating:\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 0.1.x   | :white_check_mark: |\n| < 0.1   | :x:                |\n\n## Reporting a Vulnerability\n\nIf you discover a security vulnerability, please report it responsibly:\n\n1. **Do not** open a public GitHub issue\n2. Email security details to: [security@voicebox.sh](mailto:security@voicebox.sh)\n3. Include:\n   - Description of the vulnerability\n   - Steps to reproduce\n   - Potential impact\n   - Suggested fix (if any)\n\nWe will:\n- Acknowledge receipt within 48 hours\n- Provide a timeline for addressing the issue\n- Keep you informed of progress\n- Credit you in the security advisory (if desired)\n\n## Security Best Practices\n\n### For Users\n\n- **Keep Voicebox updated** - Updates include security patches\n- **Verify downloads** - Only download from official releases\n- **Local processing** - Voice data stays on your machine\n- **Network security** - Use HTTPS when connecting to remote servers\n\n### For Developers\n\n- **Dependencies** - Keep all dependencies up to date\n- **Code review** - All PRs require review before merging\n- **Secrets** - Never commit API keys or signing keys\n- **Signing** - All releases are cryptographically signed\n\n## Known Security Considerations\n\n### Local Processing\n\nVoicebox processes all audio locally by default. Your voice data never leaves your machine unless you explicitly enable remote server mode.\n\n### Remote Server Mode\n\nWhen connecting to a remote server:\n- Ensure the server is on a trusted network\n- Use HTTPS for remote connections\n- Verify server identity before connecting\n\n### Auto-Updates\n\n- Updates are cryptographically signed\n- Signature verification happens before installation\n- Only HTTPS endpoints are allowed\n\n### Python Server\n\nThe embedded Python server:\n- Runs locally by default (localhost only)\n- Can be configured for remote access\n- Uses standard FastAPI security practices\n\n## Disclosure Timeline\n\n- **Day 0**: Vulnerability reported\n- **Day 1-2**: Initial assessment and acknowledgment\n- **Day 3-7**: Investigation and fix development\n- **Day 8-14**: Testing and release preparation\n- **Day 15+**: Public disclosure (if applicable)\n\nTimeline may vary based on severity and complexity.\n\n## Security Updates\n\nSecurity updates will be:\n- Released as patch versions (e.g., 0.1.1)\n- Documented in CHANGELOG.md\n- Announced via GitHub releases\n- Automatically delivered via auto-updater\n\n---\n\nThank you for helping keep Voicebox secure! 🔒\n"
  },
  {
    "path": "app/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/index.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/lib/hooks\"\n  }\n}\n"
  },
  {
    "path": "app/index.html",
    "content": "<!doctype html>\n<html lang=\"en\" class=\"dark\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>voicebox</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "app/package.json",
    "content": "{\n  \"name\": \"@voicebox/app\",\n  \"version\": \"0.3.1\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"lint\": \"biome lint src\",\n    \"lint:fix\": \"biome lint --write src\",\n    \"format\": \"biome format --write src\",\n    \"check\": \"biome check --write src\"\n  },\n  \"dependencies\": {\n    \"@dnd-kit/core\": \"^6.3.1\",\n    \"@dnd-kit/sortable\": \"^10.0.0\",\n    \"@dnd-kit/utilities\": \"^3.2.2\",\n    \"@hookform/resolvers\": \"^3.9.0\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.1\",\n    \"@radix-ui/react-avatar\": \"^1.1.0\",\n    \"@radix-ui/react-dialog\": \"^1.1.1\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.1\",\n    \"@radix-ui/react-label\": \"^2.1.0\",\n    \"@radix-ui/react-popover\": \"^1.1.1\",\n    \"@radix-ui/react-progress\": \"^1.1.0\",\n    \"@radix-ui/react-scroll-area\": \"^1.1.0\",\n    \"@radix-ui/react-select\": \"^2.1.1\",\n    \"@radix-ui/react-separator\": \"^1.1.0\",\n    \"@radix-ui/react-slider\": \"^1.3.6\",\n    \"@radix-ui/react-slot\": \"^1.1.0\",\n    \"@radix-ui/react-tabs\": \"^1.1.0\",\n    \"@radix-ui/react-toast\": \"^1.2.1\",\n    \"@tanstack/react-query\": \"^5.0.0\",\n    \"@tanstack/react-query-devtools\": \"^5.0.0\",\n    \"@tanstack/react-router\": \"^1.157.16\",\n    \"@tauri-apps/api\": \"^2.0.0\",\n    \"@tauri-apps/plugin-dialog\": \"^2.0.0\",\n    \"@tauri-apps/plugin-fs\": \"^2.0.0\",\n    \"@tauri-apps/plugin-process\": \"^2.3.1\",\n    \"@tauri-apps/plugin-updater\": \"^2.9.0\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"date-fns\": \"^3.6.0\",\n    \"framer-motion\": \"^12.29.0\",\n    \"lucide-react\": \"^0.454.0\",\n    \"motion\": \"^12.29.0\",\n    \"react\": \"^18.3.0\",\n    \"react-dom\": \"^18.3.0\",\n    \"react-hook-form\": \"^7.53.0\",\n    \"react-sound-visualizer\": \"^1.4.0\",\n    \"tailwind-merge\": \"^2.5.4\",\n    \"wavesurfer.js\": \"^7.0.0\",\n    \"zod\": \"^3.23.8\",\n    \"zustand\": \"^4.5.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/vite\": \"^4.1.18\",\n    \"@types/react\": \"^18.3.0\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@vitejs/plugin-react\": \"^4.3.0\",\n    \"tailwindcss\": \"^4.1.0\",\n    \"typescript\": \"^5.6.0\",\n    \"vite\": \"^5.4.0\"\n  }\n}\n"
  },
  {
    "path": "app/plugins/changelog.ts",
    "content": "import { readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport type { Plugin } from 'vite';\n\n/** Vite plugin that exposes CHANGELOG.md as `virtual:changelog`. */\nexport function changelogPlugin(repoRoot: string): Plugin {\n  const virtualId = 'virtual:changelog';\n  const resolvedId = '\\0' + virtualId;\n  const changelogPath = path.resolve(repoRoot, 'CHANGELOG.md');\n\n  return {\n    name: 'changelog',\n    resolveId(id) {\n      if (id === virtualId) return resolvedId;\n    },\n    load(id) {\n      if (id === resolvedId) {\n        const raw = readFileSync(changelogPath, 'utf-8');\n        return `export default ${JSON.stringify(raw)};`;\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "app/src/App.tsx",
    "content": "import { RouterProvider } from '@tanstack/react-router';\nimport { useEffect, useRef, useState } from 'react';\nimport voiceboxLogo from '@/assets/voicebox-logo.png';\nimport ShinyText from '@/components/ShinyText';\nimport { TitleBarDragRegion } from '@/components/TitleBarDragRegion';\nimport { useAutoUpdater } from '@/hooks/useAutoUpdater';\nimport { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';\nimport { cn } from '@/lib/utils/cn';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { router } from '@/router';\nimport { useLogStore } from '@/stores/logStore';\nimport { useServerStore } from '@/stores/serverStore';\n\nconst LOADING_MESSAGES = [\n  'Warming up tensors...',\n  'Calibrating synthesizer engine...',\n  'Initializing voice models...',\n  'Loading neural networks...',\n  'Preparing audio pipelines...',\n  'Optimizing waveform generators...',\n  'Tuning frequency analyzers...',\n  'Building voice embeddings...',\n  'Configuring text-to-speech cores...',\n  'Syncing audio buffers...',\n  'Establishing model connections...',\n  'Preprocessing training data...',\n  'Validating voice samples...',\n  'Compiling inference engines...',\n  'Mapping phoneme sequences...',\n  'Aligning prosody parameters...',\n  'Activating speech synthesis...',\n  'Fine-tuning acoustic models...',\n  'Preparing voice cloning matrices...',\n  'Initializing Qwen TTS framework...',\n];\n\nfunction App() {\n  const platform = usePlatform();\n  const [serverReady, setServerReady] = useState(false);\n  const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);\n  const serverStartingRef = useRef(false);\n\n  // Automatically check for app updates on startup and show toast notifications\n  useAutoUpdater({ checkOnMount: true, showToast: true });\n\n  // Sync stored setting to Rust on startup\n  useEffect(() => {\n    if (platform.metadata.isTauri) {\n      const keepRunning = useServerStore.getState().keepServerRunningOnClose;\n      platform.lifecycle.setKeepServerRunning(keepRunning).catch((error) => {\n        console.error('Failed to sync initial setting to Rust:', error);\n      });\n    }\n    // Empty dependency array - platform is stable from context, only run once\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.metadata.isTauri, platform.lifecycle]);\n\n  // Setup lifecycle callbacks\n  useEffect(() => {\n    platform.lifecycle.onServerReady = () => {\n      setServerReady(true);\n    };\n    // Empty dependency array - platform is stable from context, only run once\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.lifecycle]);\n\n  // Subscribe to server logs\n  useEffect(() => {\n    const unsubscribe = platform.lifecycle.subscribeToServerLogs((entry) => {\n      useLogStore.getState().addEntry(entry);\n    });\n    return unsubscribe;\n  }, [platform.lifecycle]);\n\n  // Setup window close handler and auto-start server when running in Tauri (production only)\n  useEffect(() => {\n    if (!platform.metadata.isTauri) {\n      setServerReady(true); // Web assumes server is running\n      return;\n    }\n\n    // Setup window close handler to check setting and stop server if needed\n    // This works in both dev and prod, but will only stop server if it was started by the app\n    platform.lifecycle.setupWindowCloseHandler().catch((error) => {\n      console.error('Failed to setup window close handler:', error);\n    });\n\n    // Only auto-start server in production mode\n    // In dev mode, user runs server separately\n    if (!import.meta.env?.PROD) {\n      console.log('Dev mode: Skipping auto-start of server (run it separately)');\n      setServerReady(true); // Mark as ready so UI doesn't show loading screen\n      // Mark that server was not started by app (so we don't try to stop it on close)\n      // @ts-expect-error - adding property to window\n      window.__voiceboxServerStartedByApp = false;\n      return;\n    }\n\n    // Auto-start server in production\n    if (serverStartingRef.current) {\n      return;\n    }\n\n    serverStartingRef.current = true;\n    const isRemote = useServerStore.getState().mode === 'remote';\n    const customModelsDir = useServerStore.getState().customModelsDir;\n    console.log(`Production mode: Starting bundled server... (remote: ${isRemote})`);\n\n    platform.lifecycle\n      .startServer(isRemote, customModelsDir)\n      .then((serverUrl) => {\n        console.log('Server is ready at:', serverUrl);\n        // Update the server URL in the store with the dynamically assigned port\n        useServerStore.getState().setServerUrl(serverUrl);\n        setServerReady(true);\n        // Mark that we started the server (so we know to stop it on close)\n        // @ts-expect-error - adding property to window\n        window.__voiceboxServerStartedByApp = true;\n      })\n      .catch((error) => {\n        console.error('Failed to auto-start server:', error);\n        serverStartingRef.current = false;\n        // @ts-expect-error - adding property to window\n        window.__voiceboxServerStartedByApp = false;\n      });\n\n    // Cleanup: stop server on actual unmount (not StrictMode remount)\n    // Note: Window close is handled separately in Tauri Rust code\n    return () => {\n      // Window close event handles server shutdown based on setting\n      serverStartingRef.current = false;\n    };\n    // Empty dependency array - platform is stable from context, only run once\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.metadata.isTauri, platform.lifecycle]);\n\n  // Cycle through loading messages every 3 seconds\n  useEffect(() => {\n    if (!platform.metadata.isTauri || serverReady) {\n      return;\n    }\n\n    const interval = setInterval(() => {\n      setLoadingMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length);\n    }, 3000);\n\n    return () => clearInterval(interval);\n  }, [serverReady, platform.metadata.isTauri]);\n\n  // Show loading screen while server is starting in Tauri\n  if (platform.metadata.isTauri && !serverReady) {\n    return (\n      <div\n        className={cn(\n          'min-h-screen bg-background flex items-center justify-center',\n          TOP_SAFE_AREA_PADDING,\n        )}\n      >\n        <TitleBarDragRegion />\n        <div className=\"text-center space-y-6\">\n          <div className=\"flex justify-center relative\">\n            <div className=\"absolute inset-0 flex items-center justify-center\">\n              <div className=\"w-48 h-48 rounded-full bg-accent/20 blur-3xl\" />\n            </div>\n            <img\n              src={voiceboxLogo}\n              alt=\"Voicebox\"\n              className=\"w-48 h-48 object-contain animate-fade-in-scale relative z-10\"\n            />\n          </div>\n          <div className=\"animate-fade-in-delayed\">\n            <ShinyText\n              text={LOADING_MESSAGES[loadingMessageIndex]}\n              className=\"text-lg font-medium text-muted-foreground\"\n              speed={2}\n              color=\"hsl(var(--muted-foreground))\"\n              shineColor=\"hsl(var(--foreground))\"\n            />\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return <RouterProvider router={router} />;\n}\n\nexport default App;\n"
  },
  {
    "path": "app/src/components/AppFrame/AppFrame.tsx",
    "content": "import { useRouterState } from '@tanstack/react-router';\nimport { TitleBarDragRegion } from '@/components/TitleBarDragRegion';\nimport { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer';\nimport { StoryTrackEditor } from '@/components/StoriesTab/StoryTrackEditor';\nimport { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';\nimport { cn } from '@/lib/utils/cn';\nimport { useStoryStore } from '@/stores/storyStore';\nimport { useStory } from '@/lib/hooks/useStories';\n\ninterface AppFrameProps {\n  children: React.ReactNode;\n}\n\nexport function AppFrame({ children }: AppFrameProps) {\n  const routerState = useRouterState();\n  const isStoriesRoute = routerState.location.pathname === '/stories';\n  \n  const selectedStoryId = useStoryStore((state) => state.selectedStoryId);\n  const { data: story } = useStory(selectedStoryId);\n  \n  // Show track editor when on stories route with a selected story that has items\n  const showTrackEditor = isStoriesRoute && selectedStoryId && story && story.items.length > 0;\n\n  return (\n    <div className={cn('h-screen bg-background flex flex-col overflow-hidden', TOP_SAFE_AREA_PADDING)}>\n      <TitleBarDragRegion />\n      {children}\n      {showTrackEditor ? (\n        <StoryTrackEditor storyId={story.id} items={story.items} />\n      ) : (\n        <AudioPlayer />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/AudioPlayer/AudioPlayer.tsx",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { Pause, Play, Repeat, Volume2, VolumeX, X } from 'lucide-react';\nimport { useEffect, useId, useMemo, useRef, useState } from 'react';\nimport WaveSurfer from 'wavesurfer.js';\nimport { Button } from '@/components/ui/button';\nimport { Slider } from '@/components/ui/slider';\nimport { apiClient } from '@/lib/api/client';\nimport { formatAudioDuration } from '@/lib/utils/audio';\nimport { debug } from '@/lib/utils/debug';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { usePlayerStore } from '@/stores/playerStore';\n\nexport function AudioPlayer() {\n  const platform = usePlatform();\n  const volumeLabelId = useId();\n  const {\n    audioUrl,\n    audioId,\n    profileId,\n    isPlaying,\n    currentTime,\n    duration,\n    volume,\n    isLooping,\n    shouldRestart,\n    setIsPlaying,\n    setCurrentTime,\n    setDuration,\n    setVolume,\n    toggleLoop,\n    clearRestartFlag,\n    reset,\n  } = usePlayerStore();\n\n  // Check if profile has assigned channels (for native audio routing)\n  const { data: profileChannels } = useQuery({\n    queryKey: ['profile-channels', profileId],\n    queryFn: () => {\n      if (!profileId) return { channel_ids: [] };\n      return apiClient.getProfileChannels(profileId);\n    },\n    enabled: !!profileId && platform.metadata.isTauri,\n  });\n\n  const { data: channels } = useQuery({\n    queryKey: ['channels'],\n    queryFn: () => apiClient.listChannels(),\n    enabled: !!profileChannels && profileChannels.channel_ids.length > 0,\n  });\n\n  // Determine if we should use native playback\n  const useNativePlayback = useMemo(() => {\n    if (!platform.metadata.isTauri || !profileChannels || !channels) {\n      return false;\n    }\n\n    const assignedChannels = channels.filter((ch) => profileChannels.channel_ids.includes(ch.id));\n\n    // Use native playback if any assigned channel has non-default devices\n    const shouldUseNative = assignedChannels.some(\n      (ch) => ch.device_ids.length > 0 && !ch.is_default,\n    );\n\n    return shouldUseNative;\n  }, [profileChannels, channels, platform.metadata.isTauri]);\n\n  const waveformRef = useRef<HTMLDivElement>(null);\n  const wavesurferRef = useRef<WaveSurfer | null>(null);\n  const loadingRef = useRef(false);\n  const previousAudioIdRef = useRef<string | null>(null);\n  const hasInitializedRef = useRef(false);\n  const isUsingNativePlaybackRef = useRef(false);\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [wsReady, setWsReady] = useState(false);\n\n  // Create WaveSurfer once when the player becomes visible (audioUrl is set).\n  // This instance is reused for all subsequent audio loads - never destroyed until unmount.\n  useEffect(() => {\n    if (!audioUrl) return;\n    if (wavesurferRef.current) return; // already created\n\n    const initWaveSurfer = () => {\n      const container = waveformRef.current;\n      if (!container) {\n        setTimeout(initWaveSurfer, 50);\n        return;\n      }\n\n      const rect = container.getBoundingClientRect();\n      const style = window.getComputedStyle(container);\n      const isVisible =\n        rect.width > 0 &&\n        rect.height > 0 &&\n        style.display !== 'none' &&\n        style.visibility !== 'hidden';\n\n      if (!isVisible) {\n        setTimeout(initWaveSurfer, 50);\n        return;\n      }\n\n      debug.log('Creating WaveSurfer instance', {\n        width: rect.width,\n        height: rect.height,\n      });\n\n      try {\n        const root = document.documentElement;\n        const getCSSVar = (varName: string) => {\n          const value = getComputedStyle(root).getPropertyValue(varName).trim();\n          return value ? `hsl(${value})` : '';\n        };\n\n        const wavesurfer = WaveSurfer.create({\n          container,\n          waveColor: getCSSVar('--muted'),\n          progressColor: getCSSVar('--accent'),\n          cursorColor: getCSSVar('--accent'),\n          cursorWidth: 3,\n          barWidth: 2,\n          barRadius: 2,\n          height: 80,\n          normalize: true,\n          interact: true,\n          dragToSeek: { debounceTime: 0 },\n          mediaControls: false,\n          backend: 'WebAudio',\n        });\n\n        // Wire up event handlers (these persist for the lifetime of the instance)\n        wavesurfer.on('timeupdate', (time) => {\n          const dur = usePlayerStore.getState().duration;\n          if (dur > 0 && time >= dur) {\n            setCurrentTime(dur);\n            const loop = usePlayerStore.getState().isLooping;\n            if (loop) {\n              wavesurfer.seekTo(0);\n              wavesurfer.play().catch((err) => debug.error('Loop play failed:', err));\n            } else {\n              wavesurfer.pause();\n              setIsPlaying(false);\n            }\n            return;\n          }\n          setCurrentTime(time);\n        });\n\n        wavesurfer.on('ready', () => {\n          const dur = wavesurfer.getDuration();\n          setDuration(dur);\n          loadingRef.current = false;\n          setIsLoading(false);\n          setError(null);\n          debug.log('Audio ready, duration:', dur);\n\n          wavesurfer.setVolume(usePlayerStore.getState().volume);\n          wavesurfer.setMuted(false);\n\n          // Auto-play if the flag is set (story mode advance or explicit play)\n          const shouldAutoPlayNow = usePlayerStore.getState().shouldAutoPlay;\n          if (shouldAutoPlayNow) {\n            usePlayerStore.getState().clearAutoPlayFlag();\n            wavesurfer.play().catch((err) => {\n              debug.error('Failed to autoplay:', err);\n            });\n          } else {\n            debug.log('Skipping auto-play - shouldAutoPlay is false');\n          }\n        });\n\n        wavesurfer.on('play', () => setIsPlaying(true));\n        wavesurfer.on('pause', () => {\n          setIsPlaying(false);\n          setCurrentTime(wavesurfer.getCurrentTime());\n        });\n\n        wavesurfer.on('seeking', (time) => setCurrentTime(time));\n\n        // Mute audio during drag-to-seek to prevent popping from the WebAudio\n        // backend's hard stop/start cycle on each seek. Unmute with a short\n        // fade-in when the drag ends.\n        const seekMedia = wavesurfer.getMediaElement() as any;\n        const seekGain: GainNode | null = seekMedia?.getGainNode?.() ?? null;\n        if (seekGain) {\n          const ctx = seekGain.context as AudioContext;\n          wavesurfer.on('dragstart', () => {\n            seekGain.gain.cancelScheduledValues(ctx.currentTime);\n            seekGain.gain.setTargetAtTime(0, ctx.currentTime, 0.002);\n          });\n          wavesurfer.on('dragend', () => {\n            seekGain.gain.cancelScheduledValues(ctx.currentTime);\n            seekGain.gain.setTargetAtTime(1, ctx.currentTime, 0.01);\n          });\n        }\n        wavesurfer.on('finish', () => {\n          const loop = usePlayerStore.getState().isLooping;\n          if (loop) {\n            wavesurfer.seekTo(0);\n            wavesurfer.play().catch((err) => debug.error('Loop play failed:', err));\n          } else {\n            setIsPlaying(false);\n            const onFinish = usePlayerStore.getState().onFinish;\n            if (onFinish) onFinish();\n          }\n        });\n\n        wavesurfer.on('error', (err) => {\n          debug.error('WaveSurfer error:', err);\n          setIsLoading(false);\n          setError(`Audio error: ${err instanceof Error ? err.message : String(err)}`);\n        });\n\n        wavesurfer.on('loading', (percent) => {\n          setIsLoading(true);\n          if (percent === 100) setIsLoading(false);\n        });\n\n        wavesurferRef.current = wavesurfer;\n        setWsReady(true);\n        debug.log('WaveSurfer created successfully');\n      } catch (err) {\n        debug.error('Failed to create WaveSurfer:', err);\n        setError(\n          `Failed to initialize waveform: ${err instanceof Error ? err.message : String(err)}`,\n        );\n      }\n    };\n\n    let rafId: number;\n    rafId = requestAnimationFrame(() => {\n      initWaveSurfer();\n    });\n\n    return () => {\n      cancelAnimationFrame(rafId);\n    };\n    // Only run on mount-like conditions. audioUrl is here so we create the instance\n    // when the player first appears, but we guard against re-creation above.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [audioUrl, setIsPlaying, setDuration, setCurrentTime]);\n\n  // Destroy WaveSurfer only on unmount\n  useEffect(() => {\n    return () => {\n      if (wavesurferRef.current) {\n        debug.log('Destroying WaveSurfer instance (unmount)');\n        try {\n          wavesurferRef.current.destroy();\n        } catch (err) {\n          debug.error('Error destroying WaveSurfer:', err);\n        }\n        wavesurferRef.current = null;\n        setWsReady(false);\n      }\n    };\n  }, []);\n\n  // Load audio when URL changes (reuses the existing WaveSurfer instance)\n  useEffect(() => {\n    const wavesurfer = wavesurferRef.current;\n    if (!wavesurfer || !wsReady) return;\n\n    if (!audioUrl) {\n      // No audio - pause and reset\n      wavesurfer.pause();\n      wavesurfer.seekTo(0);\n      loadingRef.current = false;\n      setIsLoading(false);\n      setDuration(0);\n      setCurrentTime(0);\n      setError(null);\n      isUsingNativePlaybackRef.current = false;\n      return;\n    }\n\n    // Reset native playback state\n    isUsingNativePlaybackRef.current = false;\n    wavesurfer.setMuted(false);\n    wavesurfer.setVolume(usePlayerStore.getState().volume);\n\n    // Stop current playback and reset position before loading new audio.\n    // With the WebAudio backend, pause() accumulates playedDuration internally.\n    // seekTo(0) resets it so the new track starts from the beginning.\n    debug.log('Loading new audio URL:', audioUrl);\n    try {\n      if (wavesurfer.isPlaying()) {\n        wavesurfer.pause();\n      }\n      wavesurfer.seekTo(0);\n    } catch (err) {\n      debug.error('Error resetting before load:', err);\n    }\n\n    loadingRef.current = true;\n    setIsLoading(true);\n    setError(null);\n    setCurrentTime(0);\n    setDuration(0);\n\n    wavesurfer\n      .load(audioUrl)\n      .then(() => {\n        debug.log('Audio loaded into WaveSurfer');\n        loadingRef.current = false;\n      })\n      .catch((err) => {\n        debug.error('Failed to load audio:', err);\n        loadingRef.current = false;\n        setIsLoading(false);\n        setError(`Failed to load audio: ${err instanceof Error ? err.message : String(err)}`);\n      });\n  }, [audioUrl, wsReady, setCurrentTime, setDuration]);\n\n  // Sync play/pause state (only when user clicks play/pause button, not auto-sync)\n  // This effect is kept for external state changes but should be minimal\n  useEffect(() => {\n    if (!wavesurferRef.current || duration === 0) return;\n\n    if (isPlaying && wavesurferRef.current.isPlaying() === false) {\n      wavesurferRef.current.play().catch((error) => {\n        debug.error('Failed to play:', error);\n        setIsPlaying(false);\n        setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);\n      });\n    } else if (!isPlaying && wavesurferRef.current.isPlaying()) {\n      wavesurferRef.current.pause();\n    }\n  }, [isPlaying, setIsPlaying, duration]);\n\n  // Sync volume\n  useEffect(() => {\n    if (wavesurferRef.current) {\n      wavesurferRef.current.setVolume(volume);\n    }\n  }, [volume]);\n\n  // Mark as initialized when audio is ready, reset when audioId changes\n  useEffect(() => {\n    if (duration > 0 && audioId) {\n      hasInitializedRef.current = true;\n    }\n    // Reset initialization flag when audioId changes to a new audio\n    if (audioId !== previousAudioIdRef.current && previousAudioIdRef.current !== null) {\n      hasInitializedRef.current = false;\n    }\n    if (audioId !== null) {\n      previousAudioIdRef.current = audioId;\n    }\n  }, [duration, audioId]);\n\n  // Handle restart flag - when history item is clicked again, restart from beginning\n  useEffect(() => {\n    const wavesurfer = wavesurferRef.current;\n    if (!wavesurfer || !shouldRestart || duration === 0) {\n      return;\n    }\n\n    debug.log('Restarting current audio from beginning');\n    wavesurfer.seekTo(0);\n    wavesurfer.play().catch((error) => {\n      debug.error('Failed to play after restart:', error);\n      setIsPlaying(false);\n      setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);\n    });\n\n    clearRestartFlag();\n  }, [shouldRestart, duration, setIsPlaying, clearRestartFlag]);\n\n  // Auto-play is handled exclusively in the WaveSurfer 'ready' event handler.\n  // A separate effect here would race with the ready event since the WebAudio\n  // backend needs to fully decode the audio before play() works correctly.\n\n  // Spacebar to play/pause (capture phase so it fires before focused elements)\n  useEffect(() => {\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.code !== 'Space') return;\n      // Ignore if user is typing in an input/textarea\n      const tag = (e.target as HTMLElement)?.tagName;\n      if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) {\n        return;\n      }\n      if (audioUrl && duration > 0 && wavesurferRef.current) {\n        e.preventDefault();\n        e.stopPropagation();\n        if (wavesurferRef.current.isPlaying()) {\n          wavesurferRef.current.pause();\n        } else {\n          wavesurferRef.current.play().catch((err) => debug.error('Spacebar play failed:', err));\n        }\n      }\n    };\n    document.addEventListener('keydown', onKeyDown, true);\n    return () => document.removeEventListener('keydown', onKeyDown, true);\n  }, [audioUrl, duration]);\n\n  const handlePlayPause = async () => {\n    // Standard WaveSurfer playback (works for both normal and native playback modes)\n    // When using native playback, WaveSurfer is muted but still controls visualization\n    if (!wavesurferRef.current) {\n      debug.error('WaveSurfer not initialized');\n      return;\n    }\n\n    // Check if audio is loaded\n    if (duration === 0 && !isLoading) {\n      debug.error('Audio not loaded yet');\n      setError('Audio not loaded. Please wait...');\n      return;\n    }\n\n    // If using native playback\n    if (useNativePlayback && audioUrl && profileChannels && channels) {\n      if (isPlaying) {\n        // Pause: stop native playback and pause WaveSurfer visualization\n        try {\n          platform.audio.stopPlayback();\n          debug.log('Stopped native audio playback');\n        } catch (error) {\n          debug.error('Failed to stop native playback:', error);\n        }\n        wavesurferRef.current.pause();\n        return;\n      }\n\n      // Play: trigger native playback\n      try {\n        // Stop any existing native playback first\n        try {\n          platform.audio.stopPlayback();\n        } catch (_error) {\n          // Ignore errors when stopping (might not be playing)\n          debug.log('No existing playback to stop');\n        }\n\n        // Collect all device IDs from assigned channels\n        const assignedChannels = channels.filter((ch) =>\n          profileChannels.channel_ids.includes(ch.id),\n        );\n        const deviceIds = assignedChannels.flatMap((ch) => ch.device_ids);\n\n        if (deviceIds.length > 0) {\n          // Fetch audio data\n          const response = await fetch(audioUrl);\n          const audioData = new Uint8Array(await response.arrayBuffer());\n\n          // Play via native audio\n          await platform.audio.playToDevices(audioData, deviceIds);\n\n          // Mark that we're using native playback\n          isUsingNativePlaybackRef.current = true;\n\n          // Mute WaveSurfer and start it for visualization\n          wavesurferRef.current.setVolume(0);\n          wavesurferRef.current.setMuted(true);\n\n          // Start WaveSurfer for visualization (muted)\n          wavesurferRef.current.play().catch((error) => {\n            debug.error('Failed to start WaveSurfer visualization:', error);\n            setIsPlaying(false);\n            setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);\n          });\n\n          return;\n        }\n      } catch (error) {\n        debug.error('Native playback failed, falling back to WaveSurfer:', error);\n        // Fall through to WaveSurfer playback\n        isUsingNativePlaybackRef.current = false;\n      }\n    }\n\n    // Standard WaveSurfer playback (or fallback from native playback failure)\n    if (wavesurferRef.current.isPlaying()) {\n      wavesurferRef.current.pause();\n    } else {\n      // Ensure WaveSurfer is not muted if not using native playback\n      if (!isUsingNativePlaybackRef.current) {\n        wavesurferRef.current.setMuted(false);\n        wavesurferRef.current.setVolume(volume);\n      }\n\n      wavesurferRef.current.play().catch((error) => {\n        debug.error('Failed to play:', error);\n        setIsPlaying(false);\n        setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);\n      });\n    }\n  };\n\n  const handleSeek = (value: number[]) => {\n    if (!wavesurferRef.current || duration === 0) return;\n    const progress = value[0] / 100;\n    wavesurferRef.current.seekTo(progress);\n  };\n\n  const handleVolumeChange = (value: number[]) => {\n    setVolume(value[0] / 100);\n  };\n\n  const handleClose = () => {\n    // Stop any native playback\n    if (isUsingNativePlaybackRef.current && platform.metadata.isTauri) {\n      try {\n        platform.audio.stopPlayback();\n      } catch (error) {\n        debug.error('Failed to stop native playback:', error);\n      }\n    }\n    // Stop WaveSurfer\n    if (wavesurferRef.current) {\n      wavesurferRef.current.pause();\n      wavesurferRef.current.seekTo(0);\n    }\n    // Reset player state\n    reset();\n  };\n\n  // Don't render if no audio\n  if (!audioUrl) {\n    return null;\n  }\n\n  return (\n    <div className=\"fixed bottom-0 left-0 right-0 border-t bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 z-50\">\n      <div className=\"container mx-auto px-4 py-3 max-w-7xl\">\n        <div className=\"flex items-center gap-4\">\n          {/* Play/Pause Button */}\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={handlePlayPause}\n            disabled={isLoading || duration === 0}\n            className={`shrink-0 -mt-2 ${isPlaying ? 'bg-accent text-accent-foreground' : ''}`}\n            title={duration === 0 && !isLoading ? 'Audio not loaded' : ''}\n            aria-label={\n              duration === 0 && !isLoading ? 'Audio not loaded' : isPlaying ? 'Pause' : 'Play'\n            }\n          >\n            {isPlaying ? (\n              <Pause className=\"h-5 w-5 fill-current\" />\n            ) : (\n              <Play className=\"h-5 w-5 fill-current\" />\n            )}\n          </Button>\n\n          {/* Waveform */}\n          <div className=\"flex-1 min-w-0 flex flex-col gap-1\">\n            <div ref={waveformRef} className=\"w-full min-h-[80px] select-none\" />\n            <Slider\n              value={duration > 0 ? [(currentTime / duration) * 100] : [0]}\n              onValueChange={handleSeek}\n              max={100}\n              step={0.1}\n              className=\"w-full\"\n              aria-label=\"Playback position\"\n              aria-valuetext={`${formatAudioDuration(currentTime)} of ${formatAudioDuration(duration)}`}\n            />\n\n            {error && <div className=\"text-xs text-destructive text-center py-2\">{error}</div>}\n          </div>\n\n          {/* Time Display */}\n          <div className=\"flex items-center gap-2 text-sm text-muted-foreground shrink-0 min-w-[100px]\">\n            <span className=\"font-mono\">{formatAudioDuration(currentTime)}</span>\n            <span>/</span>\n            <span className=\"font-mono\">{formatAudioDuration(duration)}</span>\n          </div>\n\n          {/* Loop Button */}\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={toggleLoop}\n            className={isLooping ? 'bg-accent text-accent-foreground' : ''}\n            title=\"Toggle loop\"\n            aria-label={isLooping ? 'Stop looping' : 'Loop'}\n          >\n            <Repeat className=\"h-4 w-4\" />\n          </Button>\n\n          {/* Volume Control */}\n          <div\n            className=\"flex items-center gap-2 shrink-0 w-[120px]\"\n            role=\"group\"\n            aria-label=\"Volume\"\n          >\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={() => setVolume(volume > 0 ? 0 : 1)}\n              className=\"h-8 w-8\"\n              aria-label={volume > 0 ? 'Mute' : 'Unmute'}\n            >\n              {volume > 0 ? <Volume2 className=\"h-4 w-4\" /> : <VolumeX className=\"h-4 w-4\" />}\n            </Button>\n            <span id={volumeLabelId} className=\"sr-only\">\n              Volume level, {Math.round(volume * 100)}%\n            </span>\n            <Slider\n              value={[volume * 100]}\n              onValueChange={handleVolumeChange}\n              max={100}\n              step={1}\n              className=\"flex-1\"\n              aria-labelledby={volumeLabelId}\n              aria-valuetext={`${Math.round(volume * 100)}%`}\n            />\n          </div>\n\n          {/* Close Button */}\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={handleClose}\n            className=\"shrink-0\"\n            title=\"Close player\"\n            aria-label=\"Close player\"\n          >\n            <X className=\"h-5 w-5\" />\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/AudioStudio/.gitkeep",
    "content": "# Audio studio timeline editing components\n"
  },
  {
    "path": "app/src/components/AudioTab/AudioTab.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { Check, CheckCircle2, Edit, Plus, Speaker, Trash2 } from 'lucide-react';\nimport { useState } from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { apiClient } from '@/lib/api/client';\nimport { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';\nimport { cn } from '@/lib/utils/cn';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { usePlayerStore } from '@/stores/playerStore';\n\ninterface AudioDevice {\n  id: string;\n  name: string;\n  is_default: boolean;\n}\n\nexport function AudioTab() {\n  const platform = usePlatform();\n  const [createDialogOpen, setCreateDialogOpen] = useState(false);\n  const [editingChannel, setEditingChannel] = useState<string | null>(null);\n  const [selectedChannelId, setSelectedChannelId] = useState<string | null>(null);\n  const queryClient = useQueryClient();\n  const audioUrl = usePlayerStore((state) => state.audioUrl);\n  const isPlayerVisible = !!audioUrl;\n\n  const { data: channels, isLoading: channelsLoading } = useQuery({\n    queryKey: ['channels'],\n    queryFn: () => apiClient.listChannels(),\n  });\n\n  const { data: devices, isLoading: devicesLoading } = useQuery({\n    queryKey: ['audio-devices'],\n    queryFn: async () => {\n      if (!platform.metadata.isTauri) {\n        return [];\n      }\n      try {\n        return await platform.audio.listOutputDevices();\n      } catch (error) {\n        console.error('Failed to list audio devices:', error);\n        return [];\n      }\n    },\n    enabled: platform.metadata.isTauri,\n  });\n\n  const { data: profiles } = useQuery({\n    queryKey: ['profiles'],\n    queryFn: () => apiClient.listProfiles(),\n  });\n\n  const createChannel = useMutation({\n    mutationFn: (data: { name: string; device_ids: string[] }) => apiClient.createChannel(data),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['channels'] });\n      setCreateDialogOpen(false);\n    },\n  });\n\n  const updateChannel = useMutation({\n    mutationFn: ({\n      channelId,\n      data,\n    }: {\n      channelId: string;\n      data: { name?: string; device_ids?: string[] };\n    }) => apiClient.updateChannel(channelId, data),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['channels'] });\n      queryClient.invalidateQueries({ queryKey: ['profile-channels'] });\n      setEditingChannel(null);\n    },\n  });\n\n  const deleteChannel = useMutation({\n    mutationFn: (channelId: string) => apiClient.deleteChannel(channelId),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['channels'] });\n      queryClient.invalidateQueries({ queryKey: ['profile-channels'] });\n    },\n  });\n\n  const { data: channelVoices } = useQuery({\n    queryKey: ['channel-voices', editingChannel],\n    queryFn: async () => {\n      if (!editingChannel) return { profile_ids: [] };\n      return apiClient.getChannelVoices(editingChannel);\n    },\n    enabled: !!editingChannel,\n  });\n\n  const setChannelVoices = useMutation({\n    mutationFn: ({ channelId, profileIds }: { channelId: string; profileIds: string[] }) =>\n      apiClient.setChannelVoices(channelId, profileIds),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['channel-voices'] });\n      queryClient.invalidateQueries({ queryKey: ['profile-channels'] });\n    },\n  });\n\n  if (channelsLoading || devicesLoading) {\n    return (\n      <div className=\"flex items-center justify-center h-full\">\n        <div className=\"text-muted-foreground\">Loading...</div>\n      </div>\n    );\n  }\n\n  const handleChannelDelete = async (e, channelId) => {\n    e.stopPropagation();\n    if (await confirm('Delete this channel?')) {\n      deleteChannel.mutate(channelId);\n    }\n  };\n\n  const allChannels = channels || [];\n  const allDevices = devices || [];\n  const selectedChannel = selectedChannelId\n    ? allChannels.find((c) => c.id === selectedChannelId)\n    : null;\n\n  return (\n    <div className=\"h-full flex flex-col\">\n      <div className=\"flex items-center justify-between mb-6 shrink-0\">\n        <h2 className=\"text-2xl font-bold\">Audio Channels</h2>\n        <Button onClick={() => setCreateDialogOpen(true)}>\n          <Plus className=\"h-4 w-4 mr-2\" />\n          New Channel\n        </Button>\n      </div>\n\n      <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6 h-full min-h-0\">\n        {/* Left Column - Channels */}\n        <div\n          className={cn(\n            'flex flex-col min-h-0 overflow-y-auto',\n            isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,\n          )}\n        >\n          {allChannels.length === 0 ? (\n            <div className=\"flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md\">\n              <Speaker className=\"h-12 w-12 text-muted-foreground mb-4\" />\n              <p className=\"text-muted-foreground mb-4\">\n                No audio channels yet. Create your first channel to route voices to specific\n                devices.\n              </p>\n              <Button onClick={() => setCreateDialogOpen(true)}>\n                <Plus className=\"h-4 w-4 mr-2\" />\n                Create Channel\n              </Button>\n            </div>\n          ) : (\n            <div className=\"space-y-3\">\n              {allChannels.map((channel) => {\n                const isSelected = selectedChannelId === channel.id;\n                return (\n                  <button\n                    key={channel.id}\n                    type=\"button\"\n                    className={cn(\n                      'group border rounded-lg p-4 transition-colors cursor-pointer text-left w-full',\n                      isSelected && 'ring-2 ring-primary bg-primary/5 border-primary',\n                    )}\n                    onClick={() => setSelectedChannelId(isSelected ? null : channel.id)}\n                  >\n                    <div className=\"flex items-start justify-between gap-4\">\n                      <div className=\"flex-1 min-w-0\">\n                        <div className=\"flex items-center gap-2 mb-3\">\n                          <div className=\"h-8 w-8 rounded-lg bg-muted flex items-center justify-center shrink-0\">\n                            <Speaker className=\"h-4 w-4 text-muted-foreground\" />\n                          </div>\n                          <div className=\"flex items-center gap-2 min-w-0\">\n                            <h3 className=\"font-semibold text-base truncate\">{channel.name}</h3>\n                          </div>\n                        </div>\n\n                        <div className=\"space-y-2.5 ml-10\">\n                          <div>\n                            <div className=\"text-xs font-medium text-muted-foreground mb-1\">\n                              Output Devices\n                            </div>\n                            <div className=\"flex flex-wrap gap-1.5\">\n                              {channel.device_ids.length > 0\n                                ? channel.device_ids.map((deviceId) => {\n                                    const device = allDevices.find((d) => d.id === deviceId);\n                                    return (\n                                      <Badge\n                                        key={deviceId}\n                                        variant=\"outline\"\n                                        className=\"text-xs font-normal\"\n                                      >\n                                        {device?.name || deviceId}\n                                      </Badge>\n                                    );\n                                  })\n                                : (() => {\n                                    const defaultDevice = allDevices.find((d) => d.is_default);\n                                    return defaultDevice ? (\n                                      <Badge variant=\"outline\" className=\"text-xs font-normal\">\n                                        {defaultDevice.name}\n                                      </Badge>\n                                    ) : null;\n                                  })()}\n                            </div>\n                          </div>\n\n                          <div>\n                            <div className=\"text-xs font-medium text-muted-foreground mb-1\">\n                              Assigned Voices\n                            </div>\n                            <ChannelVoicesList channelId={channel.id} />\n                          </div>\n                        </div>\n                      </div>\n\n                      {!channel.is_default && (\n                        <div className=\"flex gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity\">\n                          <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            className=\"h-8 w-8 p-0\"\n                            onClick={(e) => {\n                              e.stopPropagation();\n                              setEditingChannel(channel.id);\n                            }}\n                          >\n                            <Edit className=\"h-4 w-4\" />\n                          </Button>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            className=\"h-8 w-8 p-0\"\n                            onClick={(e) => handleChannelDelete(e, channel.id)}\n                          >\n                            <Trash2 className=\"h-4 w-4\" />\n                          </Button>\n                        </div>\n                      )}\n                    </div>\n                  </button>\n                );\n              })}\n            </div>\n          )}\n        </div>\n\n        {/* Right Column - Available Devices */}\n        <div\n          className={cn(\n            'flex flex-col min-h-0 overflow-y-auto',\n            isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,\n          )}\n        >\n          <div className=\"shrink-0 mb-4\">\n            <h3 className=\"text-lg font-semibold\">Available Devices</h3>\n            <p className=\"text-sm text-muted-foreground mt-1\">\n              {selectedChannelId\n                ? selectedChannel?.is_default\n                  ? 'Default channel uses system default device'\n                  : 'Click devices to add or remove them from the selected channel'\n                : 'Select a channel to assign devices'}\n            </p>\n          </div>\n          {allDevices.length > 0 ? (\n            <div className=\"space-y-2\">\n              {allDevices.map((device) => {\n                const isConnected =\n                  selectedChannelId &&\n                  selectedChannel &&\n                  (selectedChannel.device_ids.length === 0\n                    ? device.is_default\n                    : selectedChannel.device_ids.includes(device.id));\n                const canToggle =\n                  selectedChannelId && selectedChannel && !selectedChannel.is_default;\n\n                const handleDeviceClick = () => {\n                  if (!canToggle || !selectedChannel) return;\n\n                  const currentDeviceIds = selectedChannel.device_ids;\n                  const newDeviceIds = isConnected\n                    ? currentDeviceIds.filter((id) => id !== device.id)\n                    : [...currentDeviceIds, device.id];\n\n                  updateChannel.mutate({\n                    channelId: selectedChannelId,\n                    data: { device_ids: newDeviceIds },\n                  });\n                };\n\n                return (\n                  <button\n                    key={device.id}\n                    type=\"button\"\n                    onClick={handleDeviceClick}\n                    disabled={!canToggle}\n                    className={cn(\n                      'flex items-center gap-2 text-sm p-3 rounded-lg border transition-colors text-left w-full',\n                      isConnected\n                        ? 'bg-primary/10 border-primary ring-1 ring-primary/20'\n                        : 'hover:bg-muted/50',\n                      !canToggle && 'cursor-default opacity-60',\n                      canToggle && 'cursor-pointer',\n                    )}\n                  >\n                    {canToggle ? (\n                      <div\n                        className={cn(\n                          'h-4 w-4 rounded border-2 flex items-center justify-center shrink-0',\n                          isConnected ? 'bg-accent border-accent' : 'border-muted-foreground/30',\n                        )}\n                      >\n                        {isConnected && <Check className=\"h-3 w-3 text-accent-foreground\" />}\n                      </div>\n                    ) : device.is_default ? (\n                      <CheckCircle2 className=\"h-4 w-4 text-primary shrink-0\" />\n                    ) : null}\n                    <span className={cn('truncate flex-1', device.is_default && 'font-medium')}>\n                      {device.name}\n                    </span>\n                  </button>\n                );\n              })}\n            </div>\n          ) : (\n            <div className=\"flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md\">\n              <CheckCircle2 className=\"h-12 w-12 text-muted-foreground mb-4\" />\n              <p className=\"text-muted-foreground text-center\">\n                {platform.metadata.isTauri\n                  ? 'No audio devices found'\n                  : 'Audio device selection requires Tauri'}\n              </p>\n            </div>\n          )}\n        </div>\n      </div>\n\n      {/* Create Channel Dialog */}\n      <CreateChannelDialog\n        open={createDialogOpen}\n        onOpenChange={setCreateDialogOpen}\n        devices={devices || []}\n        onCreate={(name, deviceIds) => {\n          createChannel.mutate({ name, device_ids: deviceIds });\n        }}\n      />\n\n      {/* Edit Channel Dialog */}\n      {editingChannel &&\n        (() => {\n          const channel = channels?.find((c) => c.id === editingChannel);\n          return channel ? (\n            <EditChannelDialog\n              open={!!editingChannel}\n              onOpenChange={(open) => !open && setEditingChannel(null)}\n              channel={channel}\n              devices={devices || []}\n              profiles={profiles || []}\n              channelVoices={channelVoices?.profile_ids || []}\n              onUpdate={(name, deviceIds) => {\n                updateChannel.mutate({\n                  channelId: editingChannel,\n                  data: { name, device_ids: deviceIds },\n                });\n              }}\n              onSetVoices={(profileIds) => {\n                setChannelVoices.mutate({\n                  channelId: editingChannel,\n                  profileIds,\n                });\n              }}\n            />\n          ) : null;\n        })()}\n    </div>\n  );\n}\n\nfunction ChannelVoicesList({ channelId }: { channelId: string }) {\n  const { data: voices } = useQuery({\n    queryKey: ['channel-voices', channelId],\n    queryFn: () => apiClient.getChannelVoices(channelId),\n  });\n\n  const { data: profiles } = useQuery({\n    queryKey: ['profiles'],\n    queryFn: () => apiClient.listProfiles(),\n  });\n\n  const voiceNames =\n    voices?.profile_ids.map((id) => profiles?.find((p) => p.id === id)?.name).filter(Boolean) || [];\n\n  return (\n    <div className=\"flex flex-wrap gap-1.5\">\n      {voiceNames.length > 0 ? (\n        voiceNames.map((name) => (\n          <Badge key={name} variant=\"outline\" className=\"text-xs font-normal\">\n            {name}\n          </Badge>\n        ))\n      ) : (\n        <span className=\"text-sm text-muted-foreground\">No voices assigned</span>\n      )}\n    </div>\n  );\n}\n\ninterface CreateChannelDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  devices: AudioDevice[];\n  onCreate: (name: string, deviceIds: string[]) => void;\n}\n\nfunction CreateChannelDialog({ open, onOpenChange, devices, onCreate }: CreateChannelDialogProps) {\n  const [name, setName] = useState('');\n  const [selectedDevices, setSelectedDevices] = useState<string[]>([]);\n\n  const handleSubmit = () => {\n    if (name.trim()) {\n      onCreate(name.trim(), selectedDevices);\n      setName('');\n      setSelectedDevices([]);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Create Audio Channel</DialogTitle>\n          <DialogDescription>\n            Create a new audio channel (bus) to route voices to specific output devices.\n          </DialogDescription>\n        </DialogHeader>\n        <div className=\"space-y-4\">\n          <div>\n            <Label htmlFor=\"channel-name\">Channel Name</Label>\n            <Input\n              id=\"channel-name\"\n              value={name}\n              onChange={(e) => setName(e.target.value)}\n              placeholder=\"e.g., Virtual Cable, Broadcast\"\n            />\n          </div>\n          <div>\n            <Label>Output Devices</Label>\n            <Select\n              value={selectedDevices[0] || ''}\n              onValueChange={(value) => {\n                if (value && !selectedDevices.includes(value)) {\n                  setSelectedDevices([...selectedDevices, value]);\n                }\n              }}\n            >\n              <SelectTrigger>\n                <SelectValue placeholder=\"Select device\" />\n              </SelectTrigger>\n              <SelectContent>\n                {devices.map((device) => (\n                  <SelectItem key={device.id} value={device.id}>\n                    {device.name} {device.is_default && '(default)'}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n            {selectedDevices.length > 0 && (\n              <div className=\"mt-2 space-y-1\">\n                {selectedDevices.map((deviceId) => {\n                  const device = devices.find((d) => d.id === deviceId);\n                  return (\n                    <div\n                      key={deviceId}\n                      className=\"flex items-center justify-between text-sm bg-muted p-2 rounded\"\n                    >\n                      <span>{device?.name || deviceId}</span>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() =>\n                          setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))\n                        }\n                      >\n                        <Trash2 className=\"h-3 w-3\" />\n                      </Button>\n                    </div>\n                  );\n                })}\n              </div>\n            )}\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n            Cancel\n          </Button>\n          <Button onClick={handleSubmit} disabled={!name.trim()}>\n            Create\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\ninterface EditChannelDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  channel: {\n    id: string;\n    name: string;\n    device_ids: string[];\n  };\n  devices: AudioDevice[];\n  profiles: Array<{ id: string; name: string }>;\n  channelVoices: string[];\n  onUpdate: (name: string, deviceIds: string[]) => void;\n  onSetVoices: (profileIds: string[]) => void;\n}\n\nfunction EditChannelDialog({\n  open,\n  onOpenChange,\n  channel,\n  devices,\n  profiles,\n  channelVoices,\n  onUpdate,\n  onSetVoices,\n}: EditChannelDialogProps) {\n  const [name, setName] = useState(channel.name);\n  const [selectedDevices, setSelectedDevices] = useState<string[]>(channel.device_ids);\n  const [selectedVoices, setSelectedVoices] = useState<string[]>(channelVoices);\n\n  const handleSubmit = () => {\n    if (name.trim()) {\n      onUpdate(name.trim(), selectedDevices);\n      onSetVoices(selectedVoices);\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-w-2xl\">\n        <DialogHeader>\n          <DialogTitle>Edit Channel</DialogTitle>\n          <DialogDescription>Update channel settings and voice assignments.</DialogDescription>\n        </DialogHeader>\n        <div className=\"space-y-4\">\n          <div>\n            <Label htmlFor=\"edit-channel-name\">Channel Name</Label>\n            <Input id=\"edit-channel-name\" value={name} onChange={(e) => setName(e.target.value)} />\n          </div>\n          <div>\n            <Label>Output Devices</Label>\n            <Select\n              value=\"\"\n              onValueChange={(value) => {\n                if (value && !selectedDevices.includes(value)) {\n                  setSelectedDevices([...selectedDevices, value]);\n                }\n              }}\n            >\n              <SelectTrigger>\n                <SelectValue placeholder=\"Add device\" />\n              </SelectTrigger>\n              <SelectContent>\n                {devices.map((device) => (\n                  <SelectItem key={device.id} value={device.id}>\n                    {device.name} {device.is_default && '(default)'}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n            {selectedDevices.length > 0 && (\n              <div className=\"mt-2 space-y-1\">\n                {selectedDevices.map((deviceId) => {\n                  const device = devices.find((d) => d.id === deviceId);\n                  return (\n                    <div\n                      key={deviceId}\n                      className=\"flex items-center justify-between text-sm bg-muted p-2 rounded\"\n                    >\n                      <span>{device?.name || deviceId}</span>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() =>\n                          setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))\n                        }\n                      >\n                        <Trash2 className=\"h-3 w-3\" />\n                      </Button>\n                    </div>\n                  );\n                })}\n              </div>\n            )}\n          </div>\n          <div>\n            <Label>Assigned Voices</Label>\n            <Select\n              value=\"\"\n              onValueChange={(value) => {\n                if (value && !selectedVoices.includes(value)) {\n                  setSelectedVoices([...selectedVoices, value]);\n                }\n              }}\n            >\n              <SelectTrigger>\n                <SelectValue placeholder=\"Add voice\" />\n              </SelectTrigger>\n              <SelectContent>\n                {profiles.map((profile) => (\n                  <SelectItem key={profile.id} value={profile.id}>\n                    {profile.name}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n            {selectedVoices.length > 0 && (\n              <div className=\"mt-2 space-y-1\">\n                {selectedVoices.map((profileId) => {\n                  const profile = profiles.find((p) => p.id === profileId);\n                  return (\n                    <div\n                      key={profileId}\n                      className=\"flex items-center justify-between text-sm bg-muted p-2 rounded\"\n                    >\n                      <span>{profile?.name || profileId}</span>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={() =>\n                          setSelectedVoices(selectedVoices.filter((id) => id !== profileId))\n                        }\n                      >\n                        <Trash2 className=\"h-3 w-3\" />\n                      </Button>\n                    </div>\n                  );\n                })}\n              </div>\n            )}\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n            Cancel\n          </Button>\n          <Button onClick={handleSubmit} disabled={!name.trim()}>\n            Save\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/src/components/Effects/EffectsChainEditor.tsx",
    "content": "import {\n  closestCenter,\n  DndContext,\n  type DragEndEvent,\n  KeyboardSensor,\n  PointerSensor,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core';\nimport {\n  arrayMove,\n  SortableContext,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from '@dnd-kit/sortable';\nimport { CSS } from '@dnd-kit/utilities';\nimport { useQuery } from '@tanstack/react-query';\nimport { ChevronDown, ChevronRight, GripVertical, Plus, Power, Trash2 } from 'lucide-react';\nimport { useCallback, useMemo, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Label } from '@/components/ui/label';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { Slider } from '@/components/ui/slider';\nimport { apiClient } from '@/lib/api/client';\nimport type { AvailableEffect, EffectConfig, EffectPresetResponse } from '@/lib/api/types';\nimport { cn } from '@/lib/utils/cn';\n\n// Each effect in the chain gets a stable ID for dnd-kit\ninterface EffectWithId extends EffectConfig {\n  _id: string;\n}\n\nlet nextId = 0;\nfunction makeId() {\n  return `fx-${++nextId}`;\n}\n\ninterface EffectsChainEditorProps {\n  value: EffectConfig[];\n  onChange: (chain: EffectConfig[]) => void;\n  compact?: boolean;\n  showPresets?: boolean;\n}\n\nexport function EffectsChainEditor({\n  value,\n  onChange,\n  compact = false,\n  showPresets = true,\n}: EffectsChainEditorProps) {\n  const [expandedId, setExpandedId] = useState<string | null>(null);\n\n  // Maintain stable IDs for each effect across renders.\n  // We use a ref to map value items to IDs, rebuilding when length changes.\n  const idsRef = useRef<string[]>([]);\n  const items: EffectWithId[] = useMemo(() => {\n    // Grow ID array if effects were added\n    while (idsRef.current.length < value.length) {\n      idsRef.current.push(makeId());\n    }\n    // Shrink if effects were removed\n    if (idsRef.current.length > value.length) {\n      idsRef.current = idsRef.current.slice(0, value.length);\n    }\n    return value.map((e, i) => ({ ...e, _id: idsRef.current[i] }));\n  }, [value]);\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n  );\n\n  const { data: availableEffects } = useQuery({\n    queryKey: ['available-effects'],\n    queryFn: () => apiClient.getAvailableEffects(),\n    staleTime: Infinity,\n  });\n\n  const { data: presets } = useQuery({\n    queryKey: ['effect-presets'],\n    queryFn: () => apiClient.listEffectPresets(),\n    staleTime: 30_000,\n  });\n\n  const effectsMap = useMemo(() => {\n    const m = new Map<string, AvailableEffect>();\n    if (availableEffects) {\n      for (const e of availableEffects.effects) {\n        m.set(e.type, e);\n      }\n    }\n    return m;\n  }, [availableEffects]);\n\n  function addEffect(type: string) {\n    const def = effectsMap.get(type);\n    if (!def) return;\n    const params: Record<string, number> = {};\n    for (const [key, p] of Object.entries(def.params)) {\n      params[key] = p.default;\n    }\n    const newEffect: EffectConfig = { type, enabled: true, params };\n    const newId = makeId();\n    idsRef.current = [...idsRef.current, newId];\n    onChange([...value, newEffect]);\n    setExpandedId(newId);\n  }\n\n  const removeEffect = useCallback(\n    (index: number) => {\n      const removedId = idsRef.current[index];\n      idsRef.current = idsRef.current.filter((_, i) => i !== index);\n      onChange(value.filter((_, i) => i !== index));\n      if (expandedId === removedId) setExpandedId(null);\n    },\n    [value, onChange, expandedId],\n  );\n\n  const toggleEnabled = useCallback(\n    (index: number) => {\n      onChange(value.map((e, i) => (i === index ? { ...e, enabled: !e.enabled } : e)));\n    },\n    [value, onChange],\n  );\n\n  const updateParam = useCallback(\n    (index: number, paramName: string, paramValue: number) => {\n      onChange(\n        value.map((e, i) =>\n          i === index ? { ...e, params: { ...e.params, [paramName]: paramValue } } : e,\n        ),\n      );\n    },\n    [value, onChange],\n  );\n\n  function loadPreset(preset: EffectPresetResponse) {\n    idsRef.current = preset.effects_chain.map(() => makeId());\n    onChange(preset.effects_chain);\n    setExpandedId(null);\n  }\n\n  function clearAll() {\n    idsRef.current = [];\n    onChange([]);\n    setExpandedId(null);\n  }\n\n  function handleDragEnd(event: DragEndEvent) {\n    const { active, over } = event;\n    if (!over || active.id === over.id) return;\n\n    const oldIndex = idsRef.current.indexOf(active.id as string);\n    const newIndex = idsRef.current.indexOf(over.id as string);\n    if (oldIndex === -1 || newIndex === -1) return;\n\n    idsRef.current = arrayMove(idsRef.current, oldIndex, newIndex);\n    onChange(arrayMove([...value], oldIndex, newIndex));\n  }\n\n  return (\n    <div className={cn('space-y-2', compact && 'text-sm')}>\n      {/* Preset selector row */}\n      {showPresets && (\n        <div className=\"flex items-center gap-2\">\n          <Select\n            onValueChange={(id) => {\n              const preset = presets?.find((p) => p.id === id);\n              if (preset) loadPreset(preset);\n            }}\n          >\n            <SelectTrigger className=\"h-8 flex-1 text-xs focus:ring-0 focus:ring-offset-0\">\n              <SelectValue placeholder=\"Load preset...\" />\n            </SelectTrigger>\n            <SelectContent>\n              {presets?.map((p) => (\n                <SelectItem key={p.id} value={p.id}>\n                  {p.name}\n                  {p.description && (\n                    <span className=\"ml-1 text-muted-foreground\">- {p.description}</span>\n                  )}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n\n          {value.length > 0 && (\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              className=\"h-8 px-2 text-xs text-muted-foreground\"\n              onClick={clearAll}\n            >\n              Clear\n            </Button>\n          )}\n        </div>\n      )}\n\n      {/* Sortable effects chain */}\n      <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n        <SortableContext items={items.map((i) => i._id)} strategy={verticalListSortingStrategy}>\n          {items.map((effect, index) => (\n            <SortableEffectItem\n              key={effect._id}\n              id={effect._id}\n              effect={effect}\n              index={index}\n              effectDef={effectsMap.get(effect.type)}\n              isExpanded={expandedId === effect._id}\n              onToggleExpand={() => setExpandedId(expandedId === effect._id ? null : effect._id)}\n              onRemove={() => removeEffect(index)}\n              onToggleEnabled={() => toggleEnabled(index)}\n              onUpdateParam={(paramName, paramValue) => updateParam(index, paramName, paramValue)}\n            />\n          ))}\n        </SortableContext>\n      </DndContext>\n\n      {/* Add effect */}\n      {availableEffects && (\n        <Select onValueChange={addEffect}>\n          <SelectTrigger className=\"h-8 border-dashed text-xs text-muted-foreground focus:ring-0 focus:ring-offset-0\">\n            <Plus className=\"mr-1 h-3.5 w-3.5\" />\n            <SelectValue placeholder=\"Add effect...\" />\n          </SelectTrigger>\n          <SelectContent>\n            {availableEffects.effects.map((e) => (\n              <SelectItem key={e.type} value={e.type}>\n                {e.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      )}\n    </div>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// Sortable effect item\n// ---------------------------------------------------------------------------\n\ninterface SortableEffectItemProps {\n  id: string;\n  effect: EffectConfig;\n  index: number;\n  effectDef?: AvailableEffect;\n  isExpanded: boolean;\n  onToggleExpand: () => void;\n  onRemove: () => void;\n  onToggleEnabled: () => void;\n  onUpdateParam: (paramName: string, paramValue: number) => void;\n}\n\nfunction SortableEffectItem({\n  id,\n  effect,\n  effectDef,\n  isExpanded,\n  onToggleExpand,\n  onRemove,\n  onToggleEnabled,\n  onUpdateParam,\n}: SortableEffectItemProps) {\n  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n    id,\n  });\n\n  const style = {\n    transform: CSS.Transform.toString(transform),\n    transition,\n    zIndex: isDragging ? 10 : undefined,\n  };\n\n  const label = effectDef?.label ?? effect.type;\n\n  return (\n    <div\n      ref={setNodeRef}\n      style={style}\n      className={cn(\n        'rounded-md border',\n        effect.enabled ? 'border-border bg-card' : 'border-border/50 bg-muted/30',\n        isDragging && 'opacity-80 shadow-lg',\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex items-center gap-1 px-2 py-1.5\">\n        <button\n          type=\"button\"\n          className=\"p-0.5 text-muted-foreground hover:text-foreground\"\n          onClick={onToggleExpand}\n        >\n          {isExpanded ? (\n            <ChevronDown className=\"h-3.5 w-3.5\" />\n          ) : (\n            <ChevronRight className=\"h-3.5 w-3.5\" />\n          )}\n        </button>\n\n        <button\n          type=\"button\"\n          className=\"p-0.5 text-muted-foreground/50 hover:text-muted-foreground cursor-grab active:cursor-grabbing touch-none\"\n          {...attributes}\n          {...listeners}\n        >\n          <GripVertical className=\"h-3.5 w-3.5\" />\n        </button>\n\n        <span\n          className={cn('flex-1 text-xs font-medium', !effect.enabled && 'text-muted-foreground')}\n        >\n          {label}\n        </span>\n\n        <button\n          type=\"button\"\n          className={cn(\n            'p-0.5 transition-colors',\n            effect.enabled ? 'text-primary' : 'text-muted-foreground hover:text-foreground',\n          )}\n          onClick={onToggleEnabled}\n          title={effect.enabled ? 'Disable' : 'Enable'}\n        >\n          <Power className=\"h-3.5 w-3.5\" />\n        </button>\n\n        <button\n          type=\"button\"\n          className=\"p-0.5 text-muted-foreground hover:text-destructive\"\n          onClick={onRemove}\n          title=\"Remove\"\n        >\n          <Trash2 className=\"h-3.5 w-3.5\" />\n        </button>\n      </div>\n\n      {/* Params */}\n      {isExpanded && effectDef && (\n        <div className=\"space-y-3 border-t px-3 py-2.5\">\n          {Object.entries(effectDef.params).map(([paramName, paramDef]) => {\n            const currentValue = effect.params[paramName] ?? paramDef.default;\n            return (\n              <div key={paramName} className=\"space-y-1\">\n                <div className=\"flex items-center justify-between\">\n                  <Label className=\"text-[11px] text-muted-foreground\">\n                    {paramDef.description}\n                  </Label>\n                  <span className=\"text-[11px] font-mono tabular-nums text-foreground\">\n                    {currentValue.toFixed(\n                      paramDef.step < 1 ? Math.max(1, -Math.floor(Math.log10(paramDef.step))) : 0,\n                    )}\n                  </span>\n                </div>\n                <Slider\n                  min={paramDef.min}\n                  max={paramDef.max}\n                  step={paramDef.step}\n                  value={[currentValue]}\n                  onValueChange={([v]) => onUpdateParam(paramName, v)}\n                />\n              </div>\n            );\n          })}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/Effects/GenerationPicker.tsx",
    "content": "import { ChevronDown, Search } from 'lucide-react';\nimport { useMemo, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport type { HistoryResponse } from '@/lib/api/types';\nimport { useHistory } from '@/lib/hooks/useHistory';\nimport { cn } from '@/lib/utils/cn';\n\ninterface GenerationPickerProps {\n  selectedId: string | null;\n  onSelect: (generation: HistoryResponse) => void;\n  className?: string;\n}\n\nexport function GenerationPicker({ selectedId, onSelect, className }: GenerationPickerProps) {\n  const [open, setOpen] = useState(false);\n  const [searchQuery, setSearchQuery] = useState('');\n\n  const { data: historyData } = useHistory({ limit: 50 });\n\n  const completedGenerations = useMemo(() => {\n    if (!historyData?.items) return [];\n    return historyData.items.filter((gen) => gen.status === 'completed');\n  }, [historyData]);\n\n  const filtered = useMemo(() => {\n    if (!searchQuery) return completedGenerations;\n    const q = searchQuery.toLowerCase();\n    return completedGenerations.filter(\n      (gen) => gen.text.toLowerCase().includes(q) || gen.profile_name.toLowerCase().includes(q),\n    );\n  }, [completedGenerations, searchQuery]);\n\n  const selectedGeneration = completedGenerations.find((g) => g.id === selectedId);\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          className={cn('h-8 justify-between gap-2 text-xs font-normal', className)}\n        >\n          {selectedGeneration ? (\n            <span className=\"truncate\">\n              <span className=\"font-medium\">{selectedGeneration.profile_name}</span>\n              <span className=\"text-muted-foreground ml-1.5\">\n                {selectedGeneration.text.length > 30\n                  ? `${selectedGeneration.text.substring(0, 30)}...`\n                  : selectedGeneration.text}\n              </span>\n            </span>\n          ) : (\n            <span className=\"text-muted-foreground\">Select a generation...</span>\n          )}\n          <ChevronDown className=\"h-3.5 w-3.5 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-80 p-0\" align=\"start\">\n        <div className=\"p-2 border-b\">\n          <div className=\"relative\">\n            <Search className=\"absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground\" />\n            <Input\n              placeholder=\"Search by voice or text...\"\n              value={searchQuery}\n              onChange={(e) => setSearchQuery(e.target.value)}\n              className=\"h-8 pl-7 text-xs\"\n            />\n          </div>\n        </div>\n        <div className=\"max-h-60 overflow-y-auto\">\n          {filtered.length === 0 ? (\n            <div className=\"p-4 text-center text-xs text-muted-foreground\">\n              No generations found\n            </div>\n          ) : (\n            filtered.map((gen) => (\n              <button\n                key={gen.id}\n                type=\"button\"\n                className={cn(\n                  'w-full text-left px-3 py-2 hover:bg-muted/50 transition-colors border-b border-border/30 last:border-0',\n                  gen.id === selectedId && 'bg-accent/10',\n                )}\n                onClick={() => {\n                  onSelect(gen);\n                  setOpen(false);\n                  setSearchQuery('');\n                }}\n              >\n                <div className=\"font-medium text-sm\">{gen.profile_name}</div>\n                <div className=\"text-xs text-muted-foreground truncate\">\n                  {gen.text.length > 60 ? `${gen.text.substring(0, 60)}...` : gen.text}\n                </div>\n              </button>\n            ))\n          )}\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "app/src/components/EffectsTab/EffectsDetail.tsx",
    "content": "import { useQuery, useQueryClient } from '@tanstack/react-query';\nimport { Loader2, Play, Save, Trash2, Wand2 } from 'lucide-react';\nimport { useEffect, useRef, useState } from 'react';\n\nimport { EffectsChainEditor } from '@/components/Effects/EffectsChainEditor';\nimport { GenerationPicker } from '@/components/Effects/GenerationPicker';\nimport { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Separator } from '@/components/ui/separator';\nimport { Textarea } from '@/components/ui/textarea';\nimport { useToast } from '@/components/ui/use-toast';\nimport { apiClient } from '@/lib/api/client';\nimport type { HistoryResponse } from '@/lib/api/types';\nimport { useHistory } from '@/lib/hooks/useHistory';\nimport { useEffectsStore } from '@/stores/effectsStore';\nimport { usePlayerStore } from '@/stores/playerStore';\n\nexport function EffectsDetail() {\n  const selectedPresetId = useEffectsStore((s) => s.selectedPresetId);\n  const isCreatingNew = useEffectsStore((s) => s.isCreatingNew);\n  const workingChain = useEffectsStore((s) => s.workingChain);\n  const setWorkingChain = useEffectsStore((s) => s.setWorkingChain);\n  const setSelectedPresetId = useEffectsStore((s) => s.setSelectedPresetId);\n  const setIsCreatingNew = useEffectsStore((s) => s.setIsCreatingNew);\n\n  const [name, setName] = useState('');\n  const [description, setDescription] = useState('');\n  const [saving, setSaving] = useState(false);\n  const [deleting, setDeleting] = useState(false);\n\n  // \"Save as Custom\" dialog state\n  const [saveAsDialogOpen, setSaveAsDialogOpen] = useState(false);\n  const [saveAsName, setSaveAsName] = useState('');\n  const [saveAsDescription, setSaveAsDescription] = useState('');\n\n  // Preview state\n  const [previewGenId, setPreviewGenId] = useState<string | null>(null);\n  const [previewLoading, setPreviewLoading] = useState(false);\n  const blobUrlRef = useRef<string | null>(null);\n  const setAudioWithAutoPlay = usePlayerStore((s) => s.setAudioWithAutoPlay);\n\n  const { toast } = useToast();\n  const queryClient = useQueryClient();\n\n  // Auto-select the most recent generation as preview source\n  const { data: historyData } = useHistory({ limit: 1 });\n  useEffect(() => {\n    if (!previewGenId && historyData?.items?.length) {\n      const first = historyData.items.find((g) => g.status === 'completed');\n      if (first) setPreviewGenId(first.id);\n    }\n  }, [historyData, previewGenId]);\n\n  const { data: preset } = useQuery({\n    queryKey: ['effect-preset', selectedPresetId],\n    queryFn: () =>\n      selectedPresetId\n        ? apiClient\n            .listEffectPresets()\n            .then((all) => all.find((p) => p.id === selectedPresetId) ?? null)\n        : null,\n    enabled: !!selectedPresetId,\n    staleTime: 30_000,\n  });\n\n  // Sync name/description when selecting a preset\n  useEffect(() => {\n    if (preset) {\n      setName(preset.name);\n      setDescription(preset.description ?? '');\n    } else if (isCreatingNew) {\n      setName('');\n      setDescription('');\n    }\n  }, [preset, isCreatingNew]);\n\n  // Cleanup blob URL on unmount\n  useEffect(() => {\n    return () => {\n      if (blobUrlRef.current) {\n        URL.revokeObjectURL(blobUrlRef.current);\n        blobUrlRef.current = null;\n      }\n    };\n  }, []);\n\n  const isEditing = !!selectedPresetId || isCreatingNew;\n  const isBuiltIn = preset?.is_builtin ?? false;\n\n  async function handlePreview() {\n    if (!previewGenId || workingChain.length === 0) return;\n\n    setPreviewLoading(true);\n    try {\n      const blob = await apiClient.previewEffects(previewGenId, workingChain);\n\n      // Revoke old blob URL\n      if (blobUrlRef.current) {\n        URL.revokeObjectURL(blobUrlRef.current);\n      }\n\n      const url = URL.createObjectURL(blob);\n      blobUrlRef.current = url;\n\n      // Play through the main audio player\n      setAudioWithAutoPlay(url, `preview-${Date.now()}`, null, 'Effects Preview');\n    } catch (error) {\n      toast({\n        title: 'Preview failed',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    } finally {\n      setPreviewLoading(false);\n    }\n  }\n\n  function handleSelectGeneration(gen: HistoryResponse) {\n    setPreviewGenId(gen.id);\n  }\n\n  async function handleSaveNew() {\n    if (!name.trim()) {\n      toast({ title: 'Name required', variant: 'destructive' });\n      return;\n    }\n    setSaving(true);\n    try {\n      const created = await apiClient.createEffectPreset({\n        name: name.trim(),\n        description: description.trim() || undefined,\n        effects_chain: workingChain,\n      });\n      queryClient.invalidateQueries({ queryKey: ['effect-presets'] });\n      setIsCreatingNew(false);\n      setSelectedPresetId(created.id);\n      toast({ title: 'Preset saved', description: `\"${created.name}\" has been created.` });\n    } catch (error) {\n      toast({\n        title: 'Failed to save',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    } finally {\n      setSaving(false);\n    }\n  }\n\n  async function handleSaveExisting() {\n    if (!selectedPresetId || !name.trim()) return;\n    setSaving(true);\n    try {\n      await apiClient.updateEffectPreset(selectedPresetId, {\n        name: name.trim(),\n        description: description.trim() || undefined,\n        effects_chain: workingChain,\n      });\n      queryClient.invalidateQueries({ queryKey: ['effect-presets'] });\n      queryClient.invalidateQueries({ queryKey: ['effect-preset', selectedPresetId] });\n      toast({ title: 'Preset updated' });\n    } catch (error) {\n      toast({\n        title: 'Failed to save',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    } finally {\n      setSaving(false);\n    }\n  }\n\n  function handleSaveAsNew() {\n    // Open the dialog with a suggested name based on the current preset\n    setSaveAsName(`${name} (Copy)`);\n    setSaveAsDescription(description);\n    setSaveAsDialogOpen(true);\n  }\n\n  async function handleSaveAsConfirm() {\n    if (!saveAsName.trim()) {\n      toast({ title: 'Name required', variant: 'destructive' });\n      return;\n    }\n    setSaving(true);\n    try {\n      const created = await apiClient.createEffectPreset({\n        name: saveAsName.trim(),\n        description: saveAsDescription.trim() || undefined,\n        effects_chain: workingChain,\n      });\n      queryClient.invalidateQueries({ queryKey: ['effect-presets'] });\n      setSaveAsDialogOpen(false);\n      setSelectedPresetId(created.id);\n      toast({ title: 'Preset saved', description: `\"${created.name}\" has been created.` });\n    } catch (error) {\n      toast({\n        title: 'Failed to save',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    } finally {\n      setSaving(false);\n    }\n  }\n\n  async function handleDelete() {\n    if (!selectedPresetId) return;\n    setDeleting(true);\n    try {\n      await apiClient.deleteEffectPreset(selectedPresetId);\n      queryClient.invalidateQueries({ queryKey: ['effect-presets'] });\n      setSelectedPresetId(null);\n      setWorkingChain([]);\n      toast({ title: 'Preset deleted' });\n    } catch (error) {\n      toast({\n        title: 'Failed to delete',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    } finally {\n      setDeleting(false);\n    }\n  }\n\n  if (!isEditing) {\n    return (\n      <div className=\"flex-1 flex items-center justify-center text-muted-foreground\">\n        <div className=\"text-center space-y-2\">\n          <Wand2 className=\"h-10 w-10 mx-auto opacity-30\" />\n          <p className=\"text-sm\">Select a preset or create a new one</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col h-full min-h-0\">\n      {/* Header */}\n      <div className=\"flex items-center justify-between mb-4\">\n        <h2 className=\"text-lg font-semibold\">\n          {isCreatingNew ? 'New Preset' : isBuiltIn ? preset?.name : 'Edit Preset'}\n        </h2>\n        <div className=\"flex items-center gap-2\">\n          {!isBuiltIn && !isCreatingNew && (\n            <>\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"h-8 text-destructive hover:text-destructive gap-1.5\"\n                onClick={handleDelete}\n                disabled={deleting}\n              >\n                <Trash2 className=\"h-3.5 w-3.5\" />\n                {deleting ? 'Deleting...' : 'Delete'}\n              </Button>\n              <Button\n                size=\"sm\"\n                className=\"h-8 gap-1.5\"\n                onClick={handleSaveExisting}\n                disabled={saving || workingChain.length === 0}\n              >\n                <Save className=\"h-3.5 w-3.5\" />\n                {saving ? 'Saving...' : 'Save'}\n              </Button>\n            </>\n          )}\n          {isCreatingNew && (\n            <Button\n              size=\"sm\"\n              className=\"h-8 gap-1.5\"\n              onClick={handleSaveNew}\n              disabled={saving || workingChain.length === 0}\n            >\n              <Save className=\"h-3.5 w-3.5\" />\n              {saving ? 'Saving...' : 'Save Preset'}\n            </Button>\n          )}\n          {isBuiltIn && (\n            <Button\n              size=\"sm\"\n              variant=\"outline\"\n              className=\"h-8 gap-1.5\"\n              onClick={handleSaveAsNew}\n              disabled={saving}\n            >\n              <Save className=\"h-3.5 w-3.5\" />\n              {saving ? 'Saving...' : 'Save as Custom'}\n            </Button>\n          )}\n        </div>\n      </div>\n\n      {/* Scrollable content */}\n      <div className=\"flex-1 min-h-0 overflow-y-auto space-y-5 pr-1\">\n        {/* Name & description */}\n        {(isCreatingNew || !isBuiltIn) && (\n          <div className=\"space-y-3\">\n            <div className=\"space-y-1.5\">\n              <Label className=\"text-xs\">Name</Label>\n              <Input\n                value={name}\n                onChange={(e) => setName(e.target.value)}\n                placeholder=\"My preset...\"\n                className=\"h-9\"\n              />\n            </div>\n            <div className=\"space-y-1.5\">\n              <Label className=\"text-xs\">Description</Label>\n              <Textarea\n                value={description}\n                onChange={(e) => setDescription(e.target.value)}\n                placeholder=\"Describe what this preset does...\"\n                className=\"min-h-[60px] resize-none\"\n              />\n            </div>\n          </div>\n        )}\n\n        {/* Built-in description (read-only) */}\n        {isBuiltIn && preset?.description && (\n          <p className=\"text-sm text-muted-foreground\">{preset.description}</p>\n        )}\n\n        {/* Effects chain editor */}\n        <EffectsChainEditor value={workingChain} onChange={setWorkingChain} showPresets={false} />\n\n        <Separator />\n\n        {/* Preview section */}\n        <div className=\"space-y-3\">\n          <Label className=\"text-xs\">Preview</Label>\n          <div className=\"flex items-center gap-2\">\n            <GenerationPicker\n              selectedId={previewGenId}\n              onSelect={handleSelectGeneration}\n              className=\"flex-1\"\n            />\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              className=\"h-8 gap-1.5 shrink-0\"\n              onClick={handlePreview}\n              disabled={!previewGenId || workingChain.length === 0 || previewLoading}\n            >\n              {previewLoading ? (\n                <>\n                  <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n                  Processing...\n                </>\n              ) : (\n                <>\n                  <Play className=\"h-3.5 w-3.5\" />\n                  Preview\n                </>\n              )}\n            </Button>\n          </div>\n          <p className=\"text-[11px] text-muted-foreground\">\n            Preview applies effects to the clean version without saving.\n          </p>\n        </div>\n      </div>\n\n      {/* Save as Custom dialog */}\n      <Dialog open={saveAsDialogOpen} onOpenChange={setSaveAsDialogOpen}>\n        <DialogContent className=\"sm:max-w-md\">\n          <DialogHeader>\n            <DialogTitle>Save as Custom Preset</DialogTitle>\n            <DialogDescription>\n              Create a new custom preset based on the current effects chain.\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"space-y-3 py-2\">\n            <div className=\"space-y-1.5\">\n              <Label className=\"text-xs\">Name</Label>\n              <Input\n                value={saveAsName}\n                onChange={(e) => setSaveAsName(e.target.value)}\n                placeholder=\"My preset...\"\n                className=\"h-9\"\n                autoFocus\n                onKeyDown={(e) => {\n                  if (e.key === 'Enter' && saveAsName.trim()) {\n                    handleSaveAsConfirm();\n                  }\n                }}\n              />\n            </div>\n            <div className=\"space-y-1.5\">\n              <Label className=\"text-xs\">Description</Label>\n              <Textarea\n                value={saveAsDescription}\n                onChange={(e) => setSaveAsDescription(e.target.value)}\n                placeholder=\"Describe what this preset does...\"\n                className=\"min-h-[60px] resize-none\"\n              />\n            </div>\n          </div>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setSaveAsDialogOpen(false)} disabled={saving}>\n              Cancel\n            </Button>\n            <Button onClick={handleSaveAsConfirm} disabled={saving || !saveAsName.trim()}>\n              <Save className=\"h-3.5 w-3.5 mr-1.5\" />\n              {saving ? 'Saving...' : 'Save'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/EffectsTab/EffectsList.tsx",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { Loader2, Plus, Sparkles, Wand2 } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { apiClient } from '@/lib/api/client';\nimport type { EffectPresetResponse } from '@/lib/api/types';\nimport { cn } from '@/lib/utils/cn';\nimport { useEffectsStore } from '@/stores/effectsStore';\n\nexport function EffectsList() {\n  const selectedPresetId = useEffectsStore((s) => s.selectedPresetId);\n  const setSelectedPresetId = useEffectsStore((s) => s.setSelectedPresetId);\n  const setWorkingChain = useEffectsStore((s) => s.setWorkingChain);\n  const setIsCreatingNew = useEffectsStore((s) => s.setIsCreatingNew);\n  const isCreatingNew = useEffectsStore((s) => s.isCreatingNew);\n\n  const { data: presets, isLoading } = useQuery({\n    queryKey: ['effect-presets'],\n    queryFn: () => apiClient.listEffectPresets(),\n    staleTime: 30_000,\n  });\n\n  const builtIn = presets?.filter((p) => p.is_builtin) ?? [];\n  const userPresets = presets?.filter((p) => !p.is_builtin) ?? [];\n\n  function handleSelect(preset: EffectPresetResponse) {\n    setSelectedPresetId(preset.id);\n    setWorkingChain(preset.effects_chain);\n  }\n\n  function handleCreateNew() {\n    setIsCreatingNew(true);\n    setWorkingChain([]);\n  }\n\n  if (isLoading) {\n    return (\n      <div className=\"flex items-center justify-center h-full\">\n        <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col h-full min-h-0\">\n      {/* Header */}\n      <div className=\"flex items-center justify-between mb-4\">\n        <h2 className=\"text-lg font-semibold\">Effects</h2>\n        <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-1.5\" onClick={handleCreateNew}>\n          <Plus className=\"h-3.5 w-3.5\" />\n          New Preset\n        </Button>\n      </div>\n\n      {/* Scrollable list */}\n      <div className=\"flex-1 min-h-0 overflow-y-auto space-y-4\">\n        {/* Built-in presets */}\n        {builtIn.length > 0 && (\n          <div>\n            <div className=\"text-[11px] text-muted-foreground font-medium uppercase tracking-wider mb-2 px-1\">\n              Built-in\n            </div>\n            <div className=\"space-y-1.5\">\n              {builtIn.map((preset) => (\n                <PresetCard\n                  key={preset.id}\n                  preset={preset}\n                  isSelected={selectedPresetId === preset.id && !isCreatingNew}\n                  onSelect={() => handleSelect(preset)}\n                />\n              ))}\n            </div>\n          </div>\n        )}\n\n        {/* User presets */}\n        {userPresets.length > 0 && (\n          <div>\n            <div className=\"text-[11px] text-muted-foreground font-medium uppercase tracking-wider mb-2 px-1\">\n              Custom\n            </div>\n            <div className=\"space-y-1.5\">\n              {userPresets.map((preset) => (\n                <PresetCard\n                  key={preset.id}\n                  preset={preset}\n                  isSelected={selectedPresetId === preset.id && !isCreatingNew}\n                  onSelect={() => handleSelect(preset)}\n                />\n              ))}\n            </div>\n          </div>\n        )}\n\n        {/* New preset placeholder */}\n        {isCreatingNew && (\n          <div>\n            <div className=\"text-[11px] text-muted-foreground font-medium uppercase tracking-wider mb-2 px-1\">\n              New\n            </div>\n            <div className=\"rounded-xl border-2 border-accent/40 bg-accent/5 p-3\">\n              <div className=\"flex items-center gap-2\">\n                <Sparkles className=\"h-4 w-4 text-accent\" />\n                <span className=\"text-sm font-medium\">Unsaved Preset</span>\n              </div>\n              <p className=\"text-xs text-muted-foreground mt-1\">\n                Configure effects in the panel on the right.\n              </p>\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction PresetCard({\n  preset,\n  isSelected,\n  onSelect,\n}: {\n  preset: EffectPresetResponse;\n  isSelected: boolean;\n  onSelect: () => void;\n}) {\n  const effectCount = preset.effects_chain.length;\n\n  return (\n    <button\n      type=\"button\"\n      className={cn(\n        'w-full text-left rounded-xl border p-3 h-[88px] transition-all duration-150',\n        isSelected\n          ? 'border-accent/50 bg-accent/10'\n          : 'border-border bg-card hover:bg-muted/50 hover:border-border',\n      )}\n      onClick={onSelect}\n    >\n      <div className=\"flex items-center gap-2\">\n        <Wand2\n          className={cn('h-4 w-4 shrink-0', isSelected ? 'text-accent' : 'text-muted-foreground')}\n        />\n        <span className=\"text-sm font-medium truncate\">{preset.name}</span>\n        {preset.is_builtin && (\n          <span className=\"text-[10px] bg-muted text-muted-foreground px-1.5 py-0.5 rounded-full shrink-0\">\n            built-in\n          </span>\n        )}\n      </div>\n      <p className=\"text-xs text-muted-foreground mt-1 line-clamp-1 pl-6\">\n        {preset.description || 'No description'}\n      </p>\n      <div className=\"flex items-center gap-2 mt-1.5 pl-6\">\n        <span className=\"text-[10px] text-muted-foreground\">\n          {effectCount} effect{effectCount !== 1 ? 's' : ''}\n        </span>\n        <span className=\"text-[10px] text-muted-foreground/50\">\n          {preset.effects_chain\n            .filter((e) => e.enabled)\n            .map((e) => e.type)\n            .join(' → ')}\n        </span>\n      </div>\n    </button>\n  );\n}\n"
  },
  {
    "path": "app/src/components/EffectsTab/EffectsTab.tsx",
    "content": "import {EffectsDetail} from \"./EffectsDetail\";\nimport {EffectsList} from \"./EffectsList\";\n\nexport function EffectsTab() {\n\treturn (\n\t\t<div className=\"flex flex-col h-full min-h-0 overflow-hidden\">\n\t\t\t<div className=\"flex-1 min-h-0 flex gap-6 overflow-hidden\">\n\t\t\t\t{/* Left - Presets list */}\n\t\t\t\t<div className=\"w-full max-w-[360px] shrink-0 flex flex-col min-h-0\">\n\t\t\t\t\t<EffectsList />\n\t\t\t\t</div>\n\n\t\t\t\t{/* Right - Detail / editor */}\n\t\t\t\t<div className=\"flex-1 min-h-0 flex flex-col\">\n\t\t\t\t\t<EffectsDetail />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "app/src/components/Generation/EngineModelSelector.tsx",
    "content": "import { useEffect } from 'react';\nimport type { UseFormReturn } from 'react-hook-form';\nimport { FormControl } from '@/components/ui/form';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport type { VoiceProfileResponse } from '@/lib/api/types';\nimport { getLanguageOptionsForEngine } from '@/lib/constants/languages';\nimport type { GenerationFormValues } from '@/lib/hooks/useGenerationForm';\n\n/**\n * Engine/model options and their display metadata.\n * Adding a new engine means adding one entry here.\n */\nconst ENGINE_OPTIONS = [\n  { value: 'qwen:1.7B', label: 'Qwen3-TTS 1.7B', engine: 'qwen' },\n  { value: 'qwen:0.6B', label: 'Qwen3-TTS 0.6B', engine: 'qwen' },\n  { value: 'luxtts', label: 'LuxTTS', engine: 'luxtts' },\n  { value: 'chatterbox', label: 'Chatterbox', engine: 'chatterbox' },\n  { value: 'chatterbox_turbo', label: 'Chatterbox Turbo', engine: 'chatterbox_turbo' },\n  { value: 'tada:1B', label: 'TADA 1B', engine: 'tada' },\n  { value: 'tada:3B', label: 'TADA 3B Multilingual', engine: 'tada' },\n  { value: 'kokoro', label: 'Kokoro 82M', engine: 'kokoro' },\n] as const;\n\nconst ENGINE_DESCRIPTIONS: Record<string, string> = {\n  qwen: 'Multi-language, two sizes',\n  luxtts: 'Fast, English-focused',\n  chatterbox: '23 languages, incl. Hebrew',\n  chatterbox_turbo: 'English, [laugh] [cough] tags',\n  tada: 'HumeAI, 700s+ coherent audio',\n  kokoro: '82M params, CPU realtime, 8 langs',\n};\n\n/** Engines that only support English and should force language to 'en' on select. */\nconst ENGLISH_ONLY_ENGINES = new Set(['luxtts', 'chatterbox_turbo']);\n\n/** Engines that support cloned (reference audio) profiles. */\nconst CLONING_ENGINES = new Set(['qwen', 'luxtts', 'chatterbox', 'chatterbox_turbo', 'tada']);\n\nfunction getAvailableOptions(selectedProfile?: VoiceProfileResponse | null) {\n  if (!selectedProfile) return ENGINE_OPTIONS;\n  return ENGINE_OPTIONS.filter((opt) => isProfileCompatibleWithEngine(selectedProfile, opt.engine));\n}\n\nfunction getSelectValue(engine: string, modelSize?: string): string {\n  if (engine === 'qwen') return `qwen:${modelSize || '1.7B'}`;\n  if (engine === 'tada') return `tada:${modelSize || '1B'}`;\n  return engine;\n}\n\nfunction handleEngineChange(form: UseFormReturn<GenerationFormValues>, value: string) {\n  if (value.startsWith('qwen:')) {\n    const [, modelSize] = value.split(':');\n    form.setValue('engine', 'qwen');\n    form.setValue('modelSize', modelSize as '1.7B' | '0.6B');\n    // Validate language is supported by Qwen\n    const currentLang = form.getValues('language');\n    const available = getLanguageOptionsForEngine('qwen');\n    if (!available.some((l) => l.value === currentLang)) {\n      form.setValue('language', available[0]?.value ?? 'en');\n    }\n  } else if (value.startsWith('tada:')) {\n    const [, modelSize] = value.split(':');\n    form.setValue('engine', 'tada');\n    form.setValue('modelSize', modelSize as '1B' | '3B');\n    // TADA 1B is English-only; 3B is multilingual\n    if (modelSize === '1B') {\n      form.setValue('language', 'en');\n    } else {\n      const currentLang = form.getValues('language');\n      const available = getLanguageOptionsForEngine('tada');\n      if (!available.some((l) => l.value === currentLang)) {\n        form.setValue('language', available[0]?.value ?? 'en');\n      }\n    }\n  } else {\n    form.setValue('engine', value as GenerationFormValues['engine']);\n    form.setValue('modelSize', undefined as unknown as '1.7B' | '0.6B');\n    if (ENGLISH_ONLY_ENGINES.has(value)) {\n      form.setValue('language', 'en');\n    } else {\n      // If current language isn't supported by the new engine, reset to first available\n      const currentLang = form.getValues('language');\n      const available = getLanguageOptionsForEngine(value);\n      if (!available.some((l) => l.value === currentLang)) {\n        form.setValue('language', available[0]?.value ?? 'en');\n      }\n    }\n  }\n}\n\ninterface EngineModelSelectorProps {\n  form: UseFormReturn<GenerationFormValues>;\n  compact?: boolean;\n  selectedProfile?: VoiceProfileResponse | null;\n}\n\nexport function EngineModelSelector({ form, compact, selectedProfile }: EngineModelSelectorProps) {\n  const engine = form.watch('engine') || 'qwen';\n  const modelSize = form.watch('modelSize');\n  const selectValue = getSelectValue(engine, modelSize);\n  const availableOptions = getAvailableOptions(selectedProfile);\n\n  const currentEngineAvailable = availableOptions.some((opt) => opt.value === selectValue);\n\n  useEffect(() => {\n    if (!currentEngineAvailable && availableOptions.length > 0) {\n      handleEngineChange(form, availableOptions[0].value);\n    }\n  }, [availableOptions, currentEngineAvailable, form]);\n\n  const itemClass = compact ? 'text-xs text-muted-foreground' : undefined;\n  const triggerClass = compact\n    ? 'h-8 text-xs bg-card border-border rounded-full hover:bg-background/50 transition-all'\n    : undefined;\n\n  return (\n    <Select value={selectValue} onValueChange={(v) => handleEngineChange(form, v)}>\n      <FormControl>\n        <SelectTrigger className={triggerClass}>\n          <SelectValue />\n        </SelectTrigger>\n      </FormControl>\n      <SelectContent>\n        {availableOptions.map((opt) => (\n          <SelectItem key={opt.value} value={opt.value} className={itemClass}>\n            {opt.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\n/** Returns a human-readable description for the currently selected engine. */\nexport function getEngineDescription(engine: string): string {\n  return ENGINE_DESCRIPTIONS[engine] ?? '';\n}\n\n/**\n * Check if a profile is compatible with the currently selected engine.\n * Useful for UI hints.\n */\nexport function isProfileCompatibleWithEngine(\n  profile: VoiceProfileResponse,\n  engine: string,\n): boolean {\n  const voiceType = profile.voice_type || 'cloned';\n  if (voiceType === 'preset') return profile.preset_engine === engine;\n  if (voiceType === 'cloned') return CLONING_ENGINES.has(engine);\n  return true; // designed — future\n}\n"
  },
  {
    "path": "app/src/components/Generation/FloatingGenerateBox.tsx",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { useMatchRoute } from '@tanstack/react-router';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { Loader2, Sparkles } from 'lucide-react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { Textarea } from '@/components/ui/textarea';\nimport { apiClient } from '@/lib/api/client';\nimport { getLanguageOptionsForEngine, type LanguageCode } from '@/lib/constants/languages';\nimport { useGenerationForm } from '@/lib/hooks/useGenerationForm';\nimport { useProfile, useProfiles } from '@/lib/hooks/useProfiles';\nimport { useStory } from '@/lib/hooks/useStories';\nimport { cn } from '@/lib/utils/cn';\nimport { useGenerationStore } from '@/stores/generationStore';\nimport { useStoryStore } from '@/stores/storyStore';\nimport { useUIStore } from '@/stores/uiStore';\nimport { EngineModelSelector } from './EngineModelSelector';\nimport { ParalinguisticInput } from './ParalinguisticInput';\n\ninterface FloatingGenerateBoxProps {\n  isPlayerOpen?: boolean;\n  showVoiceSelector?: boolean;\n}\n\nexport function FloatingGenerateBox({\n  isPlayerOpen = false,\n  showVoiceSelector = false,\n}: FloatingGenerateBoxProps) {\n  const selectedProfileId = useUIStore((state) => state.selectedProfileId);\n  const setSelectedProfileId = useUIStore((state) => state.setSelectedProfileId);\n  const setSelectedEngine = useUIStore((state) => state.setSelectedEngine);\n  const { data: selectedProfile } = useProfile(selectedProfileId || '');\n  const { data: profiles } = useProfiles();\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [selectedPresetId, setSelectedPresetId] = useState<string | null>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const textareaRef = useRef<HTMLTextAreaElement | null>(null);\n  const matchRoute = useMatchRoute();\n  const isStoriesRoute = matchRoute({ to: '/stories' });\n  const selectedStoryId = useStoryStore((state) => state.selectedStoryId);\n  const trackEditorHeight = useStoryStore((state) => state.trackEditorHeight);\n  const { data: currentStory } = useStory(selectedStoryId);\n  const addPendingStoryAdd = useGenerationStore((s) => s.addPendingStoryAdd);\n\n  // Fetch effect presets for the dropdown\n  const { data: effectPresets } = useQuery({\n    queryKey: ['effectPresets'],\n    queryFn: () => apiClient.listEffectPresets(),\n  });\n\n  // Calculate if track editor is visible (on stories route with items)\n  const hasTrackEditor = isStoriesRoute && currentStory && currentStory.items.length > 0;\n\n  const { form, handleSubmit, isPending } = useGenerationForm({\n    onSuccess: async (generationId) => {\n      setIsExpanded(false);\n      // Defer the story add until TTS completes -- useGenerationProgress handles it\n      if (isStoriesRoute && selectedStoryId && generationId) {\n        addPendingStoryAdd(generationId, selectedStoryId);\n      }\n    },\n    getEffectsChain: () => {\n      if (!selectedPresetId) return undefined;\n      // Profile's own effects chain (no matching preset)\n      if (selectedPresetId === '_profile') {\n        return selectedProfile?.effects_chain ?? undefined;\n      }\n      if (!effectPresets) return undefined;\n      const preset = effectPresets.find((p) => p.id === selectedPresetId);\n      return preset?.effects_chain;\n    },\n  });\n\n  // Click away handler to collapse the box\n  useEffect(() => {\n    function handleClickOutside(event: MouseEvent) {\n      const target = event.target as HTMLElement;\n\n      // Don't collapse if clicking inside the container\n      if (containerRef.current?.contains(target)) {\n        return;\n      }\n\n      // Don't collapse if clicking on a Select dropdown (which renders in a portal)\n      if (\n        target.closest('[role=\"listbox\"]') ||\n        target.closest('[data-radix-popper-content-wrapper]')\n      ) {\n        return;\n      }\n\n      setIsExpanded(false);\n    }\n\n    if (isExpanded) {\n      document.addEventListener('mousedown', handleClickOutside);\n    }\n\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n    };\n  }, [isExpanded]);\n\n  // Set first voice as default if none selected\n  useEffect(() => {\n    if (!selectedProfileId && profiles && profiles.length > 0) {\n      setSelectedProfileId(profiles[0].id);\n    }\n  }, [selectedProfileId, profiles, setSelectedProfileId]);\n\n  // Sync engine selection to global store so ProfileList can filter\n  const watchedEngine = form.watch('engine');\n  useEffect(() => {\n    if (watchedEngine) {\n      setSelectedEngine(watchedEngine);\n    }\n  }, [watchedEngine, setSelectedEngine]);\n\n  // Sync generation form language, engine, and effects with selected profile\n  useEffect(() => {\n    if (selectedProfile?.language) {\n      form.setValue('language', selectedProfile.language as LanguageCode);\n    }\n    // Auto-switch engine if profile has a default\n    if (selectedProfile?.default_engine) {\n      form.setValue(\n        'engine',\n        selectedProfile.default_engine as\n          | 'qwen'\n          | 'luxtts'\n          | 'chatterbox'\n          | 'chatterbox_turbo'\n          | 'tada'\n          | 'kokoro',\n      );\n    }\n    // Pre-fill effects from profile defaults\n    if (\n      selectedProfile?.effects_chain &&\n      selectedProfile.effects_chain.length > 0 &&\n      effectPresets\n    ) {\n      // Try to match against a known preset\n      const profileChainJson = JSON.stringify(selectedProfile.effects_chain);\n      const matchingPreset = effectPresets.find(\n        (p) => JSON.stringify(p.effects_chain) === profileChainJson,\n      );\n      if (matchingPreset) {\n        setSelectedPresetId(matchingPreset.id);\n      } else {\n        // No matching preset — use special value to pass profile chain directly\n        setSelectedPresetId('_profile');\n      }\n    } else if (\n      selectedProfile &&\n      (!selectedProfile.effects_chain || selectedProfile.effects_chain.length === 0)\n    ) {\n      setSelectedPresetId(null);\n    }\n  }, [selectedProfile, effectPresets, form]);\n\n  // Auto-resize textarea based on content (only when expanded)\n  useEffect(() => {\n    if (!isExpanded) {\n      // Reset textarea height after collapse animation completes\n      const timeoutId = setTimeout(() => {\n        const textarea = textareaRef.current;\n        if (textarea) {\n          textarea.style.height = '32px';\n          textarea.style.overflowY = 'hidden';\n        }\n      }, 200); // Wait for animation to complete\n      return () => clearTimeout(timeoutId);\n    }\n\n    const textarea = textareaRef.current;\n    if (!textarea) return;\n\n    const adjustHeight = () => {\n      textarea.style.height = 'auto';\n      const scrollHeight = textarea.scrollHeight;\n      const minHeight = 100; // Expanded minimum\n      const maxHeight = 300; // Max height in pixels\n      const targetHeight = Math.max(minHeight, Math.min(scrollHeight, maxHeight));\n      textarea.style.height = `${targetHeight}px`;\n\n      // Show scrollbar if content exceeds max height\n      if (scrollHeight > maxHeight) {\n        textarea.style.overflowY = 'auto';\n      } else {\n        textarea.style.overflowY = 'hidden';\n      }\n    };\n\n    // Small delay to let framer animation complete\n    const timeoutId = setTimeout(() => {\n      adjustHeight();\n    }, 200);\n\n    // Adjust on mount and when value changes\n    adjustHeight();\n\n    // Watch for input changes\n    textarea.addEventListener('input', adjustHeight);\n\n    return () => {\n      clearTimeout(timeoutId);\n      textarea.removeEventListener('input', adjustHeight);\n    };\n  }, [isExpanded]);\n\n  async function onSubmit(data: Parameters<typeof handleSubmit>[0]) {\n    await handleSubmit(data, selectedProfileId);\n  }\n\n  return (\n    <motion.div\n      ref={containerRef}\n      className={cn(\n        'fixed right-auto',\n        isStoriesRoute\n          ? // Position aligned with story list: after sidebar + padding, width 360px\n            'left-[calc(5rem+2rem)] w-[360px]'\n          : 'left-[calc(5rem+2rem)] right-8 lg:right-auto lg:w-[calc((100%-5rem-4rem)/2-1rem)]',\n      )}\n      style={{\n        // On stories route: offset by track editor height when visible\n        // On other routes: offset by audio player height when visible\n        bottom: hasTrackEditor\n          ? `${trackEditorHeight + 24}px`\n          : isPlayerOpen\n            ? 'calc(7rem + 1.5rem)'\n            : '1.5rem',\n      }}\n    >\n      <motion.div\n        className=\"bg-background/30 backdrop-blur-2xl border border-accent/20 rounded-[2rem] shadow-2xl hover:bg-background/40 hover:border-accent/20 transition-all duration-300 p-3\"\n        transition={{ duration: 0.6, ease: 'easeInOut' }}\n      >\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit(onSubmit)}>\n            <div className=\"flex gap-2\">\n              <motion.div className=\"flex-1\" transition={{ duration: 0.3, ease: 'easeOut' }}>\n                <FormField\n                  control={form.control}\n                  name=\"text\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormControl>\n                        <motion.div\n                          animate={{\n                            height: isExpanded ? 'auto' : '32px',\n                          }}\n                          transition={{ duration: 0.15, ease: 'easeOut' }}\n                          style={{ overflow: 'hidden' }}\n                        >\n                          {form.watch('engine') === 'chatterbox_turbo' ? (\n                            <ParalinguisticInput\n                              value={field.value}\n                              onChange={field.onChange}\n                              placeholder={\n                                isStoriesRoute && currentStory\n                                  ? `Generate speech for \"${currentStory.name}\"... (type / for effects)`\n                                  : selectedProfile\n                                    ? `Type / for effects like [laugh], [sigh]...`\n                                    : 'Select a voice profile above...'\n                              }\n                              className=\"px-3 py-2 resize-none bg-transparent border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0 outline-none ring-0 rounded-2xl text-sm w-full\"\n                              style={{\n                                minHeight: isExpanded ? '100px' : '32px',\n                                maxHeight: '300px',\n                                overflowY: 'auto',\n                              }}\n                              disabled={!selectedProfileId}\n                              onClick={() => setIsExpanded(true)}\n                              onFocus={() => setIsExpanded(true)}\n                            />\n                          ) : (\n                            <Textarea\n                              {...field}\n                              ref={(node: HTMLTextAreaElement | null) => {\n                                textareaRef.current = node;\n                                if (typeof field.ref === 'function') {\n                                  field.ref(node);\n                                }\n                              }}\n                              placeholder={\n                                isStoriesRoute && currentStory\n                                  ? `Generate speech for \"${currentStory.name}\"...`\n                                  : selectedProfile\n                                    ? `Generate speech using ${selectedProfile.name}...`\n                                    : 'Select a voice profile above...'\n                              }\n                              className=\"resize-none bg-transparent border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0 outline-none ring-0 rounded-2xl text-sm placeholder:text-muted-foreground/60 w-full\"\n                              style={{\n                                minHeight: isExpanded ? '100px' : '32px',\n                                maxHeight: '300px',\n                              }}\n                              disabled={!selectedProfileId}\n                              onClick={() => setIsExpanded(true)}\n                              onFocus={() => setIsExpanded(true)}\n                            />\n                          )}\n                        </motion.div>\n                      </FormControl>\n                      <FormMessage className=\"text-xs\" />\n                    </FormItem>\n                  )}\n                />\n              </motion.div>\n\n              <div className=\"relative shrink-0\">\n                <div className=\"group relative\">\n                  <Button\n                    type=\"submit\"\n                    disabled={isPending || !selectedProfileId}\n                    className=\"h-10 w-10 rounded-full bg-accent hover:bg-accent/90 hover:scale-105 text-accent-foreground shadow-lg hover:shadow-accent/50 transition-all duration-200\"\n                    size=\"icon\"\n                    aria-label={\n                      isPending\n                        ? 'Generating...'\n                        : !selectedProfileId\n                          ? 'Select a voice profile first'\n                          : 'Generate speech'\n                    }\n                  >\n                    {isPending ? (\n                      <Loader2 className=\"h-4 w-4 animate-spin\" />\n                    ) : (\n                      <Sparkles className=\"h-4 w-4\" />\n                    )}\n                  </Button>\n                  <span className=\"pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 whitespace-nowrap rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground border border-border opacity-0 transition-opacity group-hover:opacity-100 z-[9999]\">\n                    {isPending\n                      ? 'Generating...'\n                      : !selectedProfileId\n                        ? 'Select a voice profile first'\n                        : 'Generate speech'}\n                  </span>\n                </div>\n              </div>\n            </div>\n\n            <AnimatePresence>\n              <motion.div\n                initial={{ height: 0, opacity: 0 }}\n                animate={{ height: 'auto', opacity: 1 }}\n                exit={{ height: 0, opacity: 0 }}\n                transition={{ duration: 0.3, ease: 'easeOut' }}\n                className=\" mt-3\"\n              >\n                <div className=\"flex items-center gap-2\">\n                  {showVoiceSelector && (\n                    <div className=\"flex-1\">\n                      <Select\n                        value={selectedProfileId || ''}\n                        onValueChange={(value) => setSelectedProfileId(value || null)}\n                      >\n                        <SelectTrigger className=\"h-8 text-xs bg-card border-border rounded-full hover:bg-background/50 transition-all w-full\">\n                          <SelectValue placeholder=\"Select a voice...\" />\n                        </SelectTrigger>\n                        <SelectContent>\n                          {profiles?.map((profile) => (\n                            <SelectItem key={profile.id} value={profile.id} className=\"text-xs\">\n                              {profile.name}\n                            </SelectItem>\n                          ))}\n                        </SelectContent>\n                      </Select>\n                    </div>\n                  )}\n\n                  <FormField\n                    control={form.control}\n                    name=\"language\"\n                    render={({ field }) => {\n                      const engineLangs = getLanguageOptionsForEngine(\n                        form.watch('engine') || 'qwen',\n                      );\n                      return (\n                        <FormItem className=\"flex-1 space-y-0\">\n                          <Select onValueChange={field.onChange} value={field.value}>\n                            <FormControl>\n                              <SelectTrigger className=\"h-8 text-xs bg-card border-border rounded-full hover:bg-background/50 transition-all\">\n                                <SelectValue />\n                              </SelectTrigger>\n                            </FormControl>\n                            <SelectContent>\n                              {engineLangs.map((lang) => (\n                                <SelectItem key={lang.value} value={lang.value} className=\"text-xs\">\n                                  {lang.label}\n                                </SelectItem>\n                              ))}\n                            </SelectContent>\n                          </Select>\n                          <FormMessage className=\"text-xs\" />\n                        </FormItem>\n                      );\n                    }}\n                  />\n\n                  <FormItem className=\"flex-1 space-y-0\">\n                    <EngineModelSelector form={form} compact selectedProfile={selectedProfile} />\n                  </FormItem>\n\n                  <FormItem className=\"flex-1 space-y-0\">\n                    <Select\n                      value={selectedPresetId || 'none'}\n                      onValueChange={(value) =>\n                        setSelectedPresetId(value === 'none' ? null : value)\n                      }\n                    >\n                      <SelectTrigger className=\"h-8 text-xs bg-card border-border rounded-full hover:bg-background/50 transition-all\">\n                        <SelectValue placeholder=\"No effects\" />\n                      </SelectTrigger>\n                      <SelectContent>\n                        <SelectItem value=\"none\" className=\"text-xs\">\n                          No effects\n                        </SelectItem>\n                        {selectedProfile?.effects_chain &&\n                          selectedProfile.effects_chain.length > 0 && (\n                            <SelectItem value=\"_profile\" className=\"text-xs\">\n                              Profile default\n                            </SelectItem>\n                          )}\n                        {effectPresets?.map((preset) => (\n                          <SelectItem key={preset.id} value={preset.id} className=\"text-xs\">\n                            {preset.name}\n                          </SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                  </FormItem>\n                </div>\n              </motion.div>\n            </AnimatePresence>\n          </form>\n        </Form>\n      </motion.div>\n    </motion.div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/Generation/GenerationForm.tsx",
    "content": "import { Loader2, Mic } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from '@/components/ui/form';\nimport { Input } from '@/components/ui/input';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { Textarea } from '@/components/ui/textarea';\nimport { getLanguageOptionsForEngine } from '@/lib/constants/languages';\nimport { useGenerationForm } from '@/lib/hooks/useGenerationForm';\nimport { useProfile } from '@/lib/hooks/useProfiles';\nimport { useUIStore } from '@/stores/uiStore';\nimport { EngineModelSelector, getEngineDescription } from './EngineModelSelector';\nimport { ParalinguisticInput } from './ParalinguisticInput';\n\nexport function GenerationForm() {\n  const selectedProfileId = useUIStore((state) => state.selectedProfileId);\n  const { data: selectedProfile } = useProfile(selectedProfileId || '');\n\n  const { form, handleSubmit, isPending } = useGenerationForm();\n\n  async function onSubmit(data: Parameters<typeof handleSubmit>[0]) {\n    await handleSubmit(data, selectedProfileId);\n  }\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>Generate Speech</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n            <div>\n              <FormLabel>Voice Profile</FormLabel>\n              {selectedProfile ? (\n                <div className=\"mt-2 p-3 border rounded-md bg-muted/50 flex items-center gap-2\">\n                  <Mic className=\"h-4 w-4 text-muted-foreground\" />\n                  <span className=\"font-medium\">{selectedProfile.name}</span>\n                  <span className=\"text-sm text-muted-foreground\">{selectedProfile.language}</span>\n                </div>\n              ) : (\n                <div className=\"mt-2 p-3 border border-dashed rounded-md text-sm text-muted-foreground\">\n                  Click on a profile card above to select a voice profile\n                </div>\n              )}\n            </div>\n\n            <FormField\n              control={form.control}\n              name=\"text\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Text to Speak</FormLabel>\n                  <FormControl>\n                    {form.watch('engine') === 'chatterbox_turbo' ? (\n                      <ParalinguisticInput\n                        value={field.value}\n                        onChange={field.onChange}\n                        placeholder=\"Enter text... type / for effects like [laugh], [sigh]\"\n                        className=\"min-h-[150px] rounded-md border border-input bg-background px-3 py-2\"\n                      />\n                    ) : (\n                      <Textarea\n                        placeholder=\"Enter the text you want to generate...\"\n                        className=\"min-h-[150px]\"\n                        {...field}\n                      />\n                    )}\n                  </FormControl>\n                  <FormDescription>\n                    {form.watch('engine') === 'chatterbox_turbo'\n                      ? 'Max 5000 characters. Type / to insert sound effects.'\n                      : 'Max 5000 characters'}\n                  </FormDescription>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            {form.watch('engine') === 'qwen' && (\n              <FormField\n                control={form.control}\n                name=\"instruct\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Delivery Instructions (optional)</FormLabel>\n                    <FormControl>\n                      <Textarea\n                        placeholder=\"e.g. Speak slowly with emphasis, Warm and friendly tone, Professional and authoritative...\"\n                        className=\"min-h-[80px]\"\n                        {...field}\n                      />\n                    </FormControl>\n                    <FormDescription>\n                      Natural language instructions to control speech delivery (tone, emotion,\n                      pace). Max 500 characters\n                    </FormDescription>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            )}\n\n            <div className=\"grid gap-4 md:grid-cols-3\">\n              <FormItem>\n                <FormLabel>Model</FormLabel>\n                <EngineModelSelector form={form} selectedProfile={selectedProfile} />\n                <FormDescription>\n                  {getEngineDescription(form.watch('engine') || 'qwen')}\n                </FormDescription>\n              </FormItem>\n\n              <FormField\n                control={form.control}\n                name=\"language\"\n                render={({ field }) => {\n                  const engineLangs = getLanguageOptionsForEngine(form.watch('engine') || 'qwen');\n                  return (\n                    <FormItem>\n                      <FormLabel>Language</FormLabel>\n                      <Select onValueChange={field.onChange} value={field.value}>\n                        <FormControl>\n                          <SelectTrigger>\n                            <SelectValue />\n                          </SelectTrigger>\n                        </FormControl>\n                        <SelectContent>\n                          {engineLangs.map((lang) => (\n                            <SelectItem key={lang.value} value={lang.value}>\n                              {lang.label}\n                            </SelectItem>\n                          ))}\n                        </SelectContent>\n                      </Select>\n                      <FormMessage />\n                    </FormItem>\n                  );\n                }}\n              />\n\n              <FormField\n                control={form.control}\n                name=\"seed\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Seed (optional)</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"number\"\n                        placeholder=\"Random\"\n                        {...field}\n                        onChange={(e) =>\n                          field.onChange(e.target.value ? parseInt(e.target.value, 10) : undefined)\n                        }\n                      />\n                    </FormControl>\n                    <FormDescription>For reproducible results</FormDescription>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            </div>\n\n            <Button type=\"submit\" className=\"w-full\" disabled={isPending || !selectedProfileId}>\n              {isPending ? (\n                <>\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  Generating...\n                </>\n              ) : (\n                'Generate Speech'\n              )}\n            </Button>\n          </form>\n        </Form>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "app/src/components/Generation/ParalinguisticInput.tsx",
    "content": "/**\n * ParalinguisticInput — a contentEditable rich text input that renders\n * Chatterbox Turbo paralinguistic tags (e.g. [laugh]) as inline badges.\n *\n * Trigger: typing \"/\" opens an autocomplete dropdown.\n * Paste:   pasting text with [tag] patterns auto-converts to badges.\n * Output:  serializes badges back to plain [tag] text for the API.\n */\n\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { cn } from '@/lib/utils/cn';\n\n// ── Tag definitions ─────────────────────────────────────────────────\nconst PARALINGUISTIC_TAGS = [\n  { tag: '[laugh]', label: 'laugh', emoji: '\\u{1F602}' },\n  { tag: '[chuckle]', label: 'chuckle', emoji: '\\u{1F60F}' },\n  { tag: '[gasp]', label: 'gasp', emoji: '\\u{1F62E}' },\n  { tag: '[cough]', label: 'cough', emoji: '\\u{1F637}' },\n  { tag: '[sigh]', label: 'sigh', emoji: '\\u{1F614}' },\n  { tag: '[groan]', label: 'groan', emoji: '\\u{1F629}' },\n  { tag: '[sniff]', label: 'sniff', emoji: '\\u{1F443}' },\n  { tag: '[shush]', label: 'shush', emoji: '\\u{1F92B}' },\n  { tag: '[clear throat]', label: 'clear throat', emoji: '\\u{1F64A}' },\n] as const;\n\nconst TAG_REGEX = /\\[(laugh|chuckle|gasp|cough|sigh|groan|sniff|shush|clear throat)\\]/gi;\n\n// Data attribute used to identify badge spans in the DOM\nconst BADGE_ATTR = 'data-ptag';\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\n/** Build an inline badge <span> for a tag. */\nfunction makeBadgeHTML(tag: string): string {\n  const entry = PARALINGUISTIC_TAGS.find((t) => t.tag.toLowerCase() === tag.toLowerCase());\n  const label = entry?.label ?? tag.replace(/[[\\]]/g, '');\n  const emoji = entry?.emoji ?? '';\n  // Non-editable inline badge. Zero-width spaces around it let the\n  // caret sit on either side so the user can type before/after.\n  return `\\u200B<span ${BADGE_ATTR}=\"${tag}\" contenteditable=\"false\" class=\"ptag-badge\">${emoji ? `${emoji}\\u00A0` : ''}${label}</span>\\u200B`;\n}\n\n/** Convert plain text with [tag] patterns into HTML with badge spans. */\nfunction textToHTML(text: string): string {\n  // Escape HTML entities first\n  const escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n  // Replace tag patterns with badge HTML\n  return escaped.replace(TAG_REGEX, (match) => makeBadgeHTML(match));\n}\n\n/** Serialize the contentEditable innerHTML back to plain text with [tag] syntax. */\nfunction htmlToText(container: HTMLElement): string {\n  let result = '';\n  for (const node of container.childNodes) {\n    if (node.nodeType === Node.TEXT_NODE) {\n      // Strip zero-width spaces we added around badges\n      result += (node.textContent ?? '').replace(/\\u200B/g, '');\n    } else if (node.nodeType === Node.ELEMENT_NODE) {\n      const el = node as HTMLElement;\n      if (el.hasAttribute(BADGE_ATTR)) {\n        result += el.getAttribute(BADGE_ATTR) ?? '';\n      } else if (el.tagName === 'BR') {\n        result += '\\n';\n      } else {\n        // Recurse for nested elements (e.g. spans from paste)\n        result += htmlToText(el);\n      }\n    }\n  }\n  return result;\n}\n\n/** Get the text content from the current caret position back to the last\n *  whitespace or start of container, to detect the \"/\" trigger. */\nfunction getWordBeforeCaret(_container: HTMLElement): { word: string; range: Range | null } {\n  const sel = window.getSelection();\n  if (!sel || sel.rangeCount === 0) return { word: '', range: null };\n  const range = sel.getRangeAt(0).cloneRange();\n  range.collapse(true);\n\n  // Walk backwards from caret through the text node\n  const textNode = range.startContainer;\n  if (textNode.nodeType !== Node.TEXT_NODE) return { word: '', range: null };\n  const text = textNode.textContent ?? '';\n  const offset = range.startOffset;\n\n  let start = offset;\n  while (\n    start > 0 &&\n    text[start - 1] !== ' ' &&\n    text[start - 1] !== '\\n' &&\n    text[start - 1] !== '\\u00A0'\n  ) {\n    start--;\n  }\n\n  const word = text.slice(start, offset);\n  const wordRange = document.createRange();\n  wordRange.setStart(textNode, start);\n  wordRange.setEnd(textNode, offset);\n\n  return { word, range: wordRange };\n}\n\n// ── Component ───────────────────────────────────────────────────────\n\nexport interface ParalinguisticInputProps {\n  value?: string;\n  onChange?: (value: string) => void;\n  placeholder?: string;\n  disabled?: boolean;\n  className?: string;\n  style?: React.CSSProperties;\n  onClick?: () => void;\n  onFocus?: () => void;\n}\n\nexport interface ParalinguisticInputRef {\n  focus: () => void;\n  element: HTMLDivElement | null;\n}\n\nexport const ParalinguisticInput = forwardRef<ParalinguisticInputRef, ParalinguisticInputProps>(\n  function ParalinguisticInput(\n    { value, onChange, placeholder, disabled, className, style, onClick, onFocus },\n    ref,\n  ) {\n    const editorRef = useRef<HTMLDivElement>(null);\n    const [showMenu, setShowMenu] = useState(false);\n    const [menuFilter, setMenuFilter] = useState('');\n    const [menuIndex, setMenuIndex] = useState(0);\n    const [menuPosition, setMenuPosition] = useState<{ bottom: number; left: number }>({\n      bottom: 0,\n      left: 0,\n    });\n    const triggerRangeRef = useRef<Range | null>(null);\n    const lastSerializedRef = useRef<string>('');\n    const isComposingRef = useRef(false);\n\n    useImperativeHandle(ref, () => ({\n      focus: () => editorRef.current?.focus(),\n      element: editorRef.current,\n    }));\n\n    // Filtered tag list for the autocomplete menu\n    const filteredTags = PARALINGUISTIC_TAGS.filter((t) =>\n      t.label.toLowerCase().includes(menuFilter.toLowerCase()),\n    );\n\n    // ── Sync external value → editor ──────────────────────────────\n    useEffect(() => {\n      const el = editorRef.current;\n      if (!el) return;\n      // Only update DOM if the external value differs from what we last emitted\n      if (value !== undefined && value !== lastSerializedRef.current) {\n        lastSerializedRef.current = value;\n        el.innerHTML = value ? textToHTML(value) : '';\n      }\n    }, [value]);\n\n    // ── Emit plain-text value on input ────────────────────────────\n    const emitChange = useCallback(() => {\n      const el = editorRef.current;\n      if (!el || !onChange) return;\n      const text = htmlToText(el);\n      lastSerializedRef.current = text;\n      onChange(text);\n    }, [onChange]);\n\n    // ── Insert a tag badge at the caret ───────────────────────────\n    const insertTag = useCallback(\n      (tag: string) => {\n        const el = editorRef.current;\n        if (!el) return;\n\n        // Delete the /filter text\n        const wordRange = triggerRangeRef.current;\n        if (wordRange) {\n          wordRange.deleteContents();\n        }\n\n        // Insert badge HTML\n        const temp = document.createElement('span');\n        temp.innerHTML = makeBadgeHTML(tag);\n        const frag = document.createDocumentFragment();\n        let lastNode: Node | null = null;\n        while (temp.firstChild) {\n          lastNode = frag.appendChild(temp.firstChild);\n        }\n\n        const sel = window.getSelection();\n        if (sel && sel.rangeCount > 0) {\n          const range = sel.getRangeAt(0);\n          range.deleteContents();\n          range.insertNode(frag);\n\n          // Move caret after the badge\n          if (lastNode) {\n            const newRange = document.createRange();\n            newRange.setStartAfter(lastNode);\n            newRange.collapse(true);\n            sel.removeAllRanges();\n            sel.addRange(newRange);\n          }\n        }\n\n        setShowMenu(false);\n        setMenuFilter('');\n        emitChange();\n        el.focus();\n      },\n      [emitChange],\n    );\n\n    // ── Handle keydown for autocomplete navigation ────────────────\n    const handleKeyDown = useCallback(\n      (e: React.KeyboardEvent) => {\n        if (showMenu) {\n          if (filteredTags.length === 0) {\n            if (e.key === 'Escape') {\n              e.preventDefault();\n              setShowMenu(false);\n            }\n            return;\n          }\n          if (e.key === 'ArrowDown') {\n            e.preventDefault();\n            setMenuIndex((i) => (i + 1) % filteredTags.length);\n          } else if (e.key === 'ArrowUp') {\n            e.preventDefault();\n            setMenuIndex((i) => (i - 1 + filteredTags.length) % filteredTags.length);\n          } else if (e.key === 'Enter' || e.key === 'Tab') {\n            e.preventDefault();\n            if (filteredTags[menuIndex]) {\n              insertTag(filteredTags[menuIndex].tag);\n            }\n          } else if (e.key === 'Escape') {\n            e.preventDefault();\n            setShowMenu(false);\n          }\n        } else {\n          // Prevent Enter from creating <div> blocks in contentEditable\n          if (e.key === 'Enter' && !e.shiftKey) {\n            // Let the form handle submit\n          }\n        }\n      },\n      [showMenu, filteredTags, menuIndex, insertTag],\n    );\n\n    // ── Handle input (check for / trigger) ────────────────────────\n    const handleInput = useCallback(() => {\n      if (isComposingRef.current) return;\n      const el = editorRef.current;\n      if (!el) return;\n\n      const { word, range } = getWordBeforeCaret(el);\n\n      if (word.startsWith('/')) {\n        const filter = word.slice(1); // strip the /\n        setMenuFilter(filter);\n        setMenuIndex(0);\n        triggerRangeRef.current = range;\n\n        // Position the menu above the caret using viewport coords (portalled)\n        const sel = window.getSelection();\n        if (sel && sel.rangeCount > 0) {\n          const rect = sel.getRangeAt(0).getBoundingClientRect();\n          setMenuPosition({\n            bottom: window.innerHeight - rect.top + 4,\n            left: rect.left,\n          });\n        }\n\n        setShowMenu(true);\n      } else {\n        setShowMenu(false);\n      }\n\n      emitChange();\n    }, [emitChange]);\n\n    // ── Handle paste — convert [tag] patterns to badges ───────────\n    const handlePaste = useCallback(\n      (e: React.ClipboardEvent) => {\n        e.preventDefault();\n        const text = e.clipboardData.getData('text/plain');\n        if (!text) return;\n\n        const el = editorRef.current;\n        if (!el) return;\n\n        const html = textToHTML(text);\n\n        // Insert at caret\n        const sel = window.getSelection();\n        if (sel && sel.rangeCount > 0) {\n          const range = sel.getRangeAt(0);\n          range.deleteContents();\n          const temp = document.createElement('div');\n          temp.innerHTML = html;\n          const frag = document.createDocumentFragment();\n          let lastNode: Node | null = null;\n          while (temp.firstChild) {\n            lastNode = frag.appendChild(temp.firstChild);\n          }\n          range.insertNode(frag);\n          if (lastNode) {\n            const newRange = document.createRange();\n            newRange.setStartAfter(lastNode);\n            newRange.collapse(true);\n            sel.removeAllRanges();\n            sel.addRange(newRange);\n          }\n        }\n\n        emitChange();\n      },\n      [emitChange],\n    );\n\n    // ── Show placeholder ──────────────────────────────────────────\n    const isEmpty = !value || value.trim() === '';\n\n    return (\n      <div className=\"relative\">\n        {/* Placeholder */}\n        {isEmpty && placeholder && (\n          <div\n            className=\"pointer-events-none absolute inset-0 text-sm text-muted-foreground/60 px-3 py-2 select-none\"\n            aria-hidden\n          >\n            {placeholder}\n          </div>\n        )}\n\n        {/* Editable area */}\n        <div\n          ref={editorRef}\n          contentEditable={!disabled}\n          suppressContentEditableWarning\n          role={disabled ? undefined : 'textbox'}\n          aria-multiline={disabled ? undefined : true}\n          aria-placeholder={placeholder}\n          aria-disabled={disabled}\n          tabIndex={disabled ? -1 : 0}\n          className={cn(\n            'min-h-[32px] text-sm whitespace-pre-wrap break-words outline-none',\n            '[&_.ptag-badge]:inline-flex [&_.ptag-badge]:items-center [&_.ptag-badge]:rounded-full',\n            '[&_.ptag-badge]:bg-accent/20 [&_.ptag-badge]:text-accent [&_.ptag-badge]:border [&_.ptag-badge]:border-accent/30',\n            '[&_.ptag-badge]:px-2 [&_.ptag-badge]:py-0 [&_.ptag-badge]:text-xs [&_.ptag-badge]:font-medium',\n            '[&_.ptag-badge]:mx-0.5 [&_.ptag-badge]:select-none [&_.ptag-badge]:cursor-default',\n            '[&_.ptag-badge]:align-baseline',\n            disabled && 'opacity-50 cursor-not-allowed',\n            className,\n          )}\n          style={style}\n          onInput={!disabled ? handleInput : undefined}\n          onKeyDown={!disabled ? handleKeyDown : undefined}\n          onPaste={!disabled ? handlePaste : undefined}\n          onClick={!disabled ? onClick : undefined}\n          onFocus={!disabled ? onFocus : undefined}\n          onBlur={() => {\n            setShowMenu(false);\n            triggerRangeRef.current = null;\n          }}\n          onCompositionStart={() => {\n            isComposingRef.current = true;\n          }}\n          onCompositionEnd={() => {\n            isComposingRef.current = false;\n            handleInput();\n          }}\n        />\n\n        {/* Autocomplete dropdown — portalled to body, positioned above the caret */}\n        {showMenu &&\n          filteredTags.length > 0 &&\n          createPortal(\n            <AnimatePresence>\n              <motion.div\n                initial={{ opacity: 0, y: 4 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: 4 }}\n                transition={{ duration: 0.12 }}\n                className=\"fixed z-[9999] min-w-[200px] max-h-[280px] overflow-y-auto rounded-lg border border-border bg-popover shadow-lg\"\n                style={{\n                  bottom: menuPosition.bottom,\n                  left: menuPosition.left,\n                }}\n              >\n                {filteredTags.map((t, i) => (\n                  <button\n                    key={t.tag}\n                    type=\"button\"\n                    className={cn(\n                      'flex items-center gap-2 w-full px-3 py-1.5 text-sm text-left transition-colors',\n                      i === menuIndex\n                        ? 'bg-accent/20 text-accent-foreground'\n                        : 'text-popover-foreground hover:bg-muted/50',\n                    )}\n                    onMouseDown={(e) => {\n                      e.preventDefault(); // Keep focus in editor\n                      insertTag(t.tag);\n                    }}\n                    onMouseEnter={() => setMenuIndex(i)}\n                  >\n                    <span className=\"text-base leading-none\">{t.emoji}</span>\n                    <span>{t.label}</span>\n                    <span className=\"ml-auto text-xs text-muted-foreground font-mono\">{t.tag}</span>\n                  </button>\n                ))}\n              </motion.div>\n            </AnimatePresence>,\n            document.body,\n          )}\n      </div>\n    );\n  },\n);\n"
  },
  {
    "path": "app/src/components/History/HistoryTable.tsx",
    "content": "import { useQueryClient } from '@tanstack/react-query';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport {\n  AlignCenter,\n  AudioLines,\n  AudioWaveform,\n  Download,\n  FileArchive,\n  Loader2,\n  MoreHorizontal,\n  Play,\n  RotateCcw,\n  Star,\n  Trash2,\n  Wand2,\n} from 'lucide-react';\nimport { useEffect, useRef, useState } from 'react';\n\nimport { EffectsChainEditor } from '@/components/Effects/EffectsChainEditor';\nimport { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { Textarea } from '@/components/ui/textarea';\nimport { useToast } from '@/components/ui/use-toast';\nimport { apiClient } from '@/lib/api/client';\nimport type { EffectConfig, GenerationVersionResponse, HistoryResponse } from '@/lib/api/types';\nimport { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';\nimport {\n  useDeleteGeneration,\n  useExportGeneration,\n  useExportGenerationAudio,\n  useHistory,\n  useImportGeneration,\n} from '@/lib/hooks/useHistory';\nimport { cn } from '@/lib/utils/cn';\nimport { formatDate, formatDuration, formatEngineName } from '@/lib/utils/format';\nimport { useGenerationStore } from '@/stores/generationStore';\nimport { usePlayerStore } from '@/stores/playerStore';\n\n// ─── Audio Bars ─────────────────────────────────────────────────────────────\n\nfunction AudioBars({ mode }: { mode: 'idle' | 'generating' | 'playing' }) {\n  const barColor = mode !== 'idle' ? 'bg-accent' : 'bg-muted-foreground/40';\n  return (\n    <div className=\"flex items-center gap-[2px] h-5\">\n      {[0, 1, 2, 3, 4].map((i) => (\n        <motion.div\n          key={`${mode}-${i}`}\n          className={`w-[3px] rounded-full ${barColor}`}\n          animate={\n            mode === 'generating'\n              ? { height: ['6px', '16px', '6px'] }\n              : mode === 'playing'\n                ? { height: ['8px', '14px', '4px', '12px', '8px'] }\n                : { height: '8px' }\n          }\n          transition={\n            mode === 'generating'\n              ? { duration: 0.6, repeat: Infinity, delay: i * 0.08, ease: 'easeInOut' }\n              : mode === 'playing'\n                ? { duration: 1.2, repeat: Infinity, delay: i * 0.15, ease: 'easeInOut' }\n                : { duration: 0.4, ease: 'easeOut' }\n          }\n        />\n      ))}\n    </div>\n  );\n}\n\n// NEW ALTERNATE HISTORY VIEW - FIXED HEIGHT ROWS WITH INFINITE SCROLL\nexport function HistoryTable() {\n  const [page, setPage] = useState(0);\n  const [allHistory, setAllHistory] = useState<HistoryResponse[]>([]);\n  const [total, setTotal] = useState(0);\n  const [isScrolled, setIsScrolled] = useState(false);\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const loadMoreRef = useRef<HTMLDivElement>(null);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [importDialogOpen, setImportDialogOpen] = useState(false);\n  const [selectedFile, setSelectedFile] = useState<File | null>(null);\n  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);\n  const [generationToDelete, setGenerationToDelete] = useState<{ id: string; name: string } | null>(\n    null,\n  );\n  const [effectsDialogOpen, setEffectsDialogOpen] = useState(false);\n  const [effectsTargetId, setEffectsTargetId] = useState<string | null>(null);\n  const [effectsTargetVersions, setEffectsTargetVersions] = useState<GenerationVersionResponse[]>(\n    [],\n  );\n  const [effectsSourceVersionId, setEffectsSourceVersionId] = useState<string | null>(null);\n  const [effectsChain, setEffectsChain] = useState<EffectConfig[]>([]);\n  const [applyingEffects, setApplyingEffects] = useState(false);\n  const [expandedVersionsId, setExpandedVersionsId] = useState<string | null>(null);\n  const limit = 20;\n  const { toast } = useToast();\n  const queryClient = useQueryClient();\n\n  const {\n    data: historyData,\n    isLoading,\n    isFetching,\n  } = useHistory({\n    limit,\n    offset: page * limit,\n  });\n\n  const deleteGeneration = useDeleteGeneration();\n  const exportGeneration = useExportGeneration();\n  const exportGenerationAudio = useExportGenerationAudio();\n  const importGeneration = useImportGeneration();\n  const addPendingGeneration = useGenerationStore((state) => state.addPendingGeneration);\n  const setAudioWithAutoPlay = usePlayerStore((state) => state.setAudioWithAutoPlay);\n  const restartCurrentAudio = usePlayerStore((state) => state.restartCurrentAudio);\n  const currentAudioId = usePlayerStore((state) => state.audioId);\n  const isPlaying = usePlayerStore((state) => state.isPlaying);\n  const audioUrl = usePlayerStore((state) => state.audioUrl);\n  const isPlayerVisible = !!audioUrl;\n\n  // Update accumulated history when new data arrives\n  useEffect(() => {\n    if (historyData?.items) {\n      setTotal(historyData.total);\n      if (page === 0) {\n        // Reset to first page\n        setAllHistory(historyData.items);\n      } else {\n        // Append new items, avoiding duplicates\n        setAllHistory((prev) => {\n          const existingIds = new Set(prev.map((item) => item.id));\n          const newItems = historyData.items.filter((item) => !existingIds.has(item.id));\n          return [...prev, ...newItems];\n        });\n      }\n    }\n  }, [historyData, page]);\n\n  // Reset to page 0 when deletions, imports, or generation completions occur\n  const pendingCount = useGenerationStore((state) => state.pendingGenerationIds.size);\n  const prevPendingCountRef = useRef(pendingCount);\n  useEffect(() => {\n    if (deleteGeneration.isSuccess || importGeneration.isSuccess) {\n      setPage(0);\n      setAllHistory([]);\n    }\n  }, [deleteGeneration.isSuccess, importGeneration.isSuccess]);\n\n  useEffect(() => {\n    // A generation finished (pending count decreased) — scroll back to show it\n    if (\n      prevPendingCountRef.current > 0 &&\n      pendingCount < prevPendingCountRef.current &&\n      page !== 0\n    ) {\n      setPage(0);\n      setAllHistory([]);\n    }\n    prevPendingCountRef.current = pendingCount;\n  }, [pendingCount, page]);\n\n  // Intersection Observer for infinite scroll\n  useEffect(() => {\n    const loadMoreEl = loadMoreRef.current;\n    if (!loadMoreEl) return;\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        const target = entries[0];\n        if (target.isIntersecting && !isFetching && allHistory.length < total) {\n          setPage((prev) => prev + 1);\n        }\n      },\n      {\n        root: scrollRef.current,\n        rootMargin: '100px',\n        threshold: 0.1,\n      },\n    );\n\n    observer.observe(loadMoreEl);\n    return () => observer.disconnect();\n  }, [isFetching, allHistory.length, total]);\n\n  // Track scroll position for gradient effect\n  useEffect(() => {\n    const scrollEl = scrollRef.current;\n    if (!scrollEl) return;\n\n    const handleScroll = () => {\n      setIsScrolled(scrollEl.scrollTop > 0);\n    };\n\n    scrollEl.addEventListener('scroll', handleScroll);\n    return () => scrollEl.removeEventListener('scroll', handleScroll);\n  }, []);\n\n  const handlePlay = (audioId: string, text: string, profileId: string) => {\n    // If clicking the same audio, restart it from the beginning\n    if (currentAudioId === audioId) {\n      restartCurrentAudio();\n    } else {\n      // Otherwise, load the new audio and auto-play it\n      const audioUrl = apiClient.getAudioUrl(audioId);\n      setAudioWithAutoPlay(audioUrl, audioId, profileId, text.substring(0, 50));\n    }\n  };\n\n  const handleDownloadAudio = (generationId: string, text: string) => {\n    exportGenerationAudio.mutate(\n      { generationId, text },\n      {\n        onError: (error) => {\n          toast({\n            title: 'Failed to download audio',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  };\n\n  const handleExportPackage = (generationId: string, text: string) => {\n    exportGeneration.mutate(\n      { generationId, text },\n      {\n        onError: (error) => {\n          toast({\n            title: 'Failed to export generation',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  };\n\n  const handleDeleteClick = (generationId: string, profileName: string) => {\n    setGenerationToDelete({ id: generationId, name: profileName });\n    setDeleteDialogOpen(true);\n  };\n\n  const handleDeleteConfirm = () => {\n    if (generationToDelete) {\n      deleteGeneration.mutate(generationToDelete.id);\n      setDeleteDialogOpen(false);\n      setGenerationToDelete(null);\n    }\n  };\n\n  const handleRetry = async (generationId: string) => {\n    try {\n      const result = await apiClient.retryGeneration(generationId);\n      addPendingGeneration(result.id);\n      queryClient.invalidateQueries({ queryKey: ['history'] });\n    } catch (error) {\n      toast({\n        title: 'Retry failed',\n        description: error instanceof Error ? error.message : 'Could not retry generation',\n        variant: 'destructive',\n      });\n    }\n  };\n\n  const handleRegenerate = async (generationId: string) => {\n    try {\n      await apiClient.regenerateGeneration(generationId);\n      addPendingGeneration(generationId);\n      queryClient.invalidateQueries({ queryKey: ['history'] });\n    } catch (error) {\n      toast({\n        title: 'Regenerate failed',\n        description: error instanceof Error ? error.message : 'Could not regenerate',\n        variant: 'destructive',\n      });\n    }\n  };\n\n  const handleToggleFavorite = async (generationId: string) => {\n    try {\n      await apiClient.toggleFavorite(generationId);\n      queryClient.invalidateQueries({ queryKey: ['history'] });\n    } catch (error) {\n      toast({\n        title: 'Failed to update favorite',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    }\n  };\n\n  const handleApplyEffects = (generationId: string) => {\n    const gen = allHistory.find((g) => g.id === generationId);\n    const versions = gen?.versions ?? [];\n    setEffectsTargetId(generationId);\n    setEffectsTargetVersions(versions);\n    // Default to clean/original version (no effects chain)\n    const cleanVersion = versions.find((v) => !v.effects_chain || v.effects_chain.length === 0);\n    setEffectsSourceVersionId(cleanVersion?.id ?? null);\n    setEffectsChain([]);\n    setEffectsDialogOpen(true);\n  };\n\n  const handleApplyEffectsConfirm = async () => {\n    if (!effectsTargetId || effectsChain.length === 0) return;\n    setApplyingEffects(true);\n    try {\n      const newVersion = await apiClient.applyEffectsToGeneration(effectsTargetId, {\n        effects_chain: effectsChain,\n        source_version_id: effectsSourceVersionId ?? undefined,\n        set_as_default: true,\n      });\n      queryClient.invalidateQueries({ queryKey: ['history'] });\n\n      // If the player is currently on this generation, reload with the new version audio\n      if (currentAudioId === effectsTargetId) {\n        const gen = allHistory.find((g) => g.id === effectsTargetId);\n        if (gen) {\n          const versionUrl = apiClient.getVersionAudioUrl(newVersion.id);\n          setAudioWithAutoPlay(\n            versionUrl,\n            effectsTargetId,\n            gen.profile_id,\n            gen.text.substring(0, 50),\n          );\n        }\n      }\n\n      setEffectsDialogOpen(false);\n      toast({ title: 'Effects applied', description: 'A new version has been created.' });\n    } catch (error) {\n      toast({\n        title: 'Failed to apply effects',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    } finally {\n      setApplyingEffects(false);\n    }\n  };\n\n  const handleSwitchVersion = async (generationId: string, versionId: string) => {\n    try {\n      await apiClient.setDefaultVersion(generationId, versionId);\n      queryClient.invalidateQueries({ queryKey: ['history'] });\n    } catch (error) {\n      toast({\n        title: 'Failed to switch version',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    }\n  };\n\n  const handlePlayVersion = (\n    generationId: string,\n    versionId: string,\n    text: string,\n    profileId: string,\n  ) => {\n    const audioUrl = apiClient.getVersionAudioUrl(versionId);\n    setAudioWithAutoPlay(audioUrl, generationId, profileId, text.substring(0, 50));\n  };\n\n  const handleImportConfirm = () => {\n    if (selectedFile) {\n      importGeneration.mutate(selectedFile, {\n        onSuccess: (data) => {\n          setImportDialogOpen(false);\n          setSelectedFile(null);\n          if (fileInputRef.current) {\n            fileInputRef.current.value = '';\n          }\n          toast({\n            title: 'Generation imported',\n            description: data.message || 'Generation imported successfully',\n          });\n        },\n        onError: (error) => {\n          toast({\n            title: 'Failed to import generation',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      });\n    }\n  };\n\n  if (isLoading && page === 0) {\n    return (\n      <div className=\"flex items-center justify-center h-full\">\n        <Loader2 className=\"h-8 w-8 animate-spin text-muted-foreground\" />\n      </div>\n    );\n  }\n\n  const history = allHistory;\n  const hasMore = allHistory.length < total;\n\n  return (\n    <div className=\"flex flex-col h-full min-h-0 relative\">\n      {history.length === 0 ? (\n        <div className=\"text-center py-12 px-5 border-2 border-dashed mb-5 border-muted rounded-md text-muted-foreground flex-1 flex items-center justify-center\">\n          No voice generations, yet...\n        </div>\n      ) : (\n        <>\n          {isScrolled && (\n            <div className=\"absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-background to-transparent z-10 pointer-events-none\" />\n          )}\n          <div\n            ref={scrollRef}\n            className={cn(\n              'flex-1 min-h-0 overflow-y-auto space-y-2 pb-4',\n              isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,\n            )}\n          >\n            {history.map((gen) => {\n              const isCurrentlyPlaying = currentAudioId === gen.id && isPlaying;\n              const isInProgress = gen.status === 'loading_model' || gen.status === 'generating';\n              const isGenerating = isInProgress;\n              const isFailed = gen.status === 'failed';\n              const isPlayable = !isGenerating && !isFailed;\n              const hasVersions = gen.versions && gen.versions.length > 1;\n              const isVersionsExpanded = expandedVersionsId === gen.id;\n              return (\n                <div\n                  key={gen.id}\n                  className={cn(\n                    'border rounded-md bg-card transition-colors text-left w-full',\n                    isCurrentlyPlaying && 'bg-muted/70',\n                  )}\n                >\n                  {/* Main row */}\n                  <div\n                    role={isPlayable ? 'button' : undefined}\n                    tabIndex={isPlayable ? 0 : undefined}\n                    className={cn(\n                      'flex items-stretch gap-4 h-26 p-3 outline-none',\n                      isPlayable && 'hover:bg-muted/70 cursor-pointer rounded-md',\n                      isVersionsExpanded && 'rounded-b-none',\n                    )}\n                    aria-label={\n                      isGenerating\n                        ? `Generating speech for ${gen.profile_name}...`\n                        : isFailed\n                          ? `Generation failed for ${gen.profile_name}`\n                          : isCurrentlyPlaying\n                            ? `Sample from ${gen.profile_name}, ${formatDuration(gen.duration ?? 0)}, ${formatDate(gen.created_at)}. Playing. Press Enter to restart.`\n                            : `Sample from ${gen.profile_name}, ${formatDuration(gen.duration ?? 0)}, ${formatDate(gen.created_at)}. Press Enter to play.`\n                    }\n                    onMouseDown={(e) => {\n                      if (!isPlayable) return;\n                      const target = e.target as HTMLElement;\n                      if (target.closest('textarea') || window.getSelection()?.toString()) {\n                        return;\n                      }\n                      handlePlay(gen.id, gen.text, gen.profile_id);\n                    }}\n                    onKeyDown={(e) => {\n                      if (!isPlayable) return;\n                      const target = e.target as HTMLElement;\n                      if (target.closest('textarea') || target.closest('button')) return;\n                      if (e.key === 'Enter' || e.key === ' ') {\n                        e.preventDefault();\n                        handlePlay(gen.id, gen.text, gen.profile_id);\n                      }\n                    }}\n                  >\n                    {/* Status icon */}\n                    <div className=\"flex items-center shrink-0 w-10 justify-center overflow-hidden\">\n                      <AudioBars\n                        mode={isGenerating ? 'generating' : isCurrentlyPlaying ? 'playing' : 'idle'}\n                      />\n                    </div>\n\n                    {/* Left side - Meta information */}\n                    <div className=\"flex flex-col gap-1.5 w-48 shrink-0 justify-center\">\n                      <div className=\"font-medium text-sm truncate\" title={gen.profile_name}>\n                        {gen.profile_name}\n                      </div>\n                      <div className=\"flex items-center gap-2\">\n                        <span className=\"text-xs text-muted-foreground\">{gen.language}</span>\n                        <span className=\"text-xs text-muted-foreground\">\n                          {formatEngineName(gen.engine, gen.model_size)}\n                        </span>\n                        {isFailed ? (\n                          <span className=\"text-xs text-destructive\">Failed</span>\n                        ) : !isGenerating ? (\n                          <span className=\"text-xs text-muted-foreground\">\n                            {formatDuration(gen.duration ?? 0)}\n                          </span>\n                        ) : null}\n                      </div>\n                      <div className=\"text-xs text-muted-foreground\">\n                        {isInProgress ? (\n                          <span className=\"text-accent\">\n                            {gen.status === 'loading_model' ? 'Loading model...' : 'Generating...'}\n                          </span>\n                        ) : (\n                          formatDate(gen.created_at)\n                        )}\n                      </div>\n                    </div>\n\n                    {/* Right side - Transcript textarea */}\n                    <div className=\"flex-1 min-w-0 flex\">\n                      <Textarea\n                        value={gen.text}\n                        className=\"flex-1 resize-none text-sm text-muted-foreground select-text\"\n                        readOnly\n                        aria-label={`Transcript for sample from ${gen.profile_name}, ${formatDuration(gen.duration ?? 0)}`}\n                      />\n                    </div>\n\n                    {/* Far right - Actions */}\n                    <div\n                      className=\"shrink-0 flex flex-col justify-center items-center gap-0.5\"\n                      onMouseDown={(e) => e.stopPropagation()}\n                      onClick={(e) => e.stopPropagation()}\n                    >\n                      <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        className={cn(\n                          'h-6 w-6 text-muted-foreground/50 hover:bg-muted-foreground/20 hover:text-muted-foreground',\n                          gen.is_favorited && 'text-accent hover:text-accent',\n                        )}\n                        aria-label={gen.is_favorited ? 'Unfavorite' : 'Favorite'}\n                        onClick={() => handleToggleFavorite(gen.id)}\n                      >\n                        <Star\n                          className=\"h-2 w-2\"\n                          fill={gen.is_favorited ? 'currentColor' : 'none'}\n                        />\n                      </Button>\n                      {hasVersions && (\n                        <Button\n                          variant=\"ghost\"\n                          size=\"icon\"\n                          className={cn(\n                            'h-6 w-6 text-muted-foreground/50 hover:bg-muted-foreground/20 hover:text-muted-foreground',\n                            isVersionsExpanded && 'text-accent hover:text-accent',\n                          )}\n                          aria-label=\"Toggle versions\"\n                          onClick={() => setExpandedVersionsId(isVersionsExpanded ? null : gen.id)}\n                        >\n                          <AudioLines className=\"h-2 w-2\" />\n                        </Button>\n                      )}\n\n                      {isFailed ? (\n                        <>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            className=\"h-6 w-6 text-muted-foreground/50 hover:bg-muted-foreground/20 hover:text-muted-foreground\"\n                            aria-label=\"Retry generation\"\n                            onClick={() => handleRetry(gen.id)}\n                          >\n                            <RotateCcw className=\"h-2 w-2\" />\n                          </Button>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            className=\"h-6 w-6 text-muted-foreground/50 hover:bg-muted-foreground/20 hover:text-muted-foreground\"\n                            aria-label=\"Delete generation\"\n                            disabled={deleteGeneration.isPending}\n                            onClick={() => handleDeleteClick(gen.id, gen.profile_name)}\n                          >\n                            <Trash2 className=\"h-2 w-2\" />\n                          </Button>\n                        </>\n                      ) : (\n                        <>\n                          <DropdownMenu>\n                            <DropdownMenuTrigger asChild>\n                              <Button\n                                variant=\"ghost\"\n                                size=\"icon\"\n                                className=\"h-6 w-6 text-muted-foreground/50 hover:bg-muted-foreground/20 hover:text-muted-foreground\"\n                                aria-label=\"Actions\"\n                                disabled={isGenerating}\n                              >\n                                <MoreHorizontal className=\"h-2 w-2\" />\n                              </Button>\n                            </DropdownMenuTrigger>\n                            <DropdownMenuContent align=\"end\">\n                              <DropdownMenuItem\n                                onClick={() => handlePlay(gen.id, gen.text, gen.profile_id)}\n                              >\n                                <Play className=\"mr-2 h-4 w-4\" />\n                                Play\n                              </DropdownMenuItem>\n                              <DropdownMenuItem\n                                onClick={() => handleDownloadAudio(gen.id, gen.text)}\n                                disabled={exportGenerationAudio.isPending}\n                              >\n                                <Download className=\"mr-2 h-4 w-4\" />\n                                Export Audio\n                              </DropdownMenuItem>\n                              <DropdownMenuItem\n                                onClick={() => handleExportPackage(gen.id, gen.text)}\n                                disabled={exportGeneration.isPending}\n                              >\n                                <FileArchive className=\"mr-2 h-4 w-4\" />\n                                Export Package\n                              </DropdownMenuItem>\n                              <DropdownMenuItem onClick={() => handleApplyEffects(gen.id)}>\n                                <Wand2 className=\"mr-2 h-4 w-4\" />\n                                Apply Effects\n                              </DropdownMenuItem>\n                              <DropdownMenuItem onClick={() => handleRegenerate(gen.id)}>\n                                <RotateCcw className=\"mr-2 h-4 w-4\" />\n                                Regenerate\n                              </DropdownMenuItem>\n                              <DropdownMenuItem\n                                onClick={() => handleDeleteClick(gen.id, gen.profile_name)}\n                                disabled={deleteGeneration.isPending}\n                                // className=\"text-destructive focus:text-destructive\"\n                              >\n                                <Trash2 className=\"mr-2 h-4 w-4\" />\n                                Delete\n                              </DropdownMenuItem>\n                            </DropdownMenuContent>\n                          </DropdownMenu>\n                        </>\n                      )}\n                    </div>\n                  </div>\n\n                  {/* Expandable versions panel */}\n                  <AnimatePresence>\n                    {isVersionsExpanded && gen.versions && (\n                      <motion.div\n                        initial={{ height: 0, opacity: 0 }}\n                        animate={{ height: 'auto', opacity: 1 }}\n                        exit={{ height: 0, opacity: 0 }}\n                        transition={{ duration: 0.2, ease: 'easeOut' }}\n                        className=\"overflow-hidden\"\n                      >\n                        <div className=\"border-t border-border/50\">\n                          <div className=\"divide-y divide-border/40\">\n                            {gen.versions.map((v) => {\n                              // Show source provenance when effects were applied to a non-clean version\n                              const sourceVersion = v.source_version_id\n                                ? gen.versions?.find((sv) => sv.id === v.source_version_id)\n                                : null;\n                              const showSource =\n                                sourceVersion &&\n                                sourceVersion.effects_chain &&\n                                sourceVersion.effects_chain.length > 0;\n\n                              return (\n                                <button\n                                  key={v.id}\n                                  type=\"button\"\n                                  className=\"flex items-center gap-2 w-full h-9 px-3 text-left hover:bg-muted/50 transition-colors\"\n                                  onClick={() => {\n                                    handlePlayVersion(gen.id, v.id, gen.text, gen.profile_id);\n                                    if (!v.is_default) {\n                                      handleSwitchVersion(gen.id, v.id);\n                                    }\n                                  }}\n                                >\n                                  <AudioLines className=\"h-3 w-3 shrink-0 text-muted-foreground\" />\n                                  <span className=\"truncate text-xs font-medium\">{v.label}</span>\n                                  {v.effects_chain && v.effects_chain.length > 0 && (\n                                    <span className=\"text-[10px] text-muted-foreground truncate\">\n                                      {v.effects_chain.map((e) => e.type).join(' → ')}\n                                    </span>\n                                  )}\n                                  {showSource && (\n                                    <span className=\"text-[10px] text-muted-foreground/60 truncate\">\n                                      from {sourceVersion.label}\n                                    </span>\n                                  )}\n                                  <span className=\"flex-1\" />\n                                  {v.is_default && (\n                                    <span className=\"text-[10px] bg-accent/15 text-accent px-1.5 py-0.5 rounded-full\">\n                                      active\n                                    </span>\n                                  )}\n                                </button>\n                              );\n                            })}\n                          </div>\n                        </div>\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n                </div>\n              );\n            })}\n\n            {/* Load more trigger element */}\n            {hasMore && (\n              <div ref={loadMoreRef} className=\"flex items-center justify-center py-4\">\n                {isFetching && <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />}\n              </div>\n            )}\n\n            {/* End of list indicator */}\n            {!hasMore && history.length > 0 && (\n              <div className=\"text-center py-4 text-xs text-muted-foreground\">\n                You've reached the end\n              </div>\n            )}\n          </div>\n        </>\n      )}\n\n      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Delete Generation</DialogTitle>\n            <DialogDescription>\n              Are you sure you want to delete this generation from \"{generationToDelete?.name}\"?\n              This action cannot be undone.\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => {\n                setDeleteDialogOpen(false);\n                setGenerationToDelete(null);\n              }}\n            >\n              Cancel\n            </Button>\n            <Button\n              variant=\"destructive\"\n              onClick={handleDeleteConfirm}\n              disabled={deleteGeneration.isPending}\n            >\n              {deleteGeneration.isPending ? 'Deleting...' : 'Delete'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      <Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Import Generation</DialogTitle>\n            <DialogDescription>\n              Import the generation from \"{selectedFile?.name}\". This will add it to your history.\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => {\n                setImportDialogOpen(false);\n                setSelectedFile(null);\n                if (fileInputRef.current) {\n                  fileInputRef.current.value = '';\n                }\n              }}\n            >\n              Cancel\n            </Button>\n            <Button\n              onClick={handleImportConfirm}\n              disabled={importGeneration.isPending || !selectedFile}\n            >\n              {importGeneration.isPending ? 'Importing...' : 'Import'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      <Dialog open={effectsDialogOpen} onOpenChange={setEffectsDialogOpen}>\n        <DialogContent className=\"max-w-md\">\n          <DialogHeader>\n            <DialogTitle>Apply Effects</DialogTitle>\n            <DialogDescription>\n              Configure post-processing effects to apply to this generation. A new version will be\n              created.\n            </DialogDescription>\n          </DialogHeader>\n          {effectsTargetVersions.length > 1 && (\n            <div className=\"space-y-1.5\">\n              <label className=\"text-xs font-medium text-muted-foreground\">Source</label>\n              <Select\n                value={effectsSourceVersionId ?? ''}\n                onValueChange={(val) => setEffectsSourceVersionId(val || null)}\n              >\n                <SelectTrigger className=\"h-8 text-xs\">\n                  <SelectValue placeholder=\"Select source version\" />\n                </SelectTrigger>\n                <SelectContent>\n                  {effectsTargetVersions.map((v) => (\n                    <SelectItem key={v.id} value={v.id} className=\"text-xs\">\n                      {v.label}\n                      {v.effects_chain && v.effects_chain.length > 0 && (\n                        <span className=\"text-muted-foreground ml-1.5\">\n                          ({v.effects_chain.map((e) => e.type).join(' + ')})\n                        </span>\n                      )}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n          )}\n          <div className=\"py-2 max-h-80 overflow-y-auto\">\n            <EffectsChainEditor value={effectsChain} onChange={setEffectsChain} />\n          </div>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setEffectsDialogOpen(false)}>\n              Cancel\n            </Button>\n            <Button\n              onClick={handleApplyEffectsConfirm}\n              disabled={applyingEffects || effectsChain.length === 0}\n            >\n              {applyingEffects ? 'Applying...' : 'Apply'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/MainEditor/MainEditor.tsx",
    "content": "import { Sparkles, Upload } from 'lucide-react';\nimport { useRef, useState } from 'react';\nimport { FloatingGenerateBox } from '@/components/Generation/FloatingGenerateBox';\nimport { HistoryTable } from '@/components/History/HistoryTable';\nimport { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport { useToast } from '@/components/ui/use-toast';\nimport { ProfileList } from '@/components/VoiceProfiles/ProfileList';\n\nimport { useImportProfile } from '@/lib/hooks/useProfiles';\nimport { cn } from '@/lib/utils/cn';\nimport { usePlayerStore } from '@/stores/playerStore';\nimport { useUIStore } from '@/stores/uiStore';\n\nexport function MainEditor() {\n  const audioUrl = usePlayerStore((state) => state.audioUrl);\n  const isPlayerVisible = !!audioUrl;\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const setDialogOpen = useUIStore((state) => state.setProfileDialogOpen);\n  const importProfile = useImportProfile();\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [importDialogOpen, setImportDialogOpen] = useState(false);\n  const [selectedFile, setSelectedFile] = useState<File | null>(null);\n  const { toast } = useToast();\n\n  const handleImportClick = () => {\n    fileInputRef.current?.click();\n  };\n\n  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (file) {\n      if (!file.name.endsWith('.voicebox.zip')) {\n        toast({\n          title: 'Invalid file type',\n          description: 'Please select a valid .voicebox.zip file',\n          variant: 'destructive',\n        });\n        return;\n      }\n      setSelectedFile(file);\n      setImportDialogOpen(true);\n    }\n  };\n\n  const handleImportConfirm = () => {\n    if (selectedFile) {\n      importProfile.mutate(selectedFile, {\n        onSuccess: () => {\n          setImportDialogOpen(false);\n          setSelectedFile(null);\n          if (fileInputRef.current) {\n            fileInputRef.current.value = '';\n          }\n          toast({\n            title: 'Profile imported',\n            description: 'Voice profile imported successfully',\n          });\n        },\n        onError: (error) => {\n          toast({\n            title: 'Failed to import profile',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      });\n    }\n  };\n\n  return (\n    // Main view: Profiles top left, Generator bottom left, History right\n    <div className=\"grid grid-cols-1 lg:grid-cols-2 lg:gap-6 h-full min-h-0 overflow-hidden relative\">\n      {/* Left Column */}\n      <div className=\"flex flex-col min-h-0 overflow-hidden relative lg:overflow-hidden\">\n        {/* Scroll Mask - Always visible, behind content */}\n        <div className=\"absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-background to-transparent z-0 pointer-events-none\" />\n\n        {/* Fixed Header */}\n        <div className=\"absolute top-0 left-0 right-0 z-10\">\n          <div className=\"flex items-center justify-between mb-4 px-1\">\n            <h2 className=\"text-2xl font-bold\">Voicebox</h2>\n            <div className=\"flex gap-2\">\n              <Button variant=\"outline\" onClick={handleImportClick}>\n                <Upload className=\"mr-2 h-4 w-4\" />\n                Import Voice\n              </Button>\n              <input\n                ref={fileInputRef}\n                type=\"file\"\n                accept=\".voicebox.zip\"\n                onChange={handleFileChange}\n                className=\"hidden\"\n              />\n              <Button onClick={() => setDialogOpen(true)}>\n                <Sparkles className=\"mr-2 h-4 w-4\" />\n                Create Voice\n              </Button>\n            </div>\n          </div>\n        </div>\n\n        {/* Scrollable Content */}\n        <div\n          ref={scrollRef}\n          className={cn('flex-1 min-h-0 overflow-y-auto pt-14 pb-4', isPlayerVisible && 'lg:pb-32')}\n        >\n          <div className=\"flex flex-col gap-6\">\n            <div className=\"shrink-0 flex flex-col\">\n              <ProfileList />\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Divider - single column only */}\n      {/* <div className=\"border-t border-border -my-3 lg:hidden\" /> */}\n\n      {/* Right Column - History */}\n      <div className=\"flex flex-col min-h-0 overflow-hidden\">\n        <HistoryTable />\n      </div>\n\n      {/* Floating Generate Box */}\n      <FloatingGenerateBox isPlayerOpen={!!audioUrl} />\n\n      {/* Import Dialog */}\n      <Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Import Profile</DialogTitle>\n            <DialogDescription>\n              Import the profile from \"{selectedFile?.name}\". This will create a new profile with\n              all samples.\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => {\n                setImportDialogOpen(false);\n                setSelectedFile(null);\n                if (fileInputRef.current) {\n                  fileInputRef.current.value = '';\n                }\n              }}\n            >\n              Cancel\n            </Button>\n            <Button\n              onClick={handleImportConfirm}\n              disabled={importProfile.isPending || !selectedFile}\n            >\n              {importProfile.isPending ? 'Importing...' : 'Import'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ModelsTab/ModelsTab.tsx",
    "content": "import { ModelManagement } from '@/components/ServerSettings/ModelManagement';\n\nexport function ModelsTab() {\n  return (\n    <div className=\"h-full flex flex-col\">\n      <ModelManagement />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerSettings/ConnectionForm.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod';\nimport { Loader2, XCircle } from 'lucide-react';\nimport { useEffect } from 'react';\nimport { useForm } from 'react-hook-form';\nimport * as z from 'zod';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from '@/components/ui/form';\nimport { Input } from '@/components/ui/input';\nimport { useToast } from '@/components/ui/use-toast';\nimport { useServerHealth } from '@/lib/hooks/useServer';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { useServerStore } from '@/stores/serverStore';\n\nconst connectionSchema = z.object({\n  serverUrl: z.string().url('Please enter a valid URL'),\n});\n\ntype ConnectionFormValues = z.infer<typeof connectionSchema>;\n\nexport function ConnectionForm() {\n  const platform = usePlatform();\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const setServerUrl = useServerStore((state) => state.setServerUrl);\n  const keepServerRunningOnClose = useServerStore((state) => state.keepServerRunningOnClose);\n  const setKeepServerRunningOnClose = useServerStore((state) => state.setKeepServerRunningOnClose);\n  const mode = useServerStore((state) => state.mode);\n  const setMode = useServerStore((state) => state.setMode);\n  const { toast } = useToast();\n  const { data: health, isLoading, error: healthError } = useServerHealth();\n\n  const form = useForm<ConnectionFormValues>({\n    resolver: zodResolver(connectionSchema),\n    defaultValues: {\n      serverUrl: serverUrl,\n    },\n  });\n\n  // Sync form with store when serverUrl changes externally\n  useEffect(() => {\n    form.reset({ serverUrl });\n  }, [serverUrl, form]);\n\n  const { isDirty } = form.formState;\n\n  function onSubmit(data: ConnectionFormValues) {\n    setServerUrl(data.serverUrl);\n    form.reset(data);\n    toast({\n      title: 'Server URL updated',\n      description: `Connected to ${data.serverUrl}`,\n    });\n  }\n\n  return (\n    <Card role=\"region\" aria-label=\"Server Connection\" tabIndex={0}>\n      <CardHeader>\n        <CardTitle>Server Connection</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n            <FormField\n              control={form.control}\n              name=\"serverUrl\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Server URL</FormLabel>\n                  <FormControl>\n                    <Input placeholder=\"http://127.0.0.1:17493\" {...field} />\n                  </FormControl>\n                  <FormDescription>Enter the URL of your voicebox backend server</FormDescription>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            {isDirty && <Button type=\"submit\">Update Connection</Button>}\n          </form>\n        </Form>\n\n        {/* Connection status */}\n        <div className=\"mt-4\">\n          {isLoading ? (\n            <div className=\"flex items-center gap-2\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n              <span className=\"text-sm text-muted-foreground\">Checking connection...</span>\n            </div>\n          ) : healthError ? (\n            <div className=\"flex items-center gap-2\">\n              <XCircle className=\"h-4 w-4 text-destructive\" />\n              <span className=\"text-sm text-destructive\">\n                Connection failed: {healthError.message}\n              </span>\n            </div>\n          ) : health ? (\n            <div className=\"flex flex-wrap gap-2\">\n              <Badge\n                variant={health.model_loaded || health.model_downloaded ? 'default' : 'secondary'}\n              >\n                {health.model_loaded || health.model_downloaded ? 'Model Ready' : 'No Model'}\n              </Badge>\n              <Badge variant={health.gpu_available ? 'default' : 'secondary'}>\n                GPU: {health.gpu_available ? 'Available' : 'Not Available'}\n              </Badge>\n              {health.vram_used_mb != null && health.vram_used_mb > 0 && (\n                <Badge variant=\"outline\">VRAM: {health.vram_used_mb.toFixed(0)} MB</Badge>\n              )}\n            </div>\n          ) : null}\n        </div>\n\n        <div className=\"mt-6 pt-6 border-t\">\n          <div className=\"flex items-start space-x-3\">\n            <Checkbox\n              id=\"keepServerRunning\"\n              className=\"mt-[6px]\"\n              checked={keepServerRunningOnClose}\n              onCheckedChange={(checked: boolean) => {\n                setKeepServerRunningOnClose(checked);\n                platform.lifecycle.setKeepServerRunning(checked).catch((error) => {\n                  console.error('Failed to sync setting to Rust:', error);\n                });\n                toast({\n                  title: 'Setting updated',\n                  description: checked\n                    ? 'Server will continue running when app closes'\n                    : 'Server will stop when app closes',\n                });\n              }}\n            />\n            <div className=\"space-y-1\">\n              <label\n                htmlFor=\"keepServerRunning\"\n                className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n              >\n                Keep server running when app closes\n              </label>\n              <p className=\"text-sm text-muted-foreground\">\n                When enabled, the server will continue running in the background after closing the\n                app. Disabled by default.\n              </p>\n            </div>\n          </div>\n        </div>\n\n        {platform.metadata.isTauri && (\n          <div className=\"mt-6 pt-6 border-t\">\n            <div className=\"flex items-start space-x-3\">\n              <Checkbox\n                id=\"allowNetworkAccess\"\n                className=\"mt-[6px]\"\n                checked={mode === 'remote'}\n                onCheckedChange={(checked: boolean) => {\n                  setMode(checked ? 'remote' : 'local');\n                  toast({\n                    title: 'Setting updated',\n                    description: checked\n                      ? 'Network access enabled. Restart the app to apply.'\n                      : 'Network access disabled. Restart the app to apply.',\n                  });\n                }}\n              />\n              <div className=\"space-y-1\">\n                <label\n                  htmlFor=\"allowNetworkAccess\"\n                  className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n                >\n                  Allow network access\n                </label>\n                <p className=\"text-sm text-muted-foreground\">\n                  Makes the server accessible from other devices on your network. Restart the app\n                  after changing this setting.\n                </p>\n              </div>\n            </div>\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerSettings/GenerationSettings.tsx",
    "content": "import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Slider } from '@/components/ui/slider';\nimport { useServerStore } from '@/stores/serverStore';\n\nexport function GenerationSettings() {\n  const maxChunkChars = useServerStore((state) => state.maxChunkChars);\n  const setMaxChunkChars = useServerStore((state) => state.setMaxChunkChars);\n  const crossfadeMs = useServerStore((state) => state.crossfadeMs);\n  const setCrossfadeMs = useServerStore((state) => state.setCrossfadeMs);\n  const normalizeAudio = useServerStore((state) => state.normalizeAudio);\n  const setNormalizeAudio = useServerStore((state) => state.setNormalizeAudio);\n  const autoplayOnGenerate = useServerStore((state) => state.autoplayOnGenerate);\n  const setAutoplayOnGenerate = useServerStore((state) => state.setAutoplayOnGenerate);\n\n  return (\n    <Card role=\"region\" aria-label=\"Generation Settings\" tabIndex={0}>\n      <CardHeader>\n        <CardTitle>Generation Settings</CardTitle>\n        <CardDescription>\n          Controls for long text generation. These settings apply to all engines.\n        </CardDescription>\n      </CardHeader>\n      <CardContent>\n        <div className=\"space-y-6\">\n          <div className=\"space-y-3\">\n            <div className=\"flex items-center justify-between\">\n              <label htmlFor=\"maxChunkChars\" className=\"text-sm font-medium leading-none\">\n                Auto-chunking limit\n              </label>\n              <span className=\"text-sm tabular-nums text-muted-foreground\">\n                {maxChunkChars} chars\n              </span>\n            </div>\n            <Slider\n              id=\"maxChunkChars\"\n              value={[maxChunkChars]}\n              onValueChange={([value]) => setMaxChunkChars(value)}\n              min={100}\n              max={5000}\n              step={50}\n              aria-label=\"Auto-chunking character limit\"\n            />\n            <p className=\"text-sm text-muted-foreground\">\n              Long text is split into chunks at sentence boundaries before generating. Lower values\n              can improve quality for long outputs.\n            </p>\n          </div>\n\n          <div className=\"space-y-3\">\n            <div className=\"flex items-center justify-between\">\n              <label htmlFor=\"crossfadeMs\" className=\"text-sm font-medium leading-none\">\n                Chunk crossfade\n              </label>\n              <span className=\"text-sm tabular-nums text-muted-foreground\">\n                {crossfadeMs === 0 ? 'Cut' : `${crossfadeMs}ms`}\n              </span>\n            </div>\n            <Slider\n              id=\"crossfadeMs\"\n              value={[crossfadeMs]}\n              onValueChange={([value]) => setCrossfadeMs(value)}\n              min={0}\n              max={200}\n              step={10}\n              aria-label=\"Chunk crossfade duration\"\n            />\n            <p className=\"text-sm text-muted-foreground\">\n              Blends audio between chunks to smooth transitions. Set to 0 for a hard cut.\n            </p>\n          </div>\n\n          <div className=\"flex items-start gap-3\">\n            <Checkbox\n              id=\"normalizeAudio\"\n              checked={normalizeAudio}\n              onCheckedChange={setNormalizeAudio}\n              className=\"mt-[6px]\"\n            />\n            <div className=\"space-y-1\">\n              <label\n                htmlFor=\"normalizeAudio\"\n                className=\"text-sm font-medium leading-none cursor-pointer\"\n              >\n                Normalize audio\n              </label>\n              <p className=\"text-sm text-muted-foreground\">\n                Adjusts output volume to a consistent level across generations.\n              </p>\n            </div>\n          </div>\n\n          <div className=\"flex items-start gap-3\">\n            <Checkbox\n              id=\"autoplayOnGenerate\"\n              checked={autoplayOnGenerate}\n              onCheckedChange={setAutoplayOnGenerate}\n              className=\"mt-[6px]\"\n            />\n            <div className=\"space-y-1\">\n              <label\n                htmlFor=\"autoplayOnGenerate\"\n                className=\"text-sm font-medium leading-none cursor-pointer\"\n              >\n                Autoplay on generate\n              </label>\n              <p className=\"text-sm text-muted-foreground\">\n                Automatically play audio when a generation completes.\n              </p>\n            </div>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerSettings/GpuAcceleration.tsx",
    "content": "import { useQuery, useQueryClient } from '@tanstack/react-query';\nimport { AlertCircle, Download, Loader2, RotateCw, Trash2 } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Progress } from '@/components/ui/progress';\nimport { apiClient } from '@/lib/api/client';\nimport type { CudaDownloadProgress } from '@/lib/api/types';\nimport { useServerHealth } from '@/lib/hooks/useServer';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { useServerStore } from '@/stores/serverStore';\n\ntype RestartPhase = 'idle' | 'stopping' | 'waiting' | 'ready';\n\nexport function GpuAcceleration() {\n  const platform = usePlatform();\n  const queryClient = useQueryClient();\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const { data: health } = useServerHealth();\n\n  const [restartPhase, setRestartPhase] = useState<RestartPhase>('idle');\n  const [error, setError] = useState<string | null>(null);\n  const [downloadProgress, setDownloadProgress] = useState<CudaDownloadProgress | null>(null);\n  const healthPollRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n  // Query CUDA backend status\n  const {\n    data: cudaStatus,\n    isLoading: _cudaStatusLoading,\n    refetch: refetchCudaStatus,\n  } = useQuery({\n    queryKey: ['cuda-status', serverUrl],\n    queryFn: () => apiClient.getCudaStatus(),\n    refetchInterval: (query) => (query.state.status === 'pending' ? false : 10000),\n    retry: 1,\n    enabled: !!health, // Only fetch when backend is reachable\n  });\n\n  // Derived state\n  const isCurrentlyCuda = health?.backend_variant === 'cuda';\n  const cudaAvailable = cudaStatus?.available ?? false;\n  const cudaDownloading = cudaStatus?.downloading ?? false;\n\n  // Clean up health poll on unmount\n  useEffect(() => {\n    return () => {\n      if (healthPollRef.current) {\n        clearInterval(healthPollRef.current);\n        healthPollRef.current = null;\n      }\n    };\n  }, []);\n\n  // SSE progress tracking during download\n  useEffect(() => {\n    if (!cudaDownloading || !serverUrl) {\n      return;\n    }\n\n    const eventSource = new EventSource(`${serverUrl}/backend/cuda-progress`);\n\n    eventSource.onmessage = (event) => {\n      try {\n        const data = JSON.parse(event.data) as CudaDownloadProgress;\n        setDownloadProgress(data);\n\n        if (data.status === 'complete') {\n          eventSource.close();\n          setDownloadProgress(null);\n          refetchCudaStatus();\n        } else if (data.status === 'error') {\n          eventSource.close();\n          setError(data.error || 'Download failed');\n          setDownloadProgress(null);\n          refetchCudaStatus();\n        }\n      } catch (e) {\n        console.error('Error parsing CUDA progress event:', e);\n      }\n    };\n\n    eventSource.onerror = () => {\n      eventSource.close();\n    };\n\n    return () => {\n      eventSource.close();\n    };\n  }, [cudaDownloading, serverUrl, refetchCudaStatus]);\n\n  // Start aggressive health polling during restart\n  const startHealthPolling = useCallback(() => {\n    if (healthPollRef.current) return;\n\n    healthPollRef.current = setInterval(async () => {\n      try {\n        const result = await apiClient.getHealth();\n        if (result.status === 'healthy') {\n          // Server is back up\n          if (healthPollRef.current) {\n            clearInterval(healthPollRef.current);\n            healthPollRef.current = null;\n          }\n          setRestartPhase('ready');\n          // Invalidate all queries to refresh UI\n          queryClient.invalidateQueries();\n          // Reset after a moment\n          setTimeout(() => setRestartPhase('idle'), 2000);\n        }\n      } catch {\n        // Server still down, keep polling\n      }\n    }, 1000);\n  }, [queryClient]);\n\n  const handleDownload = async () => {\n    setError(null);\n    try {\n      await apiClient.downloadCudaBackend();\n      refetchCudaStatus();\n    } catch (e: unknown) {\n      const msg = e instanceof Error ? e.message : 'Failed to start download';\n      if (msg.includes('already downloaded')) {\n        refetchCudaStatus();\n      } else {\n        setError(msg);\n      }\n    }\n  };\n\n  const handleRestart = async () => {\n    setError(null);\n    setRestartPhase('stopping');\n\n    try {\n      setRestartPhase('waiting');\n      startHealthPolling();\n      await platform.lifecycle.restartServer();\n      // Invoke resolved — server is likely ready. Stop polling and refresh.\n      if (healthPollRef.current) {\n        clearInterval(healthPollRef.current);\n        healthPollRef.current = null;\n      }\n      setRestartPhase('ready');\n      queryClient.invalidateQueries();\n      setTimeout(() => setRestartPhase('idle'), 2000);\n    } catch (e: unknown) {\n      setRestartPhase('idle');\n      if (healthPollRef.current) {\n        clearInterval(healthPollRef.current);\n        healthPollRef.current = null;\n      }\n      setError(e instanceof Error ? e.message : 'Restart failed');\n    }\n  };\n\n  const handleSwitchToCpu = async () => {\n    // To switch to CPU: delete the CUDA binary, then restart.\n    // start_server always prefers CUDA if present, so we must remove it first.\n    setError(null);\n    setRestartPhase('stopping');\n\n    try {\n      await apiClient.deleteCudaBackend();\n      setRestartPhase('waiting');\n      startHealthPolling();\n      await platform.lifecycle.restartServer();\n      // Invoke resolved — server is likely ready\n      if (healthPollRef.current) {\n        clearInterval(healthPollRef.current);\n        healthPollRef.current = null;\n      }\n      setRestartPhase('ready');\n      queryClient.invalidateQueries();\n      setTimeout(() => setRestartPhase('idle'), 2000);\n    } catch (e: unknown) {\n      setRestartPhase('idle');\n      if (healthPollRef.current) {\n        clearInterval(healthPollRef.current);\n        healthPollRef.current = null;\n      }\n      setError(e instanceof Error ? e.message : 'Failed to switch to CPU');\n      refetchCudaStatus();\n    }\n  };\n\n  const handleDelete = async () => {\n    setError(null);\n    try {\n      await apiClient.deleteCudaBackend();\n      refetchCudaStatus();\n    } catch (e: unknown) {\n      setError(e instanceof Error ? e.message : 'Failed to delete CUDA backend');\n    }\n  };\n\n  const formatBytes = (bytes: number): string => {\n    if (bytes === 0) return '0 B';\n    const k = 1024;\n    const sizes = ['B', 'KB', 'MB', 'GB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;\n  };\n\n  // Don't render until health data is available\n  if (!health) return null;\n\n  // If the system already has native GPU (MPS, etc.), only show info - no CUDA needed\n  const hasNativeGpu =\n    health.gpu_available &&\n    !isCurrentlyCuda &&\n    health.gpu_type &&\n    !health.gpu_type.includes('CUDA');\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>GPU Acceleration</CardTitle>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        {/* GPU status */}\n        <div className=\"space-y-1\">\n          {health.gpu_available && health.gpu_type ? (\n            <>\n              <div className=\"text-sm font-medium\">\n                {health.gpu_type.replace(/^(CUDA|ROCm|MPS|Metal|XPU|DirectML)\\s*\\((.+)\\)$/, '$2') ||\n                  health.gpu_type}\n              </div>\n              <div className=\"text-sm text-muted-foreground\">\n                {health.gpu_type.replace(/\\s*\\(.+\\)$/, '')}\n                {health.vram_used_mb != null && health.vram_used_mb > 0\n                  ? ` \\u00b7 ${health.vram_used_mb.toFixed(0)} MB VRAM used`\n                  : ''}\n              </div>\n            </>\n          ) : (\n            <>\n              <div className=\"text-sm font-medium\">CPU</div>\n              <div className=\"text-sm text-muted-foreground\">No GPU acceleration available</div>\n            </>\n          )}\n        </div>\n\n        {/* Native GPU detected - no CUDA download needed */}\n\n        {/* Currently running CUDA - show switch back to CPU */}\n        {isCurrentlyCuda && platform.metadata.isTauri && (\n          <>\n            {restartPhase !== 'idle' ? (\n              <div className=\"flex items-center gap-2 p-3 rounded-lg bg-primary/5 border\">\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\n                <span className=\"text-sm\">\n                  {restartPhase === 'stopping' && 'Stopping server...'}\n                  {restartPhase === 'waiting' && 'Restarting server...'}\n                  {restartPhase === 'ready' && 'Server restarted successfully!'}\n                </span>\n              </div>\n            ) : (\n              <div className=\"space-y-3\">\n                <p className=\"text-sm text-muted-foreground\">\n                  Running with CUDA GPU acceleration. Switch back to CPU if needed (you can\n                  re-download later).\n                </p>\n                <Button onClick={handleSwitchToCpu} variant=\"outline\" className=\"w-full\" size=\"sm\">\n                  <RotateCw className=\"h-4 w-4 mr-2\" />\n                  Switch to CPU Backend\n                </Button>\n              </div>\n            )}\n            {error && (\n              <div className=\"flex items-center gap-2 text-sm text-destructive\">\n                <AlertCircle className=\"h-4 w-4 shrink-0\" />\n                <span>{error}</span>\n              </div>\n            )}\n          </>\n        )}\n\n        {/* CUDA download/manage section - show when no native GPU and not currently running CUDA */}\n        {!hasNativeGpu && !isCurrentlyCuda && (\n          <>\n            {/* Download progress (manual download or auto-update) */}\n            {cudaDownloading && downloadProgress && (\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center justify-between text-sm\">\n                  <div className=\"flex items-center gap-2\">\n                    <Loader2 className=\"h-4 w-4 animate-spin\" />\n                    <span>\n                      {downloadProgress.filename ||\n                        (cudaAvailable\n                          ? 'Updating CUDA backend...'\n                          : 'Downloading CUDA backend...')}\n                    </span>\n                  </div>\n                  {downloadProgress.total > 0 && (\n                    <span className=\"text-muted-foreground\">\n                      {downloadProgress.progress.toFixed(1)}%\n                    </span>\n                  )}\n                </div>\n                {downloadProgress.total > 0 && (\n                  <>\n                    <Progress value={downloadProgress.progress} className=\"h-2\" />\n                    <div className=\"text-xs text-muted-foreground\">\n                      {formatBytes(downloadProgress.current)} /{' '}\n                      {formatBytes(downloadProgress.total)}\n                    </div>\n                  </>\n                )}\n              </div>\n            )}\n\n            {/* Restart in progress */}\n            {restartPhase !== 'idle' && (\n              <div className=\"flex items-center gap-2 p-3 rounded-lg bg-primary/5 border\">\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\n                <span className=\"text-sm\">\n                  {restartPhase === 'stopping' && 'Stopping server...'}\n                  {restartPhase === 'waiting' && 'Restarting server...'}\n                  {restartPhase === 'ready' && 'Server restarted successfully!'}\n                </span>\n              </div>\n            )}\n\n            {/* Error display */}\n            {error && (\n              <div className=\"flex items-center gap-2 text-sm text-destructive\">\n                <AlertCircle className=\"h-4 w-4 shrink-0\" />\n                <span>{error}</span>\n              </div>\n            )}\n\n            {/* Actions */}\n            {restartPhase === 'idle' && !cudaDownloading && (\n              <div className=\"space-y-2\">\n                {/* Not downloaded yet - show download button */}\n                {!cudaAvailable && (\n                  <div className=\"space-y-3\">\n                    <p className=\"text-sm text-muted-foreground\">\n                      Download the CUDA backend (~2.4 GB) for NVIDIA GPU acceleration. Requires an\n                      NVIDIA GPU with CUDA support.\n                    </p>\n                    <Button onClick={handleDownload} className=\"w-full\" size=\"sm\">\n                      <Download className=\"h-4 w-4 mr-2\" />\n                      Download CUDA Backend\n                    </Button>\n                  </div>\n                )}\n\n                {/* Downloaded but not active - show switch button */}\n                {cudaAvailable && platform.metadata.isTauri && (\n                  <div className=\"space-y-3\">\n                    <p className=\"text-sm text-muted-foreground\">\n                      CUDA backend is downloaded and ready. Restart the server to enable GPU\n                      acceleration.\n                    </p>\n                    <Button onClick={handleRestart} className=\"w-full\" size=\"sm\">\n                      <RotateCw className=\"h-4 w-4 mr-2\" />\n                      Switch to CUDA Backend\n                    </Button>\n                  </div>\n                )}\n\n                {/* Delete option when downloaded (and not active) */}\n                {cudaAvailable && (\n                  <Button\n                    onClick={handleDelete}\n                    variant=\"ghost\"\n                    className=\"w-full text-muted-foreground hover:text-destructive\"\n                    size=\"sm\"\n                  >\n                    <Trash2 className=\"h-4 w-4 mr-2\" />\n                    Remove CUDA Backend\n                  </Button>\n                )}\n              </div>\n            )}\n          </>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerSettings/ModelManagement.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport {\n  ChevronDown,\n  ChevronRight,\n  ChevronUp,\n  CircleCheck,\n  CircleX,\n  Download,\n  ExternalLink,\n  FolderOpen,\n  HardDrive,\n  Heart,\n  Loader2,\n  RotateCcw,\n  Scale,\n  Trash2,\n  Unplug,\n  X,\n} from 'lucide-react';\nimport { useCallback, useMemo, useState } from 'react';\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from '@/components/ui/alert-dialog';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport { Progress } from '@/components/ui/progress';\nimport { useToast } from '@/components/ui/use-toast';\nimport { apiClient } from '@/lib/api/client';\nimport type { ActiveDownloadTask, HuggingFaceModelInfo, ModelStatus } from '@/lib/api/types';\nimport { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { useServerStore } from '@/stores/serverStore';\n\nasync function fetchHuggingFaceModelInfo(repoId: string): Promise<HuggingFaceModelInfo> {\n  const response = await fetch(`https://huggingface.co/api/models/${repoId}`);\n  if (!response.ok) throw new Error(`Failed to fetch model info: ${response.status}`);\n  return response.json();\n}\n\nconst MODEL_DESCRIPTIONS: Record<string, string> = {\n  'qwen-tts-1.7B':\n    'High-quality multilingual TTS by Alibaba. Supports 10 languages with natural prosody and voice cloning from short reference audio.',\n  'qwen-tts-0.6B':\n    'Lightweight version of Qwen TTS. Same language support with faster inference, ideal for lower-end hardware.',\n  luxtts:\n    'Lightweight ZipVoice-based TTS designed for high quality voice cloning and 48kHz speech generation at speeds exceeding 150x realtime.',\n  'chatterbox-tts':\n    'Production-grade open source TTS by Resemble AI. Supports 23 languages with voice cloning and emotion exaggeration control.',\n  'chatterbox-turbo':\n    'Streamlined 350M parameter TTS by Resemble AI. High-quality English speech with less compute and VRAM than larger models.',\n  'tada-1b':\n    'HumeAI TADA 1B — English speech-language model built on Llama 3.2 1B. Generates 700s+ of coherent audio with synchronized text-acoustic alignment.',\n  'tada-3b-ml':\n    'HumeAI TADA 3B Multilingual — built on Llama 3.2 3B. Supports 10 languages with high-fidelity voice cloning via text-acoustic dual alignment.',\n  kokoro:\n    'Kokoro 82M by hexgrad. Tiny 82M-parameter TTS that runs at CPU realtime. Supports 8 languages with pre-built voice styles. Apache 2.0 licensed.',\n  'whisper-base':\n    'Smallest Whisper model (74M parameters). Fast transcription with moderate accuracy.',\n  'whisper-small':\n    'Whisper Small (244M parameters). Good balance of speed and accuracy for transcription.',\n  'whisper-medium':\n    'Whisper Medium (769M parameters). Higher accuracy transcription at moderate speed.',\n  'whisper-large':\n    'Whisper Large (1.5B parameters). Best accuracy for speech-to-text across multiple languages.',\n  'whisper-turbo':\n    'Whisper Large v3 Turbo. Pruned for significantly faster inference while maintaining near-large accuracy.',\n};\n\nfunction formatDownloads(n: number): string {\n  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;\n  if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;\n  return n.toString();\n}\n\nfunction formatLicense(license: string): string {\n  const map: Record<string, string> = {\n    'apache-2.0': 'Apache 2.0',\n    mit: 'MIT',\n    'cc-by-4.0': 'CC BY 4.0',\n    'cc-by-sa-4.0': 'CC BY-SA 4.0',\n    'cc-by-nc-4.0': 'CC BY-NC 4.0',\n    'openrail++': 'OpenRAIL++',\n    openrail: 'OpenRAIL',\n  };\n  return map[license] || license;\n}\n\nfunction formatPipelineTag(tag: string): string {\n  return tag\n    .split('-')\n    .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n    .join(' ');\n}\n\nfunction formatBytes(bytes: number): string {\n  if (bytes === 0) return '0 B';\n  const k = 1024;\n  const sizes = ['B', 'KB', 'MB', 'GB'];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;\n}\n\nexport function ModelManagement() {\n  const { toast } = useToast();\n  const queryClient = useQueryClient();\n  const platform = usePlatform();\n  const customModelsDir = useServerStore((state) => state.customModelsDir);\n  const setCustomModelsDir = useServerStore((state) => state.setCustomModelsDir);\n  const [migrating, setMigrating] = useState(false);\n  const [migrationProgress, setMigrationProgress] = useState<{\n    current: number;\n    total: number;\n    progress: number;\n    filename?: string;\n    status: string;\n  } | null>(null);\n  const [pendingMigrateDir, setPendingMigrateDir] = useState<string | null>(null);\n  const [downloadingModel, setDownloadingModel] = useState<string | null>(null);\n  const [downloadingDisplayName, setDownloadingDisplayName] = useState<string | null>(null);\n  const [consoleOpen, setConsoleOpen] = useState(false);\n  const [dismissedErrors, setDismissedErrors] = useState<Set<string>>(new Set());\n  const [localErrors, setLocalErrors] = useState<Map<string, string>>(new Map());\n\n  // Modal state\n  const [selectedModel, setSelectedModel] = useState<ModelStatus | null>(null);\n  const [detailOpen, setDetailOpen] = useState(false);\n\n  const { data: modelStatus, isLoading } = useQuery({\n    queryKey: ['modelStatus'],\n    queryFn: async () => {\n      const result = await apiClient.getModelStatus();\n      return result;\n    },\n    refetchInterval: 5000,\n  });\n\n  const { data: cacheDir } = useQuery({\n    queryKey: ['modelsCacheDir'],\n    queryFn: () => apiClient.getModelsCacheDir(),\n    staleTime: 1000 * 60 * 5,\n  });\n\n  const { data: activeTasks } = useQuery({\n    queryKey: ['activeTasks'],\n    queryFn: () => apiClient.getActiveTasks(),\n    refetchInterval: (query) => {\n      const data = query.state.data;\n      const hasActive = data?.downloads.some((d) => d.status === 'downloading');\n      return hasActive ? 1000 : 5000;\n    },\n  });\n\n  // HuggingFace model card query - only fetches when modal is open and model has a repo ID\n  const { data: hfModelInfo, isLoading: hfLoading } = useQuery({\n    queryKey: ['hfModelInfo', selectedModel?.hf_repo_id],\n    queryFn: () => fetchHuggingFaceModelInfo(selectedModel!.hf_repo_id!),\n    enabled: detailOpen && !!selectedModel?.hf_repo_id,\n    staleTime: 1000 * 60 * 30, // Cache for 30 minutes\n    retry: 1,\n  });\n\n  // Build a map of errored downloads for quick lookup, excluding dismissed ones\n  const erroredDownloads = new Map<string, ActiveDownloadTask>();\n  if (activeTasks?.downloads) {\n    for (const dl of activeTasks.downloads) {\n      if (dl.status === 'error' && !dismissedErrors.has(dl.model_name)) {\n        const localErr = localErrors.get(dl.model_name);\n        erroredDownloads.set(dl.model_name, localErr ? { ...dl, error: localErr } : dl);\n      }\n    }\n  }\n  for (const [modelName, error] of localErrors) {\n    if (!erroredDownloads.has(modelName) && !dismissedErrors.has(modelName)) {\n      erroredDownloads.set(modelName, {\n        model_name: modelName,\n        status: 'error',\n        started_at: new Date().toISOString(),\n        error,\n      });\n    }\n  }\n\n  const errorCount = erroredDownloads.size;\n\n  // Build progress map from active tasks for inline display\n  const downloadProgressMap = useMemo(() => {\n    const map = new Map<string, ActiveDownloadTask>();\n    if (activeTasks?.downloads) {\n      for (const dl of activeTasks.downloads) {\n        if (dl.status === 'downloading') {\n          map.set(dl.model_name, dl);\n        }\n      }\n    }\n    return map;\n  }, [activeTasks]);\n\n  const handleDownloadComplete = useCallback(() => {\n    setDownloadingModel(null);\n    setDownloadingDisplayName(null);\n    queryClient.invalidateQueries({ queryKey: ['modelStatus'] });\n    queryClient.invalidateQueries({ queryKey: ['activeTasks'] });\n  }, [queryClient]);\n\n  const handleDownloadError = useCallback(\n    (error: string) => {\n      if (downloadingModel) {\n        setLocalErrors((prev) => new Map(prev).set(downloadingModel, error));\n        setConsoleOpen(true);\n      }\n      setDownloadingModel(null);\n      setDownloadingDisplayName(null);\n      queryClient.invalidateQueries({ queryKey: ['activeTasks'] });\n    },\n    [queryClient, downloadingModel],\n  );\n\n  useModelDownloadToast({\n    modelName: downloadingModel || '',\n    displayName: downloadingDisplayName || '',\n    enabled: !!downloadingModel && !!downloadingDisplayName,\n    onComplete: handleDownloadComplete,\n    onError: handleDownloadError,\n  });\n\n  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);\n  const [modelToDelete, setModelToDelete] = useState<{\n    name: string;\n    displayName: string;\n    sizeMb?: number;\n  } | null>(null);\n\n  const handleDownload = async (modelName: string) => {\n    setDismissedErrors((prev) => {\n      const next = new Set(prev);\n      next.delete(modelName);\n      return next;\n    });\n\n    const model = modelStatus?.models.find((m) => m.model_name === modelName);\n    const displayName = model?.display_name || modelName;\n\n    try {\n      await apiClient.triggerModelDownload(modelName);\n\n      setDownloadingModel(modelName);\n      setDownloadingDisplayName(displayName);\n\n      queryClient.invalidateQueries({ queryKey: ['modelStatus'] });\n      queryClient.invalidateQueries({ queryKey: ['activeTasks'] });\n    } catch (error) {\n      setDownloadingModel(null);\n      setDownloadingDisplayName(null);\n      toast({\n        title: 'Download failed',\n        description: error instanceof Error ? error.message : 'Unknown error',\n        variant: 'destructive',\n      });\n    }\n  };\n\n  const cancelMutation = useMutation({\n    mutationFn: (modelName: string) => apiClient.cancelDownload(modelName),\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({ queryKey: ['modelStatus'], refetchType: 'all' });\n      await queryClient.invalidateQueries({ queryKey: ['activeTasks'], refetchType: 'all' });\n    },\n  });\n\n  const handleCancel = (modelName: string) => {\n    const prevDismissed = dismissedErrors;\n    const prevLocalErrors = localErrors;\n    const prevDownloadingModel = downloadingModel;\n    const prevDownloadingDisplayName = downloadingDisplayName;\n\n    setDismissedErrors((prev) => new Set(prev).add(modelName));\n    setLocalErrors((prev) => {\n      const next = new Map(prev);\n      next.delete(modelName);\n      return next;\n    });\n    if (downloadingModel === modelName) {\n      setDownloadingModel(null);\n      setDownloadingDisplayName(null);\n    }\n\n    cancelMutation.mutate(modelName, {\n      onError: () => {\n        setDismissedErrors(prevDismissed);\n        setLocalErrors(prevLocalErrors);\n        setDownloadingModel(prevDownloadingModel);\n        setDownloadingDisplayName(prevDownloadingDisplayName);\n        toast({\n          title: 'Cancel failed',\n          description: 'Could not cancel the download task.',\n          variant: 'destructive',\n        });\n      },\n    });\n  };\n\n  const clearAllMutation = useMutation({\n    mutationFn: () => apiClient.clearAllTasks(),\n    onSuccess: async () => {\n      setDismissedErrors(new Set());\n      setLocalErrors(new Map());\n      setDownloadingModel(null);\n      setDownloadingDisplayName(null);\n      await queryClient.invalidateQueries({ queryKey: ['modelStatus'], refetchType: 'all' });\n      await queryClient.invalidateQueries({ queryKey: ['activeTasks'], refetchType: 'all' });\n    },\n  });\n\n  const deleteMutation = useMutation({\n    mutationFn: async (modelName: string) => {\n      const result = await apiClient.deleteModel(modelName);\n      return result;\n    },\n    onSuccess: async () => {\n      toast({\n        title: 'Model deleted',\n        description: `${modelToDelete?.displayName || 'Model'} has been deleted successfully.`,\n      });\n      setDeleteDialogOpen(false);\n      setModelToDelete(null);\n      setDetailOpen(false);\n      setSelectedModel(null);\n      await queryClient.invalidateQueries({ queryKey: ['modelStatus'], refetchType: 'all' });\n      await queryClient.refetchQueries({ queryKey: ['modelStatus'] });\n    },\n    onError: (error: Error) => {\n      toast({\n        title: 'Delete failed',\n        description: error.message,\n        variant: 'destructive',\n      });\n    },\n  });\n\n  const unloadMutation = useMutation({\n    mutationFn: async (modelName: string) => {\n      return await apiClient.unloadModel(modelName);\n    },\n    onSuccess: async (_data, modelName) => {\n      toast({\n        title: 'Model unloaded',\n        description: `${modelName} has been unloaded from memory.`,\n      });\n      await queryClient.invalidateQueries({ queryKey: ['modelStatus'], refetchType: 'all' });\n      await queryClient.refetchQueries({ queryKey: ['modelStatus'] });\n    },\n    onError: (error: Error) => {\n      toast({\n        title: 'Unload failed',\n        description: error.message,\n        variant: 'destructive',\n      });\n    },\n  });\n\n  const formatSize = (sizeMb?: number): string => {\n    if (!sizeMb) return 'Unknown size';\n    if (sizeMb < 1024) return `${sizeMb.toFixed(1)} MB`;\n    return `${(sizeMb / 1024).toFixed(2)} GB`;\n  };\n\n  const getModelState = (model: ModelStatus) => {\n    const isDownloading =\n      (model.downloading || downloadingModel === model.model_name) &&\n      !erroredDownloads.has(model.model_name) &&\n      !dismissedErrors.has(model.model_name);\n    const hasError = erroredDownloads.has(model.model_name);\n    return { isDownloading, hasError };\n  };\n\n  const openModelDetail = (model: ModelStatus) => {\n    setSelectedModel(model);\n    setDetailOpen(true);\n  };\n\n  const voiceModels =\n    modelStatus?.models.filter(\n      (m) =>\n        m.model_name.startsWith('qwen-tts') ||\n        m.model_name.startsWith('luxtts') ||\n        m.model_name.startsWith('chatterbox') ||\n        m.model_name.startsWith('tada') ||\n        m.model_name.startsWith('kokoro'),\n    ) ?? [];\n  const whisperModels = modelStatus?.models.filter((m) => m.model_name.startsWith('whisper')) ?? [];\n\n  // Build sections\n  const sections: { label: string; models: ModelStatus[] }[] = [\n    { label: 'Voice Generation', models: voiceModels },\n    { label: 'Transcription', models: whisperModels },\n  ];\n\n  // Get detail modal state for selected model\n  const selectedState = selectedModel ? getModelState(selectedModel) : null;\n  const selectedError = selectedModel ? erroredDownloads.get(selectedModel.model_name) : undefined;\n\n  // Keep selectedModel data fresh from query results\n  const freshSelectedModel =\n    selectedModel && modelStatus\n      ? modelStatus.models.find((m) => m.model_name === selectedModel.model_name) || selectedModel\n      : selectedModel;\n\n  // Derive license from HF data\n  const license =\n    hfModelInfo?.cardData?.license ||\n    hfModelInfo?.tags?.find((t) => t.startsWith('license:'))?.replace('license:', '');\n\n  return (\n    <div className=\"flex flex-col h-full\">\n      {/* Header */}\n      <div className=\"shrink-0 pb-4\">\n        <h1 className=\"text-lg font-semibold\">Models</h1>\n        <p className=\"text-sm text-muted-foreground\">\n          Download and manage AI models for voice generation and transcription\n        </p>\n      </div>\n\n      {/* Model storage location */}\n      {platform.metadata.isTauri && cacheDir && (\n        <div className=\"shrink-0 pb-4 border-b mb-4\">\n          <div className=\"flex items-center justify-between gap-2\">\n            <div className=\"min-w-0\">\n              <span className=\"text-xs text-muted-foreground\">Storage location</span>\n              <p\n                className=\"text-xs font-mono text-muted-foreground/70 truncate\"\n                title={cacheDir.path}\n              >\n                {cacheDir.path}\n              </p>\n            </div>\n            <div className=\"flex items-center gap-1 shrink-0\">\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"text-xs text-muted-foreground h-7 px-2\"\n                onClick={async () => {\n                  try {\n                    await platform.filesystem.openPath(cacheDir.path);\n                  } catch {\n                    toast({ title: 'Failed to open model folder', variant: 'destructive' });\n                  }\n                }}\n              >\n                <FolderOpen className=\"h-3 w-3\" />\n                Open\n              </Button>\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                className=\"text-xs text-muted-foreground h-7 px-2\"\n                onClick={async () => {\n                  try {\n                    const newDir = await platform.filesystem.pickDirectory(\n                      'Choose model storage folder',\n                    );\n                    if (!newDir) return;\n                    setPendingMigrateDir(newDir);\n                  } catch {\n                    toast({ title: 'Failed to open folder picker', variant: 'destructive' });\n                  }\n                }}\n                disabled={migrating}\n              >\n                {migrating ? (\n                  <Loader2 className=\"h-3 w-3 animate-spin\" />\n                ) : (\n                  <FolderOpen className=\"h-3 w-3\" />\n                )}\n                {migrating ? 'Migrating...' : 'Change'}\n              </Button>\n              {customModelsDir && (\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"text-xs text-muted-foreground h-7 px-2\"\n                  disabled={migrating}\n                  onClick={async () => {\n                    setCustomModelsDir(null);\n                    toast({ title: 'Reset to default location. Restarting server...' });\n                    await platform.lifecycle.restartServer('');\n                    queryClient.invalidateQueries();\n                  }}\n                >\n                  <RotateCcw className=\"h-3 w-3\" />\n                  Reset\n                </Button>\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n\n      {/* Model list */}\n      {isLoading ? (\n        <div className=\"flex items-center justify-center py-16\">\n          <Loader2 className=\"h-5 w-5 animate-spin text-muted-foreground\" />\n        </div>\n      ) : modelStatus ? (\n        <div className=\"flex-1 min-h-0 overflow-y-auto space-y-6\">\n          {sections.map((section) => (\n            <div key={section.label}>\n              <h2 className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1 px-1\">\n                {section.label}\n              </h2>\n              <div className=\"border rounded-lg divide-y overflow-hidden\">\n                {section.models.map((model) => {\n                  const { isDownloading, hasError } = getModelState(model);\n                  return (\n                    <button\n                      key={model.model_name}\n                      type=\"button\"\n                      onClick={() => openModelDetail(model)}\n                      className=\"w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-muted/50 transition-colors group\"\n                    >\n                      {/* Status indicator */}\n                      <div className=\"shrink-0\">\n                        {hasError ? (\n                          <CircleX className=\"h-4 w-4 text-destructive\" />\n                        ) : isDownloading ? (\n                          <Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />\n                        ) : model.loaded ? (\n                          <CircleCheck className=\"h-4 w-4 text-accent\" />\n                        ) : model.downloaded ? (\n                          <CircleCheck className=\"h-4 w-4 text-emerald-500\" />\n                        ) : (\n                          <Download className=\"h-4 w-4 text-muted-foreground/50\" />\n                        )}\n                      </div>\n\n                      {/* Name + inline progress */}\n                      <div className=\"flex-1 min-w-0\">\n                        <span className=\"text-sm font-medium\">{model.display_name}</span>\n                        {isDownloading &&\n                          (() => {\n                            const dl = downloadProgressMap.get(model.model_name);\n                            const pct = dl?.progress ?? 0;\n                            const hasProgress = dl && dl.total && dl.total > 0;\n                            return (\n                              <div className=\"mt-1 space-y-0.5\">\n                                <Progress value={hasProgress ? pct : undefined} className=\"h-1\" />\n                                <div className=\"text-[10px] text-muted-foreground truncate\">\n                                  {hasProgress\n                                    ? `${formatBytes(dl.current ?? 0)} / ${formatBytes(dl.total!)} (${pct.toFixed(0)}%)`\n                                    : dl?.filename || 'Connecting...'}\n                                </div>\n                              </div>\n                            );\n                          })()}\n                      </div>\n\n                      {/* Right side info */}\n                      <div className=\"shrink-0 flex items-center gap-2\">\n                        {hasError && (\n                          <Badge variant=\"destructive\" className=\"text-[10px] h-5\">\n                            Error\n                          </Badge>\n                        )}\n                        {model.loaded && (\n                          <Badge className=\"text-[10px] h-5 bg-accent/15 text-accent border-accent/30 hover:bg-accent/15\">\n                            Loaded\n                          </Badge>\n                        )}\n                        {model.downloaded && !isDownloading && !hasError && (\n                          <span className=\"text-xs text-muted-foreground\">\n                            {formatSize(model.size_mb)}\n                          </span>\n                        )}\n\n                        <ChevronRight className=\"h-4 w-4 text-muted-foreground/40 group-hover:text-muted-foreground transition-colors\" />\n                      </div>\n                    </button>\n                  );\n                })}\n              </div>\n            </div>\n          ))}\n\n          {/* Error console */}\n          {errorCount > 0 && (\n            <div className=\"border rounded-lg overflow-hidden\">\n              <div className=\"flex items-center justify-between px-3 py-1.5 bg-muted/50 text-xs font-medium text-muted-foreground\">\n                <button\n                  type=\"button\"\n                  onClick={() => setConsoleOpen((v) => !v)}\n                  className=\"flex items-center gap-2 hover:text-foreground transition-colors\"\n                >\n                  {consoleOpen ? (\n                    <ChevronUp className=\"h-3.5 w-3.5\" />\n                  ) : (\n                    <ChevronDown className=\"h-3.5 w-3.5\" />\n                  )}\n                  <span>Problems</span>\n                  <Badge variant=\"destructive\" className=\"text-[10px] h-4 px-1.5 rounded-full\">\n                    {errorCount}\n                  </Badge>\n                </button>\n                <Button\n                  size=\"sm\"\n                  variant=\"ghost\"\n                  className=\"h-6 px-2 text-xs text-muted-foreground hover:text-foreground\"\n                  onClick={() => clearAllMutation.mutate()}\n                  disabled={clearAllMutation.isPending}\n                >\n                  <RotateCcw className=\"h-3 w-3 mr-1\" />\n                  Clear All\n                </Button>\n              </div>\n              {consoleOpen && (\n                <div className=\"bg-[#1e1e1e] text-[#d4d4d4] p-3 max-h-48 overflow-auto font-mono text-xs leading-relaxed\">\n                  {Array.from(erroredDownloads.entries()).map(([modelName, dl]) => (\n                    <div key={modelName} className=\"mb-2 last:mb-0\">\n                      <span className=\"text-[#f44747]\">[error]</span>{' '}\n                      <span className=\"text-[#569cd6]\">{modelName}</span>\n                      {dl.error ? (\n                        <>\n                          {': '}\n                          <span className=\"text-[#ce9178] whitespace-pre-wrap break-all\">\n                            {dl.error}\n                          </span>\n                        </>\n                      ) : (\n                        <>\n                          {': '}\n                          <span className=\"text-[#808080]\">\n                            No error details available. Try downloading again.\n                          </span>\n                        </>\n                      )}\n                      <div className=\"text-[#6a9955] mt-0.5\">\n                        started at {new Date(dl.started_at).toLocaleString()}\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              )}\n            </div>\n          )}\n        </div>\n      ) : null}\n\n      {/* Model Detail Modal */}\n      <Dialog open={detailOpen} onOpenChange={setDetailOpen}>\n        <DialogContent className=\"sm:max-w-md\">\n          {freshSelectedModel && (\n            <>\n              <DialogHeader>\n                <DialogTitle>{freshSelectedModel.display_name}</DialogTitle>\n                <DialogDescription className=\"flex items-center gap-1.5\">\n                  {freshSelectedModel.hf_repo_id ? (\n                    <a\n                      href={`https://huggingface.co/${freshSelectedModel.hf_repo_id}`}\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"inline-flex items-center gap-1 hover:underline\"\n                    >\n                      {freshSelectedModel.hf_repo_id}\n                      <ExternalLink className=\"h-3 w-3\" />\n                    </a>\n                  ) : (\n                    freshSelectedModel.model_name\n                  )}\n                </DialogDescription>\n              </DialogHeader>\n\n              <div className=\"space-y-4 pt-2\">\n                {/* Status badges */}\n                <div className=\"flex items-center gap-2 flex-wrap\">\n                  {freshSelectedModel.loaded && (\n                    <Badge className=\"text-xs bg-accent/15 text-accent border-accent/30 hover:bg-accent/15\">\n                      <CircleCheck className=\"h-3 w-3 mr-1\" />\n                      Loaded\n                    </Badge>\n                  )}\n                  {selectedState?.hasError && (\n                    <Badge variant=\"destructive\" className=\"text-xs\">\n                      <CircleX className=\"h-3 w-3 mr-1\" />\n                      Error\n                    </Badge>\n                  )}\n                </div>\n\n                {/* HuggingFace model card info */}\n                {hfLoading && freshSelectedModel.hf_repo_id && (\n                  <div className=\"flex items-center gap-2 text-xs text-muted-foreground py-2\">\n                    <Loader2 className=\"h-3 w-3 animate-spin\" />\n                    Loading model info...\n                  </div>\n                )}\n\n                {/* Description */}\n                {MODEL_DESCRIPTIONS[freshSelectedModel.model_name] && (\n                  <p className=\"text-xs text-muted-foreground leading-relaxed\">\n                    {MODEL_DESCRIPTIONS[freshSelectedModel.model_name]}\n                  </p>\n                )}\n\n                {hfModelInfo && (\n                  <div className=\"space-y-3\">\n                    {/* Pipeline tag + author */}\n                    <div className=\"flex flex-wrap gap-1.5\">\n                      {hfModelInfo.pipeline_tag && (\n                        <Badge variant=\"outline\" className=\"text-[10px]\">\n                          {formatPipelineTag(hfModelInfo.pipeline_tag)}\n                        </Badge>\n                      )}\n                      {hfModelInfo.library_name && (\n                        <Badge variant=\"outline\" className=\"text-[10px]\">\n                          {hfModelInfo.library_name}\n                        </Badge>\n                      )}\n                      {hfModelInfo.author && (\n                        <Badge variant=\"outline\" className=\"text-[10px]\">\n                          by {hfModelInfo.author}\n                        </Badge>\n                      )}\n                    </div>\n\n                    {/* Stats row */}\n                    <div className=\"flex items-center gap-4 text-xs text-muted-foreground\">\n                      <span className=\"flex items-center gap-1\" title=\"Downloads\">\n                        <Download className=\"h-3.5 w-3.5\" />\n                        {formatDownloads(hfModelInfo.downloads)}\n                      </span>\n                      <span className=\"flex items-center gap-1\" title=\"Likes\">\n                        <Heart className=\"h-3.5 w-3.5\" />\n                        {formatDownloads(hfModelInfo.likes)}\n                      </span>\n                      {license && (\n                        <span className=\"flex items-center gap-1\" title=\"License\">\n                          <Scale className=\"h-3.5 w-3.5\" />\n                          {formatLicense(license)}\n                        </span>\n                      )}\n                    </div>\n\n                    {/* Languages */}\n                    {hfModelInfo.cardData?.language && hfModelInfo.cardData.language.length > 0 && (\n                      <div>\n                        <span className=\"text-xs text-muted-foreground\">\n                          {hfModelInfo.cardData.language.length > 10\n                            ? `${hfModelInfo.cardData.language.length} languages supported`\n                            : `Languages: ${hfModelInfo.cardData.language.join(', ')}`}\n                        </span>\n                      </div>\n                    )}\n                  </div>\n                )}\n\n                {/* Disk size */}\n                {freshSelectedModel.downloaded && freshSelectedModel.size_mb && (\n                  <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n                    <HardDrive className=\"h-3.5 w-3.5\" />\n                    <span>{formatSize(freshSelectedModel.size_mb)} on disk</span>\n                  </div>\n                )}\n\n                {/* Error detail */}\n                {selectedError?.error && (\n                  <div className=\"rounded-md bg-destructive/10 border border-destructive/20 p-3 text-xs text-destructive\">\n                    {selectedError.error}\n                  </div>\n                )}\n\n                {/* Actions */}\n                <div className=\"flex items-center gap-2 pt-2\">\n                  {selectedState?.hasError ? (\n                    <>\n                      <Button\n                        size=\"sm\"\n                        onClick={() => handleDownload(freshSelectedModel.model_name)}\n                        variant=\"outline\"\n                        className=\"flex-1\"\n                      >\n                        <Download className=\"h-4 w-4 mr-2\" />\n                        Retry Download\n                      </Button>\n                      <Button\n                        size=\"sm\"\n                        onClick={() => handleCancel(freshSelectedModel.model_name)}\n                        variant=\"ghost\"\n                        disabled={\n                          cancelMutation.isPending &&\n                          cancelMutation.variables === freshSelectedModel.model_name\n                        }\n                      >\n                        <X className=\"h-4 w-4\" />\n                      </Button>\n                    </>\n                  ) : selectedState?.isDownloading ? (\n                    <>\n                      <div className=\"flex-1 space-y-2\">\n                        {(() => {\n                          const dl = freshSelectedModel\n                            ? downloadProgressMap.get(freshSelectedModel.model_name)\n                            : undefined;\n                          const pct = dl?.progress ?? 0;\n                          const hasProgress = dl && dl.total && dl.total > 0;\n                          return (\n                            <>\n                              <Progress value={hasProgress ? pct : undefined} className=\"h-2\" />\n                              <div className=\"text-xs text-muted-foreground\">\n                                {hasProgress\n                                  ? `${formatBytes(dl.current ?? 0)} / ${formatBytes(dl.total!)} (${pct.toFixed(1)}%)`\n                                  : dl?.filename || 'Connecting to HuggingFace...'}\n                              </div>\n                            </>\n                          );\n                        })()}\n                      </div>\n                      <Button\n                        size=\"sm\"\n                        onClick={() => handleCancel(freshSelectedModel.model_name)}\n                        variant=\"ghost\"\n                        disabled={\n                          cancelMutation.isPending &&\n                          cancelMutation.variables === freshSelectedModel.model_name\n                        }\n                      >\n                        <X className=\"h-4 w-4\" />\n                      </Button>\n                    </>\n                  ) : freshSelectedModel.downloaded ? (\n                    <div className=\"flex gap-2 flex-1\">\n                      {freshSelectedModel.loaded && (\n                        <Button\n                          size=\"sm\"\n                          onClick={() => unloadMutation.mutate(freshSelectedModel.model_name)}\n                          variant=\"outline\"\n                          disabled={unloadMutation.isPending}\n                          className=\"flex-1\"\n                        >\n                          {unloadMutation.isPending ? (\n                            <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                          ) : (\n                            <Unplug className=\"h-4 w-4 mr-2\" />\n                          )}\n                          {unloadMutation.isPending ? 'Unloading...' : 'Unload'}\n                        </Button>\n                      )}\n                      <Button\n                        size=\"sm\"\n                        onClick={() => {\n                          setModelToDelete({\n                            name: freshSelectedModel.model_name,\n                            displayName: freshSelectedModel.display_name,\n                            sizeMb: freshSelectedModel.size_mb,\n                          });\n                          setDeleteDialogOpen(true);\n                        }}\n                        variant=\"outline\"\n                        disabled={freshSelectedModel.loaded}\n                        title={\n                          freshSelectedModel.loaded\n                            ? 'Unload model before deleting'\n                            : 'Delete model'\n                        }\n                        className=\"flex-1\"\n                      >\n                        <Trash2 className=\"h-4 w-4 mr-2\" />\n                        Delete Model\n                      </Button>\n                    </div>\n                  ) : (\n                    <Button\n                      size=\"sm\"\n                      onClick={() => handleDownload(freshSelectedModel.model_name)}\n                      className=\"flex-1\"\n                    >\n                      <Download className=\"h-4 w-4 mr-2\" />\n                      Download\n                    </Button>\n                  )}\n                </div>\n              </div>\n            </>\n          )}\n        </DialogContent>\n      </Dialog>\n\n      {/* Delete Confirmation Dialog */}\n      <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Delete Model</AlertDialogTitle>\n            <AlertDialogDescription>\n              Are you sure you want to delete <strong>{modelToDelete?.displayName}</strong>?\n              {modelToDelete?.sizeMb && (\n                <>\n                  {' '}\n                  This will free up {formatSize(modelToDelete.sizeMb)} of disk space. The model will\n                  need to be re-downloaded if you want to use it again.\n                </>\n              )}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction\n              onClick={() => {\n                if (modelToDelete) {\n                  deleteMutation.mutate(modelToDelete.name);\n                }\n              }}\n              disabled={deleteMutation.isPending}\n              className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n            >\n              {deleteMutation.isPending ? (\n                <>\n                  <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n                  Deleting...\n                </>\n              ) : (\n                'Delete'\n              )}\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n\n      {/* Migration confirmation dialog */}\n      <AlertDialog\n        open={!!pendingMigrateDir}\n        onOpenChange={(open) => !open && setPendingMigrateDir(null)}\n      >\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Move models to new location?</AlertDialogTitle>\n            <AlertDialogDescription>\n              The server will shut down while models are being moved to the new folder. It will\n              restart automatically once the migration is complete.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <div\n            className=\"text-xs font-mono text-muted-foreground bg-muted/50 rounded px-3 py-2 truncate\"\n            title={pendingMigrateDir ?? ''}\n          >\n            {pendingMigrateDir}\n          </div>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction\n              onClick={async () => {\n                if (!pendingMigrateDir) return;\n                const newDir = pendingMigrateDir;\n                setPendingMigrateDir(null);\n                setMigrating(true);\n                setMigrationProgress({\n                  current: 0,\n                  total: 0,\n                  progress: 0,\n                  status: 'downloading',\n                  filename: 'Preparing...',\n                });\n                try {\n                  // Start the migration (background task)\n                  await apiClient.migrateModels(newDir);\n\n                  // Connect to SSE for progress\n                  await new Promise<void>((resolve, reject) => {\n                    const es = new EventSource(apiClient.getMigrationProgressUrl());\n                    es.onmessage = (event) => {\n                      try {\n                        const data = JSON.parse(event.data);\n                        setMigrationProgress(data);\n                        if (data.status === 'complete') {\n                          es.close();\n                          resolve();\n                        } else if (data.status === 'error') {\n                          es.close();\n                          reject(new Error(data.error || 'Migration failed'));\n                        }\n                      } catch {\n                        /* ignore parse errors */\n                      }\n                    };\n                    es.onerror = () => {\n                      es.close();\n                      reject(new Error('Lost connection during migration'));\n                    };\n                  });\n\n                  setCustomModelsDir(newDir);\n                  setMigrationProgress({\n                    current: 1,\n                    total: 1,\n                    progress: 100,\n                    status: 'complete',\n                    filename: 'Restarting server...',\n                  });\n                  await platform.lifecycle.restartServer(newDir);\n                  queryClient.invalidateQueries();\n                  toast({ title: 'Models moved successfully' });\n                } catch (e) {\n                  toast({\n                    title: 'Migration failed',\n                    description: e instanceof Error ? e.message : 'Failed to migrate models',\n                    variant: 'destructive',\n                  });\n                } finally {\n                  setMigrating(false);\n                  setMigrationProgress(null);\n                }\n              }}\n            >\n              Move Models\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n\n      {/* Migration progress overlay */}\n      {migrating && migrationProgress && (\n        <div className=\"fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex items-center justify-center\">\n          <div className=\"w-full max-w-md px-8 space-y-6 text-center\">\n            <div className=\"space-y-2\">\n              <Loader2 className=\"h-8 w-8 animate-spin mx-auto text-muted-foreground\" />\n              <h2 className=\"text-lg font-semibold\">Moving models</h2>\n              <p className=\"text-sm text-muted-foreground\">\n                {migrationProgress.status === 'complete'\n                  ? 'Restarting server...'\n                  : 'The server is offline while models are being moved.'}\n              </p>\n            </div>\n            {migrationProgress.total > 0 && (\n              <div className=\"space-y-2\">\n                <Progress value={migrationProgress.progress} className=\"h-2\" />\n                <div className=\"flex justify-between text-xs text-muted-foreground\">\n                  <span className=\"truncate max-w-[60%]\">{migrationProgress.filename}</span>\n                  <span>\n                    {formatBytes(migrationProgress.current)} /{' '}\n                    {formatBytes(migrationProgress.total)}\n                  </span>\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\ninterface ModelItemProps {\n  model: {\n    model_name: string;\n    display_name: string;\n    downloaded: boolean;\n    downloading?: boolean; // From server - true if download in progress\n    size_mb?: number;\n    loaded: boolean;\n  };\n  onDownload: () => void;\n  onDelete: () => void;\n  isDownloading: boolean; // Local state - true if user just clicked download\n  formatSize: (sizeMb?: number) => string;\n}\n\nfunction ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: ModelItemProps) {\n  // Use server's downloading state OR local state (for immediate feedback before server updates)\n  const showDownloading = model.downloading || isDownloading;\n\n  const statusText = model.loaded\n    ? 'Loaded'\n    : showDownloading\n      ? 'Downloading'\n      : model.downloaded\n        ? 'Downloaded'\n        : 'Not downloaded';\n  const sizeText =\n    model.downloaded && model.size_mb && !showDownloading ? `, ${formatSize(model.size_mb)}` : '';\n  const rowLabel = `${model.display_name}, ${statusText}${sizeText}. Use Tab to reach Download or Delete.`;\n\n  return (\n    <div\n      className=\"flex items-center justify-between p-3 border rounded-lg\"\n      role=\"group\"\n      tabIndex={0}\n      aria-label={rowLabel}\n    >\n      <div className=\"flex-1\">\n        <div className=\"flex items-center gap-2\">\n          <span className=\"font-medium text-sm\">{model.display_name}</span>\n          {model.loaded && (\n            <Badge variant=\"default\" className=\"text-xs\">\n              Loaded\n            </Badge>\n          )}\n          {/* Only show Downloaded if actually downloaded AND not downloading */}\n          {model.downloaded && !model.loaded && !showDownloading && (\n            <Badge variant=\"secondary\" className=\"text-xs\">\n              Downloaded\n            </Badge>\n          )}\n        </div>\n        {model.downloaded && model.size_mb && !showDownloading && (\n          <div className=\"text-xs text-muted-foreground mt-1\">\n            Size: {formatSize(model.size_mb)}\n          </div>\n        )}\n      </div>\n      <div className=\"flex items-center gap-2\">\n        {model.downloaded && !showDownloading ? (\n          <div className=\"flex items-center gap-2\">\n            <div className=\"flex items-center gap-1 text-sm text-muted-foreground\">\n              <span>Ready</span>\n            </div>\n            <Button\n              size=\"sm\"\n              onClick={onDelete}\n              variant=\"outline\"\n              disabled={model.loaded}\n              title={model.loaded ? 'Unload model before deleting' : 'Delete model'}\n              aria-label={\n                model.loaded ? 'Unload model before deleting' : `Delete ${model.display_name}`\n              }\n            >\n              <Trash2 className=\"h-4 w-4\" />\n            </Button>\n          </div>\n        ) : showDownloading ? (\n          <Button\n            size=\"sm\"\n            variant=\"outline\"\n            disabled\n            aria-label={`${model.display_name} downloading`}\n          >\n            <Loader2 className=\"h-4 w-4 mr-2 animate-spin\" />\n            Downloading...\n          </Button>\n        ) : (\n          <Button\n            size=\"sm\"\n            onClick={onDownload}\n            variant=\"outline\"\n            aria-label={`Download ${model.display_name}`}\n          >\n            <Download className=\"h-4 w-4 mr-2\" />\n            Download\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerSettings/ModelProgress.tsx",
    "content": "import { Loader2, XCircle } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Progress } from '@/components/ui/progress';\nimport type { ModelProgress as ModelProgressType } from '@/lib/api/types';\nimport { useServerStore } from '@/stores/serverStore';\n\ninterface ModelProgressProps {\n  modelName: string;\n  displayName: string;\n  /** Only connect to SSE when actively downloading - prevents connection exhaustion */\n  isDownloading?: boolean;\n}\n\nexport function ModelProgress({ modelName, displayName, isDownloading = false }: ModelProgressProps) {\n  const [progress, setProgress] = useState<ModelProgressType | null>(null);\n  const serverUrl = useServerStore((state) => state.serverUrl);\n\n  useEffect(() => {\n    // IMPORTANT: Only connect to SSE when this specific model is downloading\n    // Opening SSE connections for all models exhausts HTTP/1.1 connection limits (6 per origin)\n    // which causes other fetches (like the download trigger) to be queued/blocked\n    if (!serverUrl || !isDownloading) {\n      return;\n    }\n\n    console.log(`[ModelProgress] Connecting SSE for ${modelName}`);\n\n    // Subscribe to progress updates via Server-Sent Events\n    const eventSource = new EventSource(`${serverUrl}/models/progress/${modelName}`);\n\n    eventSource.onmessage = (event) => {\n      try {\n        const data = JSON.parse(event.data) as ModelProgressType;\n        setProgress(data);\n\n        // Close connection if complete or error\n        if (data.status === 'complete' || data.status === 'error') {\n          console.log(`[ModelProgress] Download ${data.status} for ${modelName}, closing SSE`);\n          eventSource.close();\n        }\n      } catch (error) {\n        console.error('Error parsing progress event:', error);\n      }\n    };\n\n    eventSource.onerror = (error) => {\n      console.error(`[ModelProgress] SSE error for ${modelName}:`, error);\n      eventSource.close();\n    };\n\n    return () => {\n      console.log(`[ModelProgress] Cleanup - closing SSE for ${modelName}`);\n      eventSource.close();\n    };\n  }, [serverUrl, modelName, isDownloading]);\n\n  // Don't render if no progress or if complete/error and some time has passed\n  if (\n    !progress ||\n    (progress.status === 'complete' && Date.now() - new Date(progress.timestamp).getTime() > 5000)\n  ) {\n    return null;\n  }\n\n  const formatBytes = (bytes: number): string => {\n    if (bytes === 0) return '0 B';\n    const k = 1024;\n    const sizes = ['B', 'KB', 'MB', 'GB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;\n  };\n\n  const getStatusIcon = () => {\n    switch (progress.status) {\n      case 'error':\n        return <XCircle className=\"h-4 w-4 text-destructive\" />;\n      case 'downloading':\n      case 'extracting':\n        return <Loader2 className=\"h-4 w-4 animate-spin\" />;\n      default:\n        return null;\n    }\n  };\n\n  const getStatusText = () => {\n    switch (progress.status) {\n      case 'complete':\n        return 'Download complete';\n      case 'error':\n        return `Error: ${progress.error || 'Unknown error'}`;\n      case 'downloading':\n        return progress.filename ? `Downloading ${progress.filename}...` : 'Downloading...';\n      case 'extracting':\n        return 'Extracting...';\n      default:\n        return 'Processing...';\n    }\n  };\n\n  return (\n    <Card className=\"mb-4\">\n      <CardHeader className=\"pb-3\">\n        <CardTitle className=\"text-sm font-medium flex items-center gap-2\">\n          {getStatusIcon()}\n          {displayName}\n        </CardTitle>\n      </CardHeader>\n      <CardContent className=\"space-y-2\">\n        <div className=\"space-y-1\">\n          <div className=\"flex justify-between text-xs text-muted-foreground\">\n            <span>{getStatusText()}</span>\n            {progress.total > 0 && (\n              <span>\n                {formatBytes(progress.current)} / {formatBytes(progress.total)} (\n                {progress.progress.toFixed(1)}%)\n              </span>\n            )}\n          </div>\n          {progress.total > 0 && <Progress value={progress.progress} className=\"h-2\" />}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerSettings/ServerStatus.tsx",
    "content": "import { Loader2, XCircle } from 'lucide-react';\nimport { Badge } from '@/components/ui/badge';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { useServerHealth } from '@/lib/hooks/useServer';\nimport { useServerStore } from '@/stores/serverStore';\n\nexport function ServerStatus() {\n  const { data: health, isLoading, error } = useServerHealth();\n  const serverUrl = useServerStore((state) => state.serverUrl);\n\n  return (\n    <Card role=\"region\" aria-label=\"Server Status\" tabIndex={0}>\n      <CardHeader>\n        <CardTitle>Server Status</CardTitle>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        <div>\n          <div className=\"text-sm text-muted-foreground mb-1\">Server URL</div>\n          <div className=\"font-mono text-sm\">{serverUrl}</div>\n        </div>\n\n        {isLoading ? (\n          <div className=\"flex items-center gap-2\">\n            <Loader2 className=\"h-4 w-4 animate-spin\" />\n            <span className=\"text-sm\">Checking connection...</span>\n          </div>\n        ) : error ? (\n          <div className=\"flex items-center gap-2\">\n            <XCircle className=\"h-4 w-4 text-destructive\" />\n            <span className=\"text-sm text-destructive\">Connection failed: {error.message}</span>\n          </div>\n        ) : health ? (\n          <div className=\"space-y-2\">\n            <div className=\"flex items-center gap-2\">\n              <span className=\"text-sm\">Connected</span>\n            </div>\n            <div className=\"flex flex-wrap gap-2\">\n              <Badge\n                variant={health.model_loaded || health.model_downloaded ? 'default' : 'secondary'}\n              >\n                {health.model_loaded || health.model_downloaded ? 'Model Ready' : 'No Model'}\n              </Badge>\n              <Badge variant={health.gpu_available ? 'default' : 'secondary'}>\n                GPU: {health.gpu_available ? 'Available' : 'Not Available'}\n              </Badge>\n              {health.vram_used_mb && (\n                <Badge variant=\"outline\">VRAM: {health.vram_used_mb.toFixed(0)} MB</Badge>\n              )}\n            </div>\n          </div>\n        ) : null}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerSettings/UpdateStatus.tsx",
    "content": "import { AlertCircle, Download, RefreshCw } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Progress } from '@/components/ui/progress';\nimport { useAutoUpdater } from '@/hooks/useAutoUpdater';\nimport { usePlatform } from '@/platform/PlatformContext';\n\nexport function UpdateStatus() {\n  const platform = usePlatform();\n  const { status, checkForUpdates, downloadAndInstall, restartAndInstall } = useAutoUpdater(false);\n  const [currentVersion, setCurrentVersion] = useState<string>('');\n  const isDev = !import.meta.env?.PROD;\n\n  useEffect(() => {\n    platform.metadata\n      .getVersion()\n      .then(setCurrentVersion)\n      .catch(() => setCurrentVersion('Unknown'));\n  }, [platform]);\n\n  return (\n    <Card role=\"region\" aria-label=\"App Updates\" tabIndex={0}>\n      <CardHeader>\n        <CardTitle>App Updates</CardTitle>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"space-y-1\">\n            <div className=\"text-sm font-medium\">Current Version</div>\n            <div className=\"text-sm text-muted-foreground\">\n              v{currentVersion}\n              {isDev ? ' (dev)' : ''}\n            </div>\n          </div>\n          {!isDev && (\n            <Button\n              onClick={checkForUpdates}\n              disabled={status.checking || status.downloading || status.readyToInstall}\n              variant=\"outline\"\n              size=\"sm\"\n            >\n              <RefreshCw className={`h-4 w-4 mr-2 ${status.checking ? 'animate-spin' : ''}`} />\n              Check for Updates\n            </Button>\n          )}\n        </div>\n\n        {isDev ? (\n          <div className=\"text-sm text-muted-foreground\">\n            Auto-updates are disabled in development mode.\n          </div>\n        ) : (\n          <>\n            {status.checking && (\n              <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n                <RefreshCw className=\"h-4 w-4 animate-spin\" />\n                Checking for updates...\n              </div>\n            )}\n\n            {status.error && (\n              <div className=\"flex items-center gap-2 text-sm text-destructive\">\n                <AlertCircle className=\"h-4 w-4\" />\n                {status.error}\n              </div>\n            )}\n\n            {status.available && !status.downloading && !status.readyToInstall && (\n              <div className=\"space-y-3 p-4 border rounded-lg bg-primary/5\">\n                <div className=\"flex items-center justify-between\">\n                  <div>\n                    <div className=\"font-semibold\">Update Available</div>\n                    <div className=\"text-sm text-muted-foreground\">Version {status.version}</div>\n                  </div>\n                  <Badge>New</Badge>\n                </div>\n                <Button onClick={downloadAndInstall} className=\"w-full\" size=\"sm\">\n                  <Download className=\"h-4 w-4 mr-2\" />\n                  Download Update\n                </Button>\n              </div>\n            )}\n\n            {status.downloading && (\n              <div className=\"space-y-2\">\n                <div className=\"flex items-center justify-between text-sm\">\n                  <div className=\"flex items-center gap-2\">\n                    <Download className=\"h-4 w-4\" />\n                    Downloading update...\n                  </div>\n                  {status.downloadProgress !== undefined && (\n                    <span className=\"text-muted-foreground\">{status.downloadProgress}%</span>\n                  )}\n                </div>\n                <Progress value={status.downloadProgress} />\n                {status.downloadedBytes !== undefined &&\n                  status.totalBytes !== undefined &&\n                  status.totalBytes > 0 && (\n                    <div className=\"text-xs text-muted-foreground\">\n                      {(status.downloadedBytes / 1024 / 1024).toFixed(1)} MB /{' '}\n                      {(status.totalBytes / 1024 / 1024).toFixed(1)} MB\n                    </div>\n                  )}\n              </div>\n            )}\n\n            {status.readyToInstall && (\n              <div className=\"space-y-3 p-4 border rounded-lg bg-accent/30 border-accent/50\">\n                <div className=\"flex items-center gap-2\">\n                  <div>\n                    <div className=\"font-semibold\">Update Ready to Install</div>\n                    <div className=\"text-sm text-muted-foreground\">\n                      Version {status.version} has been downloaded\n                    </div>\n                  </div>\n                </div>\n                <div className=\"text-sm text-muted-foreground\">\n                  The app needs to restart to complete the installation. You can do this now or\n                  later at your convenience.\n                </div>\n                <Button onClick={restartAndInstall} className=\"w-full\" size=\"sm\">\n                  <RefreshCw className=\"h-4 w-4 mr-2\" />\n                  Restart Now\n                </Button>\n              </div>\n            )}\n\n            {!status.available && !status.checking && !status.error && (\n              <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n                You're up to date\n              </div>\n            )}\n          </>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerTab/AboutPage.tsx",
    "content": "import { ArrowUpRight } from 'lucide-react';\nimport type { CSSProperties, ReactNode } from 'react';\nimport { useEffect, useState } from 'react';\nimport voiceboxLogo from '@/assets/voicebox-logo.png';\nimport { usePlatform } from '@/platform/PlatformContext';\n\nfunction FadeIn({ delay = 0, children }: { delay?: number; children: ReactNode }) {\n  return (\n    <div\n      className=\"animate-[fadeInUp_0.5s_ease_both]\"\n      style={{ animationDelay: `${delay}ms` } as CSSProperties}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function AboutPage() {\n  const platform = usePlatform();\n  const [version, setVersion] = useState('');\n\n  useEffect(() => {\n    platform.metadata\n      .getVersion()\n      .then(setVersion)\n      .catch(() => setVersion(''));\n  }, [platform]);\n\n  return (\n    <>\n      <style>{`\n        @keyframes fadeInUp {\n          from {\n            opacity: 0;\n            transform: translateY(8px);\n          }\n          to {\n            opacity: 1;\n            transform: translateY(0);\n          }\n        }\n      `}</style>\n      <div className=\"max-w-md mx-auto h-full flex items-center\">\n        <div className=\"flex flex-col items-center text-center space-y-5\">\n          <FadeIn delay={0}>\n            <img src={voiceboxLogo} alt=\"Voicebox\" className=\"w-20 h-20 object-contain\" />\n          </FadeIn>\n\n          <FadeIn delay={80}>\n            <div className=\"space-y-1.5\">\n              <h1 className=\"text-lg font-semibold\">Voicebox</h1>\n              <p className=\"text-xs text-muted-foreground/60 h-4\">\n                {version ? `v${version}` : '\\u00A0'}\n              </p>\n            </div>\n          </FadeIn>\n\n          <FadeIn delay={160}>\n            <p className=\"text-sm text-muted-foreground leading-relaxed max-w-sm\">\n              The open-source voice synthesis studio. Clone voices, generate speech, apply effects,\n              and build voice-powered apps — all running locally on your machine.\n            </p>\n          </FadeIn>\n\n          <FadeIn delay={240}>\n            <div className=\"flex items-center gap-1.5 text-sm text-muted-foreground\">\n              <span>Created by</span>\n              <a\n                href=\"https://github.com/jamiepine\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"text-accent hover:underline\"\n              >\n                Jamie Pine\n              </a>\n            </div>\n          </FadeIn>\n\n          <FadeIn delay={320}>\n            <div className=\"flex flex-wrap justify-center gap-3 pt-2\">\n              <a\n                href=\"https://buymeacoffee.com/jamiepine\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"group inline-flex items-center gap-2 rounded-lg border border-border/60 px-4 py-2 text-sm transition-colors hover:bg-muted/50\"\n              >\n                <svg\n                  className=\"h-4 w-4 text-[#FFDD00]\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"currentColor\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"m20.216 6.415-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 0 0-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 0 0-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 0 1-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 0 1 3.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 0 1-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 0 1-4.743.295 37.059 37.059 0 0 1-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0 0 11.343.376.483.483 0 0 1 .535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 0 1 .39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 0 1-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 0 1-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 0 0-1.322-.238c-.826 0-1.491.284-2.26.613z\" />\n                </svg>\n                Buy me a coffee\n                <ArrowUpRight className=\"h-3.5 w-3.5 text-muted-foreground/40 group-hover:text-muted-foreground transition-colors\" />\n              </a>\n              <a\n                href=\"https://github.com/jamiepine/voicebox\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"group inline-flex items-center gap-2 rounded-lg border border-border/60 px-4 py-2 text-sm transition-colors hover:bg-muted/50\"\n              >\n                <svg\n                  className=\"h-4 w-4 text-muted-foreground\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"currentColor\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z\" />\n                </svg>\n                GitHub\n                <ArrowUpRight className=\"h-3.5 w-3.5 text-muted-foreground/40 group-hover:text-muted-foreground transition-colors\" />\n              </a>\n            </div>\n          </FadeIn>\n\n          <FadeIn delay={400}>\n            <p className=\"text-xs text-muted-foreground/40 pt-4\">\n              Licensed under{' '}\n              <a\n                href=\"https://github.com/jamiepine/voicebox/blob/main/LICENSE\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"hover:text-muted-foreground/60 transition-colors\"\n              >\n                MIT\n              </a>\n            </p>\n          </FadeIn>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerTab/ChangelogPage.tsx",
    "content": "import changelogRaw from 'virtual:changelog';\nimport { useMemo, useState } from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { type ChangelogEntry, parseChangelog } from '@/lib/utils/parseChangelog';\n\nfunction renderMarkdown(md: string): React.ReactNode[] {\n  const lines = md.split('\\n');\n  const elements: React.ReactNode[] = [];\n  let i = 0;\n\n  while (i < lines.length) {\n    const line = lines[i];\n\n    // Skip empty lines\n    if (line.trim() === '') {\n      i++;\n      continue;\n    }\n\n    // Tables — collect all lines starting with |\n    if (line.trim().startsWith('|')) {\n      const tableLines: string[] = [];\n      while (i < lines.length && lines[i].trim().startsWith('|')) {\n        tableLines.push(lines[i]);\n        i++;\n      }\n      elements.push(renderTable(tableLines, elements.length));\n      continue;\n    }\n\n    // Headings\n    if (line.startsWith('#### ')) {\n      elements.push(\n        <h5 key={elements.length} className=\"text-sm font-medium mt-5 mb-1\">\n          {inlineMarkdown(line.slice(5))}\n        </h5>,\n      );\n      i++;\n      continue;\n    }\n    if (line.startsWith('### ')) {\n      elements.push(\n        <h4 key={elements.length} className=\"text-sm font-medium mt-6 mb-2\">\n          {inlineMarkdown(line.slice(4))}\n        </h4>,\n      );\n      i++;\n      continue;\n    }\n\n    // List items — collect consecutive\n    if (line.startsWith('- ')) {\n      const items: string[] = [];\n      while (i < lines.length && lines[i].startsWith('- ')) {\n        items.push(lines[i].slice(2));\n        i++;\n      }\n      elements.push(\n        <ul key={elements.length} className=\"space-y-1 my-2\">\n          {items.map((item, idx) => (\n            <li key={idx} className=\"text-sm text-muted-foreground flex gap-2\">\n              <span className=\"text-muted-foreground/50 select-none shrink-0\">&bull;</span>\n              <span>{inlineMarkdown(item)}</span>\n            </li>\n          ))}\n        </ul>,\n      );\n      continue;\n    }\n\n    // Paragraph\n    elements.push(\n      <p key={elements.length} className=\"text-sm text-muted-foreground my-2\">\n        {inlineMarkdown(line)}\n      </p>,\n    );\n    i++;\n  }\n\n  return elements;\n}\n\nfunction renderTable(tableLines: string[], keyBase: number): React.ReactNode {\n  const parseRow = (line: string) =>\n    line\n      .split('|')\n      .slice(1, -1)\n      .map((c) => c.trim());\n\n  const headers = parseRow(tableLines[0]);\n  // Skip separator line (index 1)\n  const rows = tableLines.slice(2).map(parseRow);\n\n  return (\n    <div key={keyBase} className=\"overflow-x-auto my-3\">\n      <table className=\"text-sm w-full\">\n        <thead>\n          <tr className=\"border-b\">\n            {headers.map((h, hIdx) => (\n              <th\n                key={hIdx}\n                className=\"text-left py-1.5 pr-4 text-muted-foreground font-medium text-xs\"\n              >\n                {inlineMarkdown(h)}\n              </th>\n            ))}\n          </tr>\n        </thead>\n        <tbody>\n          {rows.map((row, rowIdx) => (\n            <tr key={rowIdx} className=\"border-b border-border/50\">\n              {row.map((cell, cellIdx) => (\n                <td key={cellIdx} className=\"py-1.5 pr-4 text-muted-foreground\">\n                  {inlineMarkdown(cell)}\n                </td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n\nfunction inlineMarkdown(text: string): React.ReactNode {\n  // Process inline markdown: bold, code, links\n  const parts: React.ReactNode[] = [];\n  // Regex matches: **bold**, `code`, [text](url)\n  const inlineRe = /\\*\\*(.+?)\\*\\*|`([^`]+)`|\\[([^\\]]+)\\]\\(([^)]+)\\)/g;\n  let lastIndex = 0;\n  let match: RegExpExecArray | null = inlineRe.exec(text);\n\n  while (match !== null) {\n    if (match.index > lastIndex) {\n      parts.push(text.slice(lastIndex, match.index));\n    }\n\n    if (match[1] !== undefined) {\n      // Bold\n      parts.push(\n        <strong key={parts.length} className=\"font-medium text-foreground\">\n          {match[1]}\n        </strong>,\n      );\n    } else if (match[2] !== undefined) {\n      // Code\n      parts.push(\n        <code key={parts.length} className=\"px-1 py-0.5 rounded bg-muted text-xs font-mono\">\n          {match[2]}\n        </code>,\n      );\n    } else if (match[3] !== undefined && match[4] !== undefined) {\n      // Link\n      parts.push(\n        <a\n          key={parts.length}\n          href={match[4]}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"text-accent hover:underline\"\n        >\n          {match[3]}\n        </a>,\n      );\n    }\n\n    lastIndex = match.index + match[0].length;\n    match = inlineRe.exec(text);\n  }\n\n  if (lastIndex < text.length) {\n    parts.push(text.slice(lastIndex));\n  }\n\n  return parts.length === 1 ? parts[0] : parts;\n}\n\nfunction ChangelogEntryCard({ entry }: { entry: ChangelogEntry }) {\n  const [expanded, setExpanded] = useState(false);\n  const content = useMemo(() => renderMarkdown(entry.body), [entry.body]);\n  const isLong = entry.body.split('\\n').length > 12;\n\n  return (\n    <div className=\"border-b border-border/50 pb-6\">\n      <div className=\"flex items-baseline gap-3 mb-1\">\n        <h3 className=\"text-sm font-medium\">{entry.version}</h3>\n        {entry.date && <span className=\"text-xs text-muted-foreground\">{entry.date}</span>}\n        {entry.version === 'Unreleased' && <Badge variant=\"outline\">dev</Badge>}\n      </div>\n\n      <div className={isLong && !expanded ? 'max-h-48 overflow-hidden relative' : ''}>\n        {content}\n        {isLong && !expanded && (\n          <div className=\"absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-background to-transparent\" />\n        )}\n      </div>\n\n      {isLong && (\n        <button\n          onClick={() => setExpanded(!expanded)}\n          className=\"text-xs text-accent hover:underline mt-2\"\n        >\n          {expanded ? 'Show less' : 'Show more'}\n        </button>\n      )}\n    </div>\n  );\n}\n\nexport function ChangelogPage() {\n  const entries = useMemo(() => parseChangelog(changelogRaw), []);\n\n  return (\n    <div className=\"space-y-6 max-w-2xl\">\n      {entries.map((entry) => (\n        <ChangelogEntryCard key={entry.version} entry={entry} />\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerTab/GeneralPage.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod';\nimport { AlertCircle, ArrowUpRight, Book, Download, Loader2, RefreshCw } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport * as z from 'zod';\nimport { Button } from '@/components/ui/button';\nimport { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';\nimport { Input } from '@/components/ui/input';\nimport { Progress } from '@/components/ui/progress';\nimport { Toggle } from '@/components/ui/toggle';\nimport { useToast } from '@/components/ui/use-toast';\nimport { useAutoUpdater } from '@/hooks/useAutoUpdater';\nimport { useServerHealth } from '@/lib/hooks/useServer';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { useServerStore } from '@/stores/serverStore';\nimport { SettingRow, SettingSection } from './SettingRow';\n\nconst connectionSchema = z.object({\n  serverUrl: z.string().url('Please enter a valid URL'),\n});\n\ntype ConnectionFormValues = z.infer<typeof connectionSchema>;\n\nexport function GeneralPage() {\n  const platform = usePlatform();\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const setServerUrl = useServerStore((state) => state.setServerUrl);\n  const keepServerRunningOnClose = useServerStore((state) => state.keepServerRunningOnClose);\n  const setKeepServerRunningOnClose = useServerStore((state) => state.setKeepServerRunningOnClose);\n  const mode = useServerStore((state) => state.mode);\n  const setMode = useServerStore((state) => state.setMode);\n  const { toast } = useToast();\n  const { data: health, isLoading, error: healthError } = useServerHealth();\n\n  const form = useForm<ConnectionFormValues>({\n    resolver: zodResolver(connectionSchema),\n    defaultValues: { serverUrl },\n  });\n\n  useEffect(() => {\n    form.reset({ serverUrl });\n  }, [serverUrl, form]);\n\n  const { isDirty } = form.formState;\n\n  function onSubmit(data: ConnectionFormValues) {\n    setServerUrl(data.serverUrl);\n    form.reset(data);\n    toast({\n      title: 'Server URL updated',\n      description: `Connected to ${data.serverUrl}`,\n    });\n  }\n\n  return (\n    <div className=\"space-y-8 max-w-2xl\">\n      <div className=\"grid grid-cols-2 gap-3\">\n        <a\n          href=\"https://docs.voicebox.sh\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"group flex items-center gap-3 rounded-lg border border-border/60 p-4 transition-colors hover:bg-muted/50\"\n        >\n          <Book className=\"h-5 w-5 shrink-0 text-accent\" strokeWidth={2.5} />\n          <div className=\"min-w-0 flex-1\">\n            <div className=\"text-sm font-medium\">Read the Docs</div>\n            <div className=\"text-xs text-muted-foreground\">docs.voicebox.sh</div>\n          </div>\n          <ArrowUpRight className=\"h-4 w-4 text-muted-foreground/40 group-hover:text-muted-foreground transition-colors\" />\n        </a>\n        <a\n          href=\"https://discord.gg/StkzQasqPS\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"group flex items-center gap-3 rounded-lg border border-border/60 p-4 transition-colors hover:bg-muted/50\"\n        >\n          <svg\n            className=\"h-5 w-5 shrink-0 text-accent\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.095 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z\" />\n          </svg>\n          <div className=\"min-w-0 flex-1\">\n            <div className=\"text-sm font-medium\">Join the Discord</div>\n            <div className=\"text-xs text-muted-foreground\">Get help & share voices</div>\n          </div>\n          <ArrowUpRight className=\"h-4 w-4 text-muted-foreground/40 group-hover:text-muted-foreground transition-colors\" />\n        </a>\n      </div>\n\n      <SettingSection>\n        <SettingRow\n          title=\"Server URL\"\n          description=\"The address of your voicebox backend server.\"\n          action={\n            <ConnectionStatus health={health} isLoading={isLoading} healthError={healthError} />\n          }\n        >\n          <Form {...form}>\n            <form onSubmit={form.handleSubmit(onSubmit)} className=\"flex gap-2\">\n              <FormField\n                control={form.control}\n                name=\"serverUrl\"\n                render={({ field }) => (\n                  <FormItem className=\"flex-1\">\n                    <FormControl>\n                      <Input placeholder=\"http://127.0.0.1:17493\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              {isDirty && (\n                <Button type=\"submit\" size=\"sm\">\n                  Save\n                </Button>\n              )}\n            </form>\n          </Form>\n        </SettingRow>\n\n        <SettingRow\n          title=\"Keep server running when app closes\"\n          description=\"The server will continue running in the background after closing the app.\"\n          htmlFor=\"keepServerRunning\"\n          action={\n            <Toggle\n              id=\"keepServerRunning\"\n              checked={keepServerRunningOnClose}\n              onCheckedChange={(checked: boolean) => {\n                setKeepServerRunningOnClose(checked);\n                platform.lifecycle.setKeepServerRunning(checked).catch((error) => {\n                  console.error('Failed to sync setting to Rust:', error);\n                  setKeepServerRunningOnClose(!checked);\n                  toast({\n                    title: 'Failed to update setting',\n                    description: 'Could not sync setting to backend.',\n                    variant: 'destructive',\n                  });\n                  return;\n                });\n                toast({\n                  title: 'Setting updated',\n                  description: checked\n                    ? 'Server will continue running when app closes'\n                    : 'Server will stop when app closes',\n                });\n              }}\n            />\n          }\n        />\n\n        {platform.metadata.isTauri && (\n          <SettingRow\n            title=\"Allow network access\"\n            description=\"Makes the server accessible from other devices on your network. Restart the app after changing.\"\n            htmlFor=\"allowNetworkAccess\"\n            action={\n              <Toggle\n                id=\"allowNetworkAccess\"\n                checked={mode === 'remote'}\n                onCheckedChange={(checked: boolean) => {\n                  setMode(checked ? 'remote' : 'local');\n                  toast({\n                    title: 'Setting updated',\n                    description: checked\n                      ? 'Network access enabled. Restart the app to apply.'\n                      : 'Network access disabled. Restart the app to apply.',\n                  });\n                }}\n              />\n            }\n          />\n        )}\n      </SettingSection>\n\n      <ApiReferenceCard serverUrl={serverUrl} />\n\n      {platform.metadata.isTauri && <UpdatesSection />}\n    </div>\n  );\n}\n\nfunction ConnectionStatus({\n  health,\n  isLoading,\n  healthError,\n}: {\n  health: ReturnType<typeof useServerHealth>['data'];\n  isLoading: boolean;\n  healthError: ReturnType<typeof useServerHealth>['error'];\n}) {\n  if (isLoading) {\n    return (\n      <div className=\"flex items-center gap-2 rounded-full border border-border/60 px-3 py-1\">\n        <Loader2 className=\"h-3 w-3 animate-spin text-muted-foreground\" />\n        <span className=\"text-xs text-muted-foreground\">Connecting</span>\n      </div>\n    );\n  }\n  if (healthError) {\n    return (\n      <div className=\"flex items-center gap-2 rounded-full border border-destructive/30 px-3 py-1\">\n        <span className=\"relative flex h-2 w-2\">\n          <span className=\"absolute inline-flex h-full w-full rounded-full bg-destructive/40\" />\n          <span className=\"relative inline-flex h-2 w-2 rounded-full bg-destructive\" />\n        </span>\n        <span className=\"text-xs text-destructive\">Offline</span>\n      </div>\n    );\n  }\n  if (health) {\n    return (\n      <div className=\"flex items-center gap-2 rounded-full border border-accent/30 px-3 py-1\">\n        <span className=\"relative flex h-2 w-2\">\n          <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-accent/60\" />\n          <span className=\"relative inline-flex h-2 w-2 rounded-full bg-accent shadow-[0_0_6px_1px_hsl(var(--accent)/0.5)]\" />\n        </span>\n        <span className=\"text-xs text-muted-foreground\">Online</span>\n      </div>\n    );\n  }\n  return null;\n}\n\nfunction UpdatesSection() {\n  const platform = usePlatform();\n  const { status, checkForUpdates, downloadAndInstall, restartAndInstall } = useAutoUpdater(false);\n  const [currentVersion, setCurrentVersion] = useState<string>('');\n  const isDev = !import.meta.env?.PROD;\n\n  useEffect(() => {\n    platform.metadata\n      .getVersion()\n      .then(setCurrentVersion)\n      .catch(() => setCurrentVersion('Unknown'));\n  }, [platform]);\n\n  return (\n    <SettingSection title=\"App Updates\" description={`v${currentVersion}${isDev ? ' (dev)' : ''}`}>\n      {isDev ? (\n        <SettingRow\n          title=\"Development mode\"\n          description=\"Auto-updates are disabled in development mode.\"\n        />\n      ) : (\n        <>\n          <SettingRow\n            title=\"Check for updates\"\n            description={\n              status.available\n                ? `Version ${status.version} available`\n                : status.checking\n                  ? 'Checking...'\n                  : \"You're up to date\"\n            }\n            action={\n              <Button\n                onClick={checkForUpdates}\n                disabled={status.checking || status.downloading || status.readyToInstall}\n                variant=\"outline\"\n                size=\"sm\"\n              >\n                <RefreshCw\n                  className={`h-3.5 w-3.5 mr-1.5 ${status.checking ? 'animate-spin' : ''}`}\n                />\n                Check\n              </Button>\n            }\n          />\n\n          {status.error && (\n            <SettingRow title=\"Update error\">\n              <div className=\"flex items-center gap-2 text-sm text-destructive\">\n                <AlertCircle className=\"h-4 w-4\" />\n                {status.error}\n              </div>\n            </SettingRow>\n          )}\n\n          {status.available && !status.downloading && !status.readyToInstall && (\n            <SettingRow\n              title={`Update to ${status.version}`}\n              description=\"Download and install the latest version.\"\n              action={\n                <Button onClick={downloadAndInstall} size=\"sm\">\n                  <Download className=\"h-3.5 w-3.5 mr-1.5\" />\n                  Download\n                </Button>\n              }\n            />\n          )}\n\n          {status.downloading && (\n            <SettingRow title=\"Downloading update...\">\n              <div className=\"space-y-1.5\">\n                <Progress value={status.downloadProgress} />\n                <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n                  {status.downloadedBytes !== undefined &&\n                  status.totalBytes !== undefined &&\n                  status.totalBytes > 0 ? (\n                    <span>\n                      {(status.downloadedBytes / 1024 / 1024).toFixed(1)} MB /{' '}\n                      {(status.totalBytes / 1024 / 1024).toFixed(1)} MB\n                    </span>\n                  ) : (\n                    <span />\n                  )}\n                  {status.downloadProgress !== undefined && <span>{status.downloadProgress}%</span>}\n                </div>\n              </div>\n            </SettingRow>\n          )}\n\n          {status.readyToInstall && (\n            <SettingRow\n              title=\"Update ready to install\"\n              description={`Version ${status.version} has been downloaded. Restart to complete.`}\n              action={\n                <Button onClick={restartAndInstall} size=\"sm\">\n                  <RefreshCw className=\"h-3.5 w-3.5 mr-1.5\" />\n                  Restart Now\n                </Button>\n              }\n            />\n          )}\n        </>\n      )}\n    </SettingSection>\n  );\n}\n\nconst API_ENDPOINTS = [\n  { method: 'POST', path: '/generate', label: 'Generate speech' },\n  { method: 'GET', path: '/health', label: 'Server status' },\n  { method: 'GET', path: '/profiles', label: 'List voices' },\n  { method: 'GET', path: '/history', label: 'Past generations' },\n];\n\nfunction ApiReferenceCard({ serverUrl }: { serverUrl: string }) {\n  return (\n    <div className=\"rounded-lg border border-border/60 p-4 space-y-3\">\n      <div>\n        <h3 className=\"text-sm font-medium\">API Access</h3>\n        <p className=\"text-sm text-muted-foreground\">\n          Integrate Voicebox into your workflow via the REST API at{' '}\n          <code className=\"text-xs bg-muted px-1 py-0.5 rounded font-mono\">{serverUrl}</code>\n        </p>\n      </div>\n      <div className=\"space-y-1\">\n        {API_ENDPOINTS.map((ep) => (\n          <div key={ep.path} className=\"flex items-center gap-2.5 py-1\">\n            <span\n              className={`text-[10px] font-mono font-semibold w-9 text-center rounded px-1 py-px ${\n                ep.method === 'POST' ? 'bg-accent/10 text-accent' : 'bg-muted text-muted-foreground'\n              }`}\n            >\n              {ep.method}\n            </span>\n            <code className=\"text-xs font-mono text-muted-foreground\">{ep.path}</code>\n            <span className=\"text-xs text-muted-foreground/50 ml-auto\">{ep.label}</span>\n          </div>\n        ))}\n      </div>\n      <p className=\"text-xs text-muted-foreground\">\n        <a\n          href={`${serverUrl}/docs`}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"text-accent hover:underline\"\n        >\n          View the full API reference\n        </a>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerTab/GenerationPage.tsx",
    "content": "import { FolderOpen } from 'lucide-react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Slider } from '@/components/ui/slider';\nimport { Toggle } from '@/components/ui/toggle';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { useServerStore } from '@/stores/serverStore';\nimport { SettingRow, SettingSection } from './SettingRow';\n\nexport function GenerationPage() {\n  const platform = usePlatform();\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const maxChunkChars = useServerStore((state) => state.maxChunkChars);\n  const setMaxChunkChars = useServerStore((state) => state.setMaxChunkChars);\n  const crossfadeMs = useServerStore((state) => state.crossfadeMs);\n  const setCrossfadeMs = useServerStore((state) => state.setCrossfadeMs);\n  const normalizeAudio = useServerStore((state) => state.normalizeAudio);\n  const setNormalizeAudio = useServerStore((state) => state.setNormalizeAudio);\n  const autoplayOnGenerate = useServerStore((state) => state.autoplayOnGenerate);\n  const setAutoplayOnGenerate = useServerStore((state) => state.setAutoplayOnGenerate);\n  const [opening, setOpening] = useState(false);\n  const [generationsPath, setGenerationsPath] = useState<string | null>(null);\n\n  useEffect(() => {\n    fetch(`${serverUrl}/health/filesystem`)\n      .then((res) => res.json())\n      .then((data) => {\n        const genDir = data.directories?.find((d: { path: string }) =>\n          d.path.includes('generations'),\n        );\n        if (genDir?.path) setGenerationsPath(genDir.path);\n      })\n      .catch(() => {});\n  }, [serverUrl]);\n\n  const openGenerationsFolder = useCallback(async () => {\n    if (!generationsPath) return;\n    setOpening(true);\n    try {\n      await platform.filesystem.openPath(generationsPath);\n    } catch (e) {\n      console.error('Failed to open generations folder:', e);\n    } finally {\n      setOpening(false);\n    }\n  }, [platform, generationsPath]);\n\n  return (\n    <div className=\"space-y-8 max-w-2xl\">\n      <SettingSection\n        title=\"Generation\"\n        description=\"Controls for long text generation. These settings apply to all engines.\"\n      >\n        <SettingRow\n          title=\"Auto-chunking limit\"\n          description=\"Long text is split into chunks at sentence boundaries. Lower values can improve quality for long outputs.\"\n          action={\n            <span className=\"text-sm tabular-nums text-muted-foreground\">\n              {maxChunkChars} chars\n            </span>\n          }\n        >\n          <Slider\n            id=\"maxChunkChars\"\n            value={[maxChunkChars]}\n            onValueChange={([value]) => setMaxChunkChars(value)}\n            min={100}\n            max={5000}\n            step={50}\n            aria-label=\"Auto-chunking character limit\"\n          />\n        </SettingRow>\n\n        <SettingRow\n          title=\"Chunk crossfade\"\n          description=\"Blends audio between chunks to smooth transitions. Set to 0 for a hard cut.\"\n          action={\n            <span className=\"text-sm tabular-nums text-muted-foreground\">\n              {crossfadeMs === 0 ? 'Cut' : `${crossfadeMs}ms`}\n            </span>\n          }\n        >\n          <Slider\n            id=\"crossfadeMs\"\n            value={[crossfadeMs]}\n            onValueChange={([value]) => setCrossfadeMs(value)}\n            min={0}\n            max={200}\n            step={10}\n            aria-label=\"Chunk crossfade duration\"\n          />\n        </SettingRow>\n\n        <SettingRow\n          title=\"Normalize audio\"\n          description=\"Adjusts output volume to a consistent level across generations.\"\n          htmlFor=\"normalizeAudio\"\n          action={\n            <Toggle\n              id=\"normalizeAudio\"\n              checked={normalizeAudio}\n              onCheckedChange={setNormalizeAudio}\n            />\n          }\n        />\n\n        <SettingRow\n          title=\"Autoplay on generate\"\n          description=\"Automatically play audio when a generation completes.\"\n          htmlFor=\"autoplayOnGenerate\"\n          action={\n            <Toggle\n              id=\"autoplayOnGenerate\"\n              checked={autoplayOnGenerate}\n              onCheckedChange={setAutoplayOnGenerate}\n            />\n          }\n        />\n\n        <SettingRow\n          title=\"Generations folder\"\n          description={generationsPath ?? 'Where generated audio files are stored on disk.'}\n          action={\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={openGenerationsFolder}\n              disabled={opening || !generationsPath}\n            >\n              <FolderOpen className=\"h-3.5 w-3.5 mr-1.5\" />\n              Open\n            </Button>\n          }\n        />\n      </SettingSection>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerTab/GpuPage.tsx",
    "content": "import { useQuery, useQueryClient } from '@tanstack/react-query';\nimport { AlertCircle, Cpu, Download, Loader2, RotateCw, Trash2 } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Progress } from '@/components/ui/progress';\nimport { apiClient } from '@/lib/api/client';\nimport type { CudaDownloadProgress, HealthResponse } from '@/lib/api/types';\nimport { useServerHealth } from '@/lib/hooks/useServer';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { useServerStore } from '@/stores/serverStore';\nimport { SettingRow, SettingSection } from './SettingRow';\n\ntype RestartPhase = 'idle' | 'stopping' | 'waiting' | 'ready';\n\nfunction AppleLogo({ className }: { className?: string }) {\n  return (\n    <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n      <path d=\"M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z\" />\n    </svg>\n  );\n}\n\nfunction GpuIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <rect x=\"4\" y=\"6\" width=\"16\" height=\"12\" rx=\"2\" />\n      <path d=\"M2 10h2M2 14h2M20 10h2M20 14h2\" />\n      <path d=\"M9 10h6M9 14h4\" />\n    </svg>\n  );\n}\n\nfunction GpuInfoCard({ health }: { health: HealthResponse }) {\n  const hasGpu = health.gpu_available && health.gpu_type;\n\n  // Parse GPU name from type string like \"CUDA (NVIDIA RTX 4090)\" or \"MPS (Apple M2 Pro)\"\n  const gpuName = hasGpu\n    ? health.gpu_type!.replace(/^(CUDA|ROCm|MPS|Metal|XPU|DirectML)\\s*\\((.+)\\)$/, '$2') ||\n      health.gpu_type!\n    : null;\n  const gpuBackend = hasGpu ? health.gpu_type!.replace(/\\s*\\(.+\\)$/, '') : null;\n  const isApple = gpuBackend === 'MPS' || gpuBackend === 'Metal';\n  const showBackendVariant = health.backend_variant && health.backend_variant !== 'cpu';\n\n  return (\n    <div className=\"rounded-lg border border-border/60 p-4\">\n      <div className=\"flex items-center gap-3\">\n        {hasGpu ? (\n          isApple ? (\n            <AppleLogo className=\"h-5 w-5 shrink-0 text-muted-foreground\" />\n          ) : (\n            <GpuIcon className=\"h-5 w-5 shrink-0 text-accent\" />\n          )\n        ) : (\n          <Cpu className=\"h-5 w-5 shrink-0 text-muted-foreground\" />\n        )}\n        <div className=\"flex-1 min-w-0 space-y-0.5\">\n          <div className=\"text-sm font-medium\">{hasGpu ? gpuName : 'CPU Only'}</div>\n          <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground\">\n            {hasGpu ? (\n              <>\n                <span>{gpuBackend}</span>\n                {showBackendVariant && (\n                  <>\n                    <span className=\"text-border\">|</span>\n                    <span className=\"uppercase\">{health.backend_variant}</span>\n                  </>\n                )}\n                {health.vram_used_mb != null && health.vram_used_mb > 0 && (\n                  <>\n                    <span className=\"text-border\">|</span>\n                    <span>{health.vram_used_mb.toFixed(0)} MB VRAM</span>\n                  </>\n                )}\n              </>\n            ) : (\n              <span>No GPU acceleration detected</span>\n            )}\n          </div>\n        </div>\n        {hasGpu && (\n          <div className=\"flex items-center gap-2 rounded-full border border-accent/30 px-2.5 py-0.5\">\n            <span className=\"relative flex h-1.5 w-1.5\">\n              <span className=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-accent/60\" />\n              <span className=\"relative inline-flex h-1.5 w-1.5 rounded-full bg-accent shadow-[0_0_4px_1px_hsl(var(--accent)/0.4)]\" />\n            </span>\n            <span className=\"text-[10px] font-medium text-muted-foreground\">Active</span>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport function GpuPage() {\n  const platform = usePlatform();\n  const queryClient = useQueryClient();\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const { data: health } = useServerHealth();\n\n  const [restartPhase, setRestartPhase] = useState<RestartPhase>('idle');\n  const [error, setError] = useState<string | null>(null);\n  const [downloadProgress, setDownloadProgress] = useState<CudaDownloadProgress | null>(null);\n  const healthPollRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n  const {\n    data: cudaStatus,\n    isLoading: _cudaStatusLoading,\n    refetch: refetchCudaStatus,\n  } = useQuery({\n    queryKey: ['cuda-status', serverUrl],\n    queryFn: () => apiClient.getCudaStatus(),\n    refetchInterval: (query) => (query.state.status === 'pending' ? false : 10000),\n    retry: 1,\n    enabled: !!health,\n  });\n\n  const isCurrentlyCuda = health?.backend_variant === 'cuda';\n  const cudaAvailable = cudaStatus?.available ?? false;\n  const cudaDownloading = cudaStatus?.downloading ?? false;\n\n  useEffect(() => {\n    return () => {\n      if (healthPollRef.current) {\n        clearInterval(healthPollRef.current);\n        healthPollRef.current = null;\n      }\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!cudaDownloading || !serverUrl) return;\n\n    const eventSource = new EventSource(`${serverUrl}/backend/cuda-progress`);\n\n    eventSource.onmessage = (event) => {\n      try {\n        const data = JSON.parse(event.data) as CudaDownloadProgress;\n        setDownloadProgress(data);\n\n        if (data.status === 'complete') {\n          eventSource.close();\n          setDownloadProgress(null);\n          refetchCudaStatus();\n        } else if (data.status === 'error') {\n          eventSource.close();\n          setError(data.error || 'Download failed');\n          setDownloadProgress(null);\n          refetchCudaStatus();\n        }\n      } catch (e) {\n        console.error('Error parsing CUDA progress event:', e);\n      }\n    };\n\n    eventSource.onerror = () => {\n      eventSource.close();\n    };\n\n    return () => {\n      eventSource.close();\n    };\n  }, [cudaDownloading, serverUrl, refetchCudaStatus]);\n\n  const clearHealthPolling = useCallback(() => {\n    if (healthPollRef.current) {\n      clearInterval(healthPollRef.current);\n      healthPollRef.current = null;\n    }\n  }, []);\n\n  const startHealthPolling = useCallback(() => {\n    clearHealthPolling();\n\n    healthPollRef.current = setInterval(async () => {\n      try {\n        const result = await apiClient.getHealth();\n        if (result.status === 'healthy') {\n          clearHealthPolling();\n          setRestartPhase('ready');\n          queryClient.invalidateQueries();\n          setTimeout(() => setRestartPhase('idle'), 2000);\n        }\n      } catch {\n        // Server still down, keep polling\n      }\n    }, 1000);\n  }, [queryClient, clearHealthPolling]);\n\n  const restartServerWithPolling = useCallback(\n    async (errorMessage: string) => {\n      setRestartPhase('stopping');\n      try {\n        await platform.lifecycle.restartServer();\n        setRestartPhase('waiting');\n        startHealthPolling();\n      } catch (e: unknown) {\n        clearHealthPolling();\n        setRestartPhase('idle');\n        throw new Error(e instanceof Error ? e.message : errorMessage);\n      }\n    },\n    [platform, startHealthPolling, clearHealthPolling],\n  );\n\n  const handleDownload = async () => {\n    setError(null);\n    try {\n      await apiClient.downloadCudaBackend();\n      refetchCudaStatus();\n    } catch (e: unknown) {\n      const msg = e instanceof Error ? e.message : 'Failed to start download';\n      if (msg.includes('already downloaded')) {\n        refetchCudaStatus();\n      } else {\n        setError(msg);\n      }\n    }\n  };\n\n  const handleRestart = async () => {\n    setError(null);\n    try {\n      await restartServerWithPolling('Restart failed');\n    } catch (e: unknown) {\n      setError(e instanceof Error ? e.message : 'Restart failed');\n    }\n  };\n\n  const handleSwitchToCpu = async () => {\n    setError(null);\n    setRestartPhase('stopping');\n    try {\n      await apiClient.deleteCudaBackend();\n      await restartServerWithPolling('Failed to switch to CPU');\n    } catch (e: unknown) {\n      setError(e instanceof Error ? e.message : 'Failed to switch to CPU');\n      refetchCudaStatus();\n    }\n  };\n\n  const handleDelete = async () => {\n    setError(null);\n    try {\n      await apiClient.deleteCudaBackend();\n      refetchCudaStatus();\n    } catch (e: unknown) {\n      setError(e instanceof Error ? e.message : 'Failed to delete CUDA backend');\n    }\n  };\n\n  const formatBytes = (bytes: number): string => {\n    if (bytes === 0) return '0 B';\n    const k = 1024;\n    const sizes = ['B', 'KB', 'MB', 'GB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;\n  };\n\n  if (!health) return null;\n\n  const hasNativeGpu =\n    health.gpu_available &&\n    !isCurrentlyCuda &&\n    health.gpu_type &&\n    !health.gpu_type.includes('CUDA');\n\n  return (\n    <div className=\"space-y-8 max-w-2xl\">\n      <GpuInfoCard health={health} />\n\n      {/* CUDA section — only when no native GPU and not already on CUDA */}\n      {!hasNativeGpu && !isCurrentlyCuda && (\n        <SettingSection\n          title=\"CUDA Backend\"\n          description=\"NVIDIA GPU acceleration via a downloadable CUDA backend.\"\n        >\n          {/* Download progress */}\n          {cudaDownloading && downloadProgress && (\n            <SettingRow title=\"Downloading CUDA backend...\">\n              <div className=\"space-y-1.5\">\n                <Progress value={downloadProgress.progress} className=\"h-2\" />\n                <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n                  <span>\n                    {downloadProgress.filename ||\n                      (cudaAvailable ? 'Updating...' : 'Downloading...')}\n                  </span>\n                  <span>\n                    {downloadProgress.total > 0\n                      ? `${formatBytes(downloadProgress.current)} / ${formatBytes(downloadProgress.total)}`\n                      : `${downloadProgress.progress.toFixed(1)}%`}\n                  </span>\n                </div>\n              </div>\n            </SettingRow>\n          )}\n\n          {/* Restart in progress */}\n          {restartPhase !== 'idle' && (\n            <SettingRow\n              title={\n                restartPhase === 'ready'\n                  ? 'Server restarted successfully'\n                  : restartPhase === 'waiting'\n                    ? 'Restarting server...'\n                    : 'Stopping server...'\n              }\n              action={<Loader2 className=\"h-4 w-4 animate-spin text-muted-foreground\" />}\n            />\n          )}\n\n          {/* Error */}\n          {error && (\n            <SettingRow title=\"Error\">\n              <div className=\"flex items-center gap-2 text-sm text-destructive\">\n                <AlertCircle className=\"h-4 w-4 shrink-0\" />\n                <span>{error}</span>\n              </div>\n            </SettingRow>\n          )}\n\n          {/* Actions */}\n          {restartPhase === 'idle' && !cudaDownloading && (\n            <>\n              {!cudaAvailable && !isCurrentlyCuda && (\n                <SettingRow\n                  title=\"Download CUDA backend\"\n                  description=\"~2.4 GB download. Requires an NVIDIA GPU with CUDA support.\"\n                  action={\n                    <Button onClick={handleDownload} size=\"sm\">\n                      <Download className=\"h-3.5 w-3.5 mr-1.5\" />\n                      Download\n                    </Button>\n                  }\n                />\n              )}\n\n              {cudaAvailable && !isCurrentlyCuda && platform.metadata.isTauri && (\n                <SettingRow\n                  title=\"Switch to CUDA backend\"\n                  description=\"CUDA backend is downloaded and ready. Restart to enable.\"\n                  action={\n                    <Button onClick={handleRestart} size=\"sm\">\n                      <RotateCw className=\"h-3.5 w-3.5 mr-1.5\" />\n                      Restart\n                    </Button>\n                  }\n                />\n              )}\n\n              {isCurrentlyCuda && platform.metadata.isTauri && (\n                <SettingRow\n                  title=\"Switch to CPU backend\"\n                  description=\"Disable GPU acceleration. You can re-download CUDA later.\"\n                  action={\n                    <Button onClick={handleSwitchToCpu} variant=\"outline\" size=\"sm\">\n                      <RotateCw className=\"h-3.5 w-3.5 mr-1.5\" />\n                      Switch\n                    </Button>\n                  }\n                />\n              )}\n\n              {cudaAvailable && !isCurrentlyCuda && (\n                <SettingRow\n                  title=\"Remove CUDA backend\"\n                  description=\"Delete the downloaded CUDA binary to free disk space.\"\n                  action={\n                    <Button\n                      onClick={handleDelete}\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"text-muted-foreground hover:text-destructive\"\n                    >\n                      <Trash2 className=\"h-3.5 w-3.5 mr-1.5\" />\n                      Remove\n                    </Button>\n                  }\n                />\n              )}\n            </>\n          )}\n        </SettingSection>\n      )}\n\n      <p className=\"text-xs text-muted-foreground/60 leading-relaxed\">\n        Voicebox automatically detects and uses the best available GPU on your system. On Apple\n        Silicon Macs, the MLX backend runs natively on the Neural Engine and GPU via Metal\n        Performance Shaders (MPS), with no additional setup required. On Windows and Linux with\n        NVIDIA GPUs, you can download an optional CUDA backend for hardware-accelerated inference.\n        AMD ROCm, Intel XPU, and DirectML are also supported where available through PyTorch. When\n        no GPU is detected, Voicebox falls back to CPU — all engines still work, just slower.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerTab/LogsPage.tsx",
    "content": "import { useEffect, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { cn } from '@/lib/utils/cn';\nimport { type LogEntry, useLogStore } from '@/stores/logStore';\n\nfunction formatTime(timestamp: number): string {\n  const d = new Date(timestamp);\n  return d.toLocaleTimeString(undefined, {\n    hour: '2-digit',\n    minute: '2-digit',\n    second: '2-digit',\n    hour12: false,\n  });\n}\n\nfunction LogLine({ entry }: { entry: LogEntry }) {\n  return (\n    <div className=\"flex gap-3 font-mono text-xs leading-5 hover:bg-muted/30\">\n      <span className=\"text-muted-foreground/50 select-none shrink-0\">\n        {formatTime(entry.timestamp)}\n      </span>\n      <span\n        className={cn(\n          'whitespace-pre-wrap break-all',\n          entry.stream === 'stderr' ? 'text-orange-400/80' : 'text-muted-foreground',\n        )}\n      >\n        {entry.line}\n      </span>\n    </div>\n  );\n}\n\nexport function LogsPage() {\n  const entries = useLogStore((s) => s.entries);\n  const clear = useLogStore((s) => s.clear);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [autoScroll, setAutoScroll] = useState(true);\n\n  // Auto-scroll to bottom when new entries arrive\n  useEffect(() => {\n    if (autoScroll && containerRef.current) {\n      containerRef.current.scrollTop = containerRef.current.scrollHeight;\n    }\n  }, [entries.length, autoScroll]);\n\n  // Detect manual scroll to disable auto-scroll\n  const handleScroll = () => {\n    const el = containerRef.current;\n    if (!el) return;\n    const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40;\n    setAutoScroll(atBottom);\n  };\n\n  return (\n    <div className=\"flex flex-col h-full min-h-0\">\n      <div className=\"flex items-center justify-between mb-3\">\n        <div>\n          <h3 className=\"text-sm font-medium\">Server Logs</h3>\n          <p className=\"text-sm text-muted-foreground\">\n            {entries.length} {entries.length === 1 ? 'line' : 'lines'}\n          </p>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          {!autoScroll && (\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={() => {\n                setAutoScroll(true);\n                containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight });\n              }}\n            >\n              Scroll to bottom\n            </Button>\n          )}\n          <Button variant=\"outline\" size=\"sm\" onClick={clear}>\n            Clear\n          </Button>\n        </div>\n      </div>\n\n      <div\n        ref={containerRef}\n        onScroll={handleScroll}\n        className=\"flex-1 min-h-0 overflow-y-auto rounded-md border bg-black/20 p-3\"\n      >\n        {entries.length === 0 ? (\n          <div className=\"text-sm text-muted-foreground/50 font-mono space-y-1\">\n            <p>No log output yet.</p>\n            {!import.meta.env?.PROD && (\n              <p>\n                Server logs are only captured when the app manages the server process (production\n                builds).\n              </p>\n            )}\n          </div>\n        ) : (\n          entries.map((entry) => <LogLine key={entry.id} entry={entry} />)\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerTab/ServerTab.tsx",
    "content": "import { Link, Outlet, useMatchRoute } from '@tanstack/react-router';\nimport { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';\nimport { cn } from '@/lib/utils/cn';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { usePlayerStore } from '@/stores/playerStore';\n\ninterface SettingsTab {\n  label: string;\n  path:\n    | '/settings'\n    | '/settings/generation'\n    | '/settings/gpu'\n    | '/settings/logs'\n    | '/settings/changelog'\n    | '/settings/about';\n  tauriOnly?: boolean;\n}\n\nconst tabs: SettingsTab[] = [\n  { label: 'General', path: '/settings' },\n  { label: 'Generation', path: '/settings/generation' },\n  { label: 'GPU', path: '/settings/gpu', tauriOnly: true },\n  { label: 'Logs', path: '/settings/logs', tauriOnly: true },\n  { label: 'Changelog', path: '/settings/changelog' },\n  { label: 'About', path: '/settings/about' },\n];\n\nexport function SettingsLayout() {\n  const platform = usePlatform();\n  const isPlayerVisible = !!usePlayerStore((state) => state.audioUrl);\n  const matchRoute = useMatchRoute();\n\n  return (\n    <div className=\"flex flex-col h-full min-h-0\">\n      <nav className=\"flex gap-1 border-b shrink-0\">\n        {tabs.map((tab) => {\n          if (tab.tauriOnly && !platform.metadata.isTauri) return null;\n\n          const isActive =\n            tab.path === '/settings'\n              ? matchRoute({ to: tab.path, fuzzy: false })\n              : matchRoute({ to: tab.path });\n\n          return (\n            <Link\n              key={tab.path}\n              to={tab.path}\n              className={cn(\n                'px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px',\n                isActive\n                  ? 'border-accent text-foreground'\n                  : 'border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground/30',\n              )}\n            >\n              {tab.label}\n            </Link>\n          );\n        })}\n      </nav>\n\n      <div\n        className={cn(\n          'flex-1 overflow-y-auto pt-6 pb-6 px-2 -mx-2',\n          isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,\n        )}\n      >\n        <Outlet />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ServerTab/SettingRow.tsx",
    "content": "import type { ReactNode } from 'react';\n\n/**\n * A section header with title and optional description, separated by a border.\n */\nexport function SettingSection({\n  title,\n  description,\n  children,\n}: {\n  title?: string;\n  description?: string;\n  children: ReactNode;\n}) {\n  return (\n    <div className=\"space-y-1\">\n      {title && <h3 className=\"text-sm font-medium\">{title}</h3>}\n      {description && <p className=\"text-sm text-muted-foreground\">{description}</p>}\n      <div className={`${title || description ? 'pt-3' : ''} space-y-0 divide-y divide-border/60`}>\n        {children}\n      </div>\n    </div>\n  );\n}\n\n/**\n * A single settings row: label+description on the left, action on the right.\n * Use for toggles, inputs, buttons, badges — any control type.\n */\nexport function SettingRow({\n  title,\n  description,\n  htmlFor,\n  action,\n  children,\n}: {\n  title: string;\n  description?: string;\n  htmlFor?: string;\n  /** Right-aligned control (checkbox, button, badge, etc.) */\n  action?: ReactNode;\n  /** Full-width content rendered below the label row (for sliders, inputs, etc.) */\n  children?: ReactNode;\n}) {\n  return (\n    <div className=\"py-3\">\n      <div className=\"flex items-center justify-between gap-8\">\n        <div className=\"min-w-0\">\n          <label\n            htmlFor={htmlFor}\n            className={`text-sm font-medium leading-none select-none ${htmlFor ? 'cursor-pointer' : ''}`}\n          >\n            {title}\n          </label>\n          {description && <p className=\"text-sm text-muted-foreground mt-0.5\">{description}</p>}\n        </div>\n        {action && <div className=\"shrink-0\">{action}</div>}\n      </div>\n      {children && <div className=\"mt-3\">{children}</div>}\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ShinyText.tsx",
    "content": "import { motion, useAnimationFrame, useMotionValue, useTransform } from 'motion/react';\nimport type React from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface ShinyTextProps {\n  text: string;\n  disabled?: boolean;\n  speed?: number;\n  className?: string;\n  color?: string;\n  shineColor?: string;\n  spread?: number;\n  yoyo?: boolean;\n  pauseOnHover?: boolean;\n  direction?: 'left' | 'right';\n  delay?: number;\n}\n\nconst ShinyText: React.FC<ShinyTextProps> = ({\n  text,\n  disabled = false,\n  speed = 2,\n  className = '',\n  color = '#b5b5b5',\n  shineColor = '#ffffff',\n  spread = 120,\n  yoyo = false,\n  pauseOnHover = false,\n  direction = 'left',\n  delay = 0,\n}) => {\n  const [isPaused, setIsPaused] = useState(false);\n  const progress = useMotionValue(0);\n  const elapsedRef = useRef(0);\n  const lastTimeRef = useRef<number | null>(null);\n  const directionRef = useRef(direction === 'left' ? 1 : -1);\n\n  const animationDuration = speed * 1000;\n  const delayDuration = delay * 1000;\n\n  useAnimationFrame((time) => {\n    if (disabled || isPaused) {\n      lastTimeRef.current = null;\n      return;\n    }\n\n    if (lastTimeRef.current === null) {\n      lastTimeRef.current = time;\n      return;\n    }\n\n    const deltaTime = time - lastTimeRef.current;\n    lastTimeRef.current = time;\n\n    elapsedRef.current += deltaTime;\n\n    // Animation goes from 0 to 100\n    if (yoyo) {\n      const cycleDuration = animationDuration + delayDuration;\n      const fullCycle = cycleDuration * 2;\n      const cycleTime = elapsedRef.current % fullCycle;\n\n      if (cycleTime < animationDuration) {\n        // Forward animation: 0 -> 100\n        const p = (cycleTime / animationDuration) * 100;\n        progress.set(directionRef.current === 1 ? p : 100 - p);\n      } else if (cycleTime < cycleDuration) {\n        // Delay at end\n        progress.set(directionRef.current === 1 ? 100 : 0);\n      } else if (cycleTime < cycleDuration + animationDuration) {\n        // Reverse animation: 100 -> 0\n        const reverseTime = cycleTime - cycleDuration;\n        const p = 100 - (reverseTime / animationDuration) * 100;\n        progress.set(directionRef.current === 1 ? p : 100 - p);\n      } else {\n        // Delay at start\n        progress.set(directionRef.current === 1 ? 0 : 100);\n      }\n    } else {\n      const cycleDuration = animationDuration + delayDuration;\n      const cycleTime = elapsedRef.current % cycleDuration;\n\n      if (cycleTime < animationDuration) {\n        // Animation phase: 0 -> 100\n        const p = (cycleTime / animationDuration) * 100;\n        progress.set(directionRef.current === 1 ? p : 100 - p);\n      } else {\n        // Delay phase - hold at end (shine off-screen)\n        progress.set(directionRef.current === 1 ? 100 : 0);\n      }\n    }\n  });\n\n  useEffect(() => {\n    directionRef.current = direction === 'left' ? 1 : -1;\n    elapsedRef.current = 0;\n    progress.set(0);\n    // eslint-d, progress.setisable-next-line react-hooks/exhaustive-deps\n  }, [direction]);\n\n  // Transform: p=0 -> 150% (shine off right), p=100 -> -50% (shine off left)\n  const backgroundPosition = useTransform(progress, (p) => `${150 - p * 2}% center`);\n\n  const handleMouseEnter = useCallback(() => {\n    if (pauseOnHover) setIsPaused(true);\n  }, [pauseOnHover]);\n\n  const handleMouseLeave = useCallback(() => {\n    if (pauseOnHover) setIsPaused(false);\n  }, [pauseOnHover]);\n\n  const gradientStyle: React.CSSProperties = {\n    backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`,\n    backgroundSize: '200% auto',\n    WebkitBackgroundClip: 'text',\n    backgroundClip: 'text',\n    WebkitTextFillColor: 'transparent',\n  };\n\n  return (\n    <motion.span\n      className={`inline-block ${className}`}\n      style={{ ...gradientStyle, backgroundPosition }}\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n    >\n      {text}\n    </motion.span>\n  );\n};\n\nexport default ShinyText;\n//   plugins: [],\n// };\n"
  },
  {
    "path": "app/src/components/Sidebar.tsx",
    "content": "import { Link, useMatchRoute } from '@tanstack/react-router';\nimport { AudioLines, Box, Mic, Settings, Speaker, Volume2, Wand2 } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport voiceboxLogo from '@/assets/voicebox-logo.png';\nimport { cn } from '@/lib/utils/cn';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport type { UpdateStatus } from '@/platform/types';\nimport { usePlayerStore } from '@/stores/playerStore';\nimport { version } from '../../package.json';\n\ninterface SidebarProps {\n  isMacOS?: boolean;\n}\n\nconst tabs = [\n  { id: 'main', path: '/', icon: Volume2, label: 'Generate' },\n  { id: 'stories', path: '/stories', icon: AudioLines, label: 'Stories' },\n  { id: 'voices', path: '/voices', icon: Mic, label: 'Voices' },\n  { id: 'effects', path: '/effects', icon: Wand2, label: 'Effects' },\n  { id: 'audio', path: '/audio', icon: Speaker, label: 'Audio' },\n  { id: 'models', path: '/models', icon: Box, label: 'Models' },\n  { id: 'settings', path: '/settings', icon: Settings, label: 'Settings' },\n];\n\nexport function Sidebar({ isMacOS }: SidebarProps) {\n  const matchRoute = useMatchRoute();\n  const isPlayerOpen = !!usePlayerStore((s) => s.audioUrl);\n  const platform = usePlatform();\n\n  const [updateStatus, setUpdateStatus] = useState<UpdateStatus>(platform.updater.getStatus());\n  useEffect(() => platform.updater.subscribe(setUpdateStatus), [platform.updater]);\n\n  return (\n    <div\n      className={cn(\n        'fixed left-0 top-0 h-full w-20 bg-sidebar border-r border-border flex flex-col items-center py-6 gap-6',\n        isMacOS && 'pt-14',\n      )}\n    >\n      {/* Logo */}\n      <div className=\"mb-2\">\n        <img\n          src={voiceboxLogo}\n          alt=\"Voicebox\"\n          className=\"w-12 h-12 object-contain\"\n          style={{\n            filter:\n              'drop-shadow(0 0 6px hsl(var(--accent) / 0.5)) drop-shadow(0 0 14px hsl(var(--accent) / 0.35)) drop-shadow(0 0 28px hsl(var(--accent) / 0.2))',\n          }}\n        />\n      </div>\n\n      {/* Navigation Buttons */}\n      <div className=\"flex flex-col gap-3\">\n        {tabs.map((tab, index) => {\n          const Icon = tab.icon;\n          const isActive =\n            tab.path === '/'\n              ? matchRoute({ to: '/', fuzzy: false })\n              : matchRoute({ to: tab.path, fuzzy: true });\n\n          // Accent fades as buttons get further from the logo\n          const accentOpacity = Math.max(0.08, 0.5 - index * 0.07);\n\n          return (\n            <Link\n              key={tab.id}\n              to={tab.path}\n              className={cn(\n                'relative w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200 overflow-hidden',\n                isActive\n                  ? 'bg-white/[0.07] text-foreground shadow-lg backdrop-blur-sm border border-white/[0.08]'\n                  : 'text-muted-foreground hover:bg-muted/50',\n              )}\n              title={tab.label}\n              aria-label={tab.label}\n            >\n              {isActive && (\n                <div\n                  className=\"absolute inset-0 rounded-full pointer-events-none\"\n                  style={{\n                    maskImage: 'linear-gradient(to bottom, black, transparent 60%)',\n                    WebkitMaskImage: 'linear-gradient(to bottom, black, transparent 60%)',\n                    border: `1px solid hsl(var(--accent) / ${accentOpacity})`,\n                  }}\n                />\n              )}\n              <Icon className=\"h-5 w-5 relative z-10\" />\n            </Link>\n          );\n        })}\n      </div>\n\n      {/* Version */}\n      <div\n        className=\"mt-auto flex flex-col items-center gap-1.5 transition-all duration-300\"\n        style={{ paddingBottom: isPlayerOpen ? '7rem' : undefined }}\n      >\n        <span className=\"text-[10px] text-muted-foreground/50\">v{version}</span>\n        {updateStatus.available && (\n          <Link\n            to=\"/settings\"\n            className=\"text-[9px] font-semibold tracking-wide uppercase px-2 py-0.5 rounded-full bg-accent/15 text-accent hover:bg-accent/25 transition-colors\"\n          >\n            Update\n          </Link>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/StoriesTab/StoriesTab.tsx",
    "content": "import { FloatingGenerateBox } from '@/components/Generation/FloatingGenerateBox';\nimport { usePlayerStore } from '@/stores/playerStore';\nimport { StoryContent } from './StoryContent';\nimport { StoryList } from './StoryList';\n\nexport function StoriesTab() {\n  const audioUrl = usePlayerStore((state) => state.audioUrl);\n\n  return (\n    <div className=\"flex flex-col h-full min-h-0 overflow-hidden\">\n      {/* Main content area */}\n      <div className=\"flex-1 min-h-0 flex gap-6 overflow-hidden relative\">\n        {/* Left Column - Story List */}\n        <div className=\"flex flex-col min-h-0 overflow-hidden w-full max-w-[360px] shrink-0\">\n          <StoryList />\n        </div>\n\n        {/* Right Column - Story Content */}\n        <div className=\"flex flex-col min-h-0 overflow-hidden flex-1\">\n          <StoryContent />\n        </div>\n\n        {/* Floating Generate Box - position is managed via storyStore.trackEditorHeight */}\n        <FloatingGenerateBox showVoiceSelector isPlayerOpen={!!audioUrl} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/StoriesTab/StoryChatItem.tsx",
    "content": "import { useSortable } from '@dnd-kit/sortable';\nimport { CSS } from '@dnd-kit/utilities';\nimport { GripVertical, Mic, MoreHorizontal, Play, Trash2 } from 'lucide-react';\nimport { useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { Textarea } from '@/components/ui/textarea';\nimport type { StoryItemDetail } from '@/lib/api/types';\nimport { cn } from '@/lib/utils/cn';\nimport { useStoryStore } from '@/stores/storyStore';\nimport { useServerStore } from '@/stores/serverStore';\n\ninterface StoryChatItemProps {\n  item: StoryItemDetail;\n  storyId: string;\n  index: number;\n  onRemove: () => void;\n  currentTimeMs: number;\n  isPlaying: boolean;\n  dragHandleProps?: React.HTMLAttributes<HTMLButtonElement>;\n  isDragging?: boolean;\n}\n\nexport function StoryChatItem({\n  item,\n  onRemove,\n  currentTimeMs,\n  isPlaying,\n  dragHandleProps,\n  isDragging,\n}: StoryChatItemProps) {\n  const seek = useStoryStore((state) => state.seek);\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const [avatarError, setAvatarError] = useState(false);\n\n  const avatarUrl = `${serverUrl}/profiles/${item.profile_id}/avatar`;\n\n  // Check if this item is currently playing based on timecode\n  const itemStartMs = item.start_time_ms;\n  const itemEndMs = item.start_time_ms + item.duration * 1000;\n  const isCurrentlyPlaying = isPlaying && currentTimeMs >= itemStartMs && currentTimeMs < itemEndMs;\n\n  const handlePlay = () => {\n    // Seek to the start of this item\n    seek(itemStartMs);\n  };\n\n  const formatTime = (ms: number): string => {\n    const totalSeconds = Math.floor(ms / 1000);\n    const minutes = Math.floor(totalSeconds / 60);\n    const seconds = totalSeconds % 60;\n    const milliseconds = Math.floor((ms % 1000) / 100);\n    return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`;\n  };\n\n  return (\n    <div\n      className={cn(\n        'flex items-start gap-3 p-4 rounded-lg border transition-colors',\n        isCurrentlyPlaying && 'bg-muted/70 border-primary',\n        !isCurrentlyPlaying && 'hover:bg-muted/50',\n        isDragging && 'opacity-50 shadow-lg',\n      )}\n    >\n      {/* Drag Handle */}\n      {dragHandleProps && (\n        <button\n          type=\"button\"\n          className=\"shrink-0 cursor-grab active:cursor-grabbing touch-none text-muted-foreground hover:text-foreground transition-colors\"\n          {...dragHandleProps}\n        >\n          <GripVertical className=\"h-5 w-5\" />\n        </button>\n      )}\n\n      {/* Voice Avatar */}\n      <div className=\"shrink-0\">\n        <div className=\"h-10 w-10 rounded-full bg-muted flex items-center justify-center overflow-hidden\">\n          {!avatarError ? (\n            <img\n              src={avatarUrl}\n              alt={`${item.profile_name} avatar`}\n              className={cn(\n                'h-full w-full object-cover transition-all duration-200',\n                !isCurrentlyPlaying && 'grayscale'\n              )}\n              onError={() => setAvatarError(true)}\n            />\n          ) : (\n            <Mic className=\"h-5 w-5 text-muted-foreground\" />\n          )}\n        </div>\n      </div>\n\n      {/* Content */}\n      <div className=\"flex-1 min-w-0\">\n        <div className=\"flex items-center gap-2 mb-2\">\n          <span className=\"font-medium text-sm\">{item.profile_name}</span>\n          <span className=\"text-xs text-muted-foreground\">{item.language}</span>\n          <span className=\"text-xs text-muted-foreground tabular-nums ml-auto\">\n            {formatTime(itemStartMs)}\n          </span>\n        </div>\n        <Textarea\n          value={item.text}\n          className=\"flex-1 resize-none text-sm text-muted-foreground select-text bg-card cursor-text\"\n          readOnly\n          onDoubleClick={handlePlay}\n        />\n      </div>\n\n      {/* Actions */}\n      <div className=\"shrink-0\">\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant=\"ghost\" size=\"icon\" className=\"h-8 w-8\" aria-label=\"Actions\">\n              <MoreHorizontal className=\"h-4 w-4\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem onClick={handlePlay}>\n              <Play className=\"mr-2 h-4 w-4\" />\n              Play from here\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={onRemove} className=\"text-destructive focus:text-destructive\">\n              <Trash2 className=\"mr-2 h-4 w-4\" />\n              Remove from Story\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </div>\n  );\n}\n\n// Sortable wrapper component\nexport function SortableStoryChatItem(props: Omit<StoryChatItemProps, 'dragHandleProps' | 'isDragging'>) {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({ id: props.item.generation_id });\n\n  const style = {\n    transform: CSS.Transform.toString(transform),\n    transition,\n  };\n\n  return (\n    <div ref={setNodeRef} style={style} {...attributes}>\n      <StoryChatItem\n        {...props}\n        dragHandleProps={listeners}\n        isDragging={isDragging}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/StoriesTab/StoryContent.tsx",
    "content": "import {\n  closestCenter,\n  DndContext,\n  type DragEndEvent,\n  KeyboardSensor,\n  PointerSensor,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core';\nimport {\n  arrayMove,\n  SortableContext,\n  sortableKeyboardCoordinates,\n  verticalListSortingStrategy,\n} from '@dnd-kit/sortable';\nimport { Link } from '@tanstack/react-router';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { Download, Plus } from 'lucide-react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport Loader from 'react-loaders';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport { useToast } from '@/components/ui/use-toast';\nimport { useHistory } from '@/lib/hooks/useHistory';\nimport {\n  useAddStoryItem,\n  useExportStoryAudio,\n  useRemoveStoryItem,\n  useReorderStoryItems,\n  useStory,\n} from '@/lib/hooks/useStories';\nimport { useStoryPlayback } from '@/lib/hooks/useStoryPlayback';\nimport { useGenerationStore } from '@/stores/generationStore';\nimport { useStoryStore } from '@/stores/storyStore';\nimport { SortableStoryChatItem } from './StoryChatItem';\n\nexport function StoryContent() {\n  const selectedStoryId = useStoryStore((state) => state.selectedStoryId);\n  const { data: story, isLoading } = useStory(selectedStoryId);\n  const removeItem = useRemoveStoryItem();\n  const reorderItems = useReorderStoryItems();\n  const exportAudio = useExportStoryAudio();\n  const addStoryItem = useAddStoryItem();\n  const { toast } = useToast();\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const pendingCount = useGenerationStore((s) => s.pendingGenerationIds.size);\n\n  // Add generation popover state\n  const [searchQuery, setSearchQuery] = useState('');\n  const [isAddOpen, setIsAddOpen] = useState(false);\n  const { data: historyData } = useHistory();\n\n  // Filter generations not in story and matching search\n  const availableGenerations = useMemo(() => {\n    if (!historyData?.items || !story) return [];\n    const storyGenerationIds = new Set(story.items.map((i) => i.generation_id));\n    const query = searchQuery.toLowerCase();\n    return historyData.items.filter(\n      (gen) =>\n        gen.status === 'completed' &&\n        !storyGenerationIds.has(gen.id) &&\n        (gen.text.toLowerCase().includes(query) || gen.profile_name.toLowerCase().includes(query)),\n    );\n  }, [historyData, story, searchQuery]);\n\n  // Get track editor height from store for dynamic padding\n  const trackEditorHeight = useStoryStore((state) => state.trackEditorHeight);\n\n  // Track editor is shown when story has items\n  const hasBottomBar = story && story.items.length > 0;\n\n  // Calculate dynamic bottom padding: track editor + gap\n  const bottomPadding = hasBottomBar ? trackEditorHeight + 24 : 0;\n\n  // Drag and drop sensors\n  const sensors = useSensors(\n    useSensor(PointerSensor, {\n      activationConstraint: {\n        distance: 8,\n      },\n    }),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    }),\n  );\n\n  // Playback state (for auto-scroll and item highlighting)\n  const isPlaying = useStoryStore((state) => state.isPlaying);\n  const currentTimeMs = useStoryStore((state) => state.currentTimeMs);\n  const playbackStoryId = useStoryStore((state) => state.playbackStoryId);\n\n  // Refs for auto-scrolling to playing item\n  const itemRefsMap = useRef<Map<string, HTMLDivElement>>(new Map());\n  const lastScrolledItemRef = useRef<string | null>(null);\n\n  // Use playback hook\n  useStoryPlayback(story?.items);\n\n  // Sort items by start_time_ms\n  const sortedItems = useMemo(() => {\n    if (!story?.items) return [];\n    return [...story.items].sort((a, b) => a.start_time_ms - b.start_time_ms);\n  }, [story?.items]);\n\n  // Find the currently playing item based on timecode\n  const currentlyPlayingItemId = useMemo(() => {\n    if (!isPlaying || playbackStoryId !== story?.id || !sortedItems.length) {\n      return null;\n    }\n    const playingItem = sortedItems.find((item) => {\n      const itemStart = item.start_time_ms;\n      const itemEnd = item.start_time_ms + item.duration * 1000;\n      return currentTimeMs >= itemStart && currentTimeMs < itemEnd;\n    });\n    return playingItem?.generation_id ?? null;\n  }, [isPlaying, playbackStoryId, story?.id, sortedItems, currentTimeMs]);\n\n  // Auto-scroll to the currently playing item\n  useEffect(() => {\n    if (!currentlyPlayingItemId || currentlyPlayingItemId === lastScrolledItemRef.current) {\n      return;\n    }\n\n    const element = itemRefsMap.current.get(currentlyPlayingItemId);\n    if (element && scrollRef.current) {\n      element.scrollIntoView({ behavior: 'smooth', block: 'start' });\n      lastScrolledItemRef.current = currentlyPlayingItemId;\n    }\n  }, [currentlyPlayingItemId]);\n\n  // Reset last scrolled item when playback stops\n  useEffect(() => {\n    if (!isPlaying) {\n      lastScrolledItemRef.current = null;\n    }\n  }, [isPlaying]);\n\n  const handleRemoveItem = (itemId: string) => {\n    if (!story) return;\n\n    removeItem.mutate(\n      {\n        storyId: story.id,\n        itemId,\n      },\n      {\n        onError: (error) => {\n          toast({\n            title: 'Failed to remove item',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  };\n\n  const handleDragEnd = (event: DragEndEvent) => {\n    const { active, over } = event;\n\n    if (!story || !over || active.id === over.id) return;\n\n    const oldIndex = sortedItems.findIndex((item) => item.generation_id === active.id);\n    const newIndex = sortedItems.findIndex((item) => item.generation_id === over.id);\n\n    if (oldIndex === -1 || newIndex === -1) return;\n\n    // Calculate the new order\n    const newOrder = arrayMove(sortedItems, oldIndex, newIndex);\n    const generationIds = newOrder.map((item) => item.generation_id);\n\n    // Send reorder request to backend\n    reorderItems.mutate(\n      {\n        storyId: story.id,\n        data: { generation_ids: generationIds },\n      },\n      {\n        onError: (error) => {\n          toast({\n            title: 'Failed to reorder items',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  };\n\n  const handleExportAudio = () => {\n    if (!story) return;\n\n    exportAudio.mutate(\n      {\n        storyId: story.id,\n        storyName: story.name,\n      },\n      {\n        onError: (error) => {\n          toast({\n            title: 'Failed to export audio',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  };\n\n  const handleAddGeneration = (generationId: string) => {\n    if (!story) return;\n\n    addStoryItem.mutate(\n      {\n        storyId: story.id,\n        data: { generation_id: generationId },\n      },\n      {\n        onSuccess: () => {\n          setIsAddOpen(false);\n          setSearchQuery('');\n        },\n        onError: (error) => {\n          toast({\n            title: 'Failed to add generation',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  };\n\n  if (!selectedStoryId) {\n    return (\n      <div className=\"flex items-center justify-center h-full text-muted-foreground\">\n        <div className=\"text-center\">\n          <p className=\"text-lg font-medium mb-2\">Select a story</p>\n          <p className=\"text-sm\">Choose a story from the list to view its content</p>\n        </div>\n      </div>\n    );\n  }\n\n  if (isLoading) {\n    return (\n      <div className=\"flex items-center justify-center h-full\">\n        <div className=\"text-muted-foreground\">Loading story...</div>\n      </div>\n    );\n  }\n\n  if (!story) {\n    return (\n      <div className=\"flex items-center justify-center h-full text-muted-foreground\">\n        <div className=\"text-center\">\n          <p className=\"text-lg font-medium mb-2\">Story not found</p>\n          <p className=\"text-sm\">The selected story could not be loaded</p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col h-full min-h-0\">\n      {/* Header */}\n      <div className=\"flex items-center justify-between mb-4 px-1\">\n        <div>\n          <h2 className=\"text-2xl font-bold\">{story.name}</h2>\n          {story.description && (\n            <p className=\"text-sm text-muted-foreground mt-1\">{story.description}</p>\n          )}\n        </div>\n        <div className=\"flex gap-2 items-center\">\n          <AnimatePresence>\n            {pendingCount > 0 && (\n              <motion.div\n                initial={{ opacity: 0, scale: 0.9, width: 0 }}\n                animate={{ opacity: 1, scale: 1, width: 'auto' }}\n                exit={{ opacity: 0, scale: 0.9, width: 0 }}\n                transition={{ duration: 0.2 }}\n              >\n                <Link\n                  to=\"/\"\n                  className=\"flex items-center gap-2 h-8 pl-1.5 pr-3 rounded-full bg-card border border-border hover:bg-muted/50 transition-all duration-200 cursor-pointer\"\n                >\n                  <div className=\"shrink-0 w-10 h-5 overflow-hidden flex items-center justify-center\">\n                    <div className=\"scale-[0.45]\">\n                      <Loader type=\"line-scale\" active />\n                    </div>\n                  </div>\n                  <span className=\"text-xs text-muted-foreground whitespace-nowrap\">\n                    Generating {pendingCount} {pendingCount === 1 ? 'audio' : 'audios'}\n                  </span>\n                </Link>\n              </motion.div>\n            )}\n          </AnimatePresence>\n          <Popover open={isAddOpen} onOpenChange={setIsAddOpen}>\n            <PopoverTrigger asChild>\n              <Button variant=\"outline\" size=\"sm\">\n                <Plus className=\"mr-2 h-4 w-4\" />\n                Add\n              </Button>\n            </PopoverTrigger>\n            <PopoverContent className=\"w-80 p-0\" align=\"end\">\n              <div className=\"p-2 border-b\">\n                <Input\n                  placeholder=\"Search by name or transcript...\"\n                  value={searchQuery}\n                  onChange={(e) => setSearchQuery(e.target.value)}\n                  autoFocus\n                />\n              </div>\n              <div className=\"max-h-60 overflow-y-auto\">\n                {availableGenerations.length === 0 ? (\n                  <div className=\"p-4 text-center text-sm text-muted-foreground\">\n                    {searchQuery ? 'No matching generations found' : 'No available generations'}\n                  </div>\n                ) : (\n                  availableGenerations.map((gen) => (\n                    <button\n                      key={gen.id}\n                      type=\"button\"\n                      className=\"w-full text-left px-3 py-2 hover:bg-muted transition-colors border-b last:border-b-0\"\n                      onClick={() => handleAddGeneration(gen.id)}\n                    >\n                      <div className=\"font-medium text-sm\">{gen.profile_name}</div>\n                      <div className=\"text-xs text-muted-foreground truncate\">\n                        {gen.text.length > 50 ? `${gen.text.substring(0, 50)}...` : gen.text}\n                      </div>\n                    </button>\n                  ))\n                )}\n              </div>\n            </PopoverContent>\n          </Popover>\n          {story.items.length > 0 && (\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={handleExportAudio}\n              disabled={exportAudio.isPending}\n            >\n              <Download className=\"mr-2 h-4 w-4\" />\n              Export Audio\n            </Button>\n          )}\n        </div>\n      </div>\n\n      {/* Content */}\n      <div\n        ref={scrollRef}\n        className=\"flex-1 min-h-0 overflow-y-auto space-y-3\"\n        style={{ paddingBottom: bottomPadding > 0 ? `${bottomPadding}px` : undefined }}\n      >\n        {sortedItems.length === 0 ? (\n          <div className=\"text-center py-12 px-5 border-2 border-dashed border-muted rounded-md text-muted-foreground\">\n            <p className=\"text-sm\">No items in this story</p>\n            <p className=\"text-xs mt-2\">Generate speech using the box below to add items</p>\n          </div>\n        ) : (\n          <DndContext\n            sensors={sensors}\n            collisionDetection={closestCenter}\n            onDragEnd={handleDragEnd}\n          >\n            <SortableContext\n              items={sortedItems.map((item) => item.generation_id)}\n              strategy={verticalListSortingStrategy}\n            >\n              <div className=\"space-y-3\">\n                {sortedItems.map((item, index) => (\n                  <div\n                    key={item.id}\n                    ref={(el) => {\n                      if (el) {\n                        itemRefsMap.current.set(item.generation_id, el);\n                      } else {\n                        itemRefsMap.current.delete(item.generation_id);\n                      }\n                    }}\n                  >\n                    <SortableStoryChatItem\n                      item={item}\n                      storyId={story.id}\n                      index={index}\n                      onRemove={() => handleRemoveItem(item.id)}\n                      currentTimeMs={currentTimeMs}\n                      isPlaying={isPlaying && playbackStoryId === story.id}\n                    />\n                  </div>\n                ))}\n              </div>\n            </SortableContext>\n          </DndContext>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/StoriesTab/StoryList.tsx",
    "content": "import { BookOpen, MoreHorizontal, Pencil, Plus, Trash2 } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from '@/components/ui/alert-dialog';\nimport { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Textarea } from '@/components/ui/textarea';\nimport { useToast } from '@/components/ui/use-toast';\nimport {\n  useCreateStory,\n  useDeleteStory,\n  useStories,\n  useStory,\n  useUpdateStory,\n} from '@/lib/hooks/useStories';\nimport { cn } from '@/lib/utils/cn';\nimport { formatDate } from '@/lib/utils/format';\nimport { useStoryStore } from '@/stores/storyStore';\n\nexport function StoryList() {\n  const { data: stories, isLoading } = useStories();\n  const selectedStoryId = useStoryStore((state) => state.selectedStoryId);\n  const setSelectedStoryId = useStoryStore((state) => state.setSelectedStoryId);\n  const trackEditorHeight = useStoryStore((state) => state.trackEditorHeight);\n  const { data: selectedStory } = useStory(selectedStoryId);\n  const createStory = useCreateStory();\n  const updateStory = useUpdateStory();\n  const deleteStory = useDeleteStory();\n  const [createDialogOpen, setCreateDialogOpen] = useState(false);\n  const [editDialogOpen, setEditDialogOpen] = useState(false);\n  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);\n  const [editingStory, setEditingStory] = useState<{\n    id: string;\n    name: string;\n    description?: string;\n  } | null>(null);\n  const [deletingStoryId, setDeletingStoryId] = useState<string | null>(null);\n  const [newStoryName, setNewStoryName] = useState('');\n  const [newStoryDescription, setNewStoryDescription] = useState('');\n  const { toast } = useToast();\n\n  // Auto-select the first story when the list loads with no selection\n  useEffect(() => {\n    if (!selectedStoryId && stories && stories.length > 0) {\n      setSelectedStoryId(stories[0].id);\n    }\n  }, [selectedStoryId, stories, setSelectedStoryId]);\n\n  const handleCreateStory = () => {\n    if (!newStoryName.trim()) {\n      toast({\n        title: 'Name required',\n        description: 'Please enter a story name',\n        variant: 'destructive',\n      });\n      return;\n    }\n\n    createStory.mutate(\n      {\n        name: newStoryName.trim(),\n        description: newStoryDescription.trim() || undefined,\n      },\n      {\n        onSuccess: (story) => {\n          setSelectedStoryId(story.id);\n          setCreateDialogOpen(false);\n          setNewStoryName('');\n          setNewStoryDescription('');\n          toast({\n            title: 'Story created',\n            description: `\"${story.name}\" has been created`,\n          });\n        },\n        onError: (error) => {\n          toast({\n            title: 'Failed to create story',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  };\n\n  const handleEditClick = (story: { id: string; name: string; description?: string }) => {\n    setEditingStory(story);\n    setNewStoryName(story.name);\n    setNewStoryDescription(story.description || '');\n    setEditDialogOpen(true);\n  };\n\n  const handleUpdateStory = () => {\n    if (!editingStory || !newStoryName.trim()) {\n      toast({\n        title: 'Name required',\n        description: 'Please enter a story name',\n        variant: 'destructive',\n      });\n      return;\n    }\n\n    updateStory.mutate(\n      {\n        storyId: editingStory.id,\n        data: {\n          name: newStoryName.trim(),\n          description: newStoryDescription.trim() || undefined,\n        },\n      },\n      {\n        onSuccess: () => {\n          setEditDialogOpen(false);\n          setEditingStory(null);\n          setNewStoryName('');\n          setNewStoryDescription('');\n        },\n        onError: (error) => {\n          toast({\n            title: 'Failed to update story',\n            description: error.message,\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  };\n\n  const handleDeleteClick = (storyId: string) => {\n    setDeletingStoryId(storyId);\n    setDeleteDialogOpen(true);\n  };\n\n  const handleDeleteConfirm = () => {\n    if (!deletingStoryId) return;\n\n    deleteStory.mutate(deletingStoryId, {\n      onSuccess: () => {\n        // Clear selection if deleting the currently selected story\n        if (selectedStoryId === deletingStoryId) {\n          setSelectedStoryId(null);\n        }\n        setDeleteDialogOpen(false);\n        setDeletingStoryId(null);\n      },\n      onError: (error) => {\n        toast({\n          title: 'Failed to delete story',\n          description: error.message,\n          variant: 'destructive',\n        });\n      },\n    });\n  };\n\n  if (isLoading) {\n    return (\n      <div className=\"flex items-center justify-center h-full\">\n        <div className=\"text-muted-foreground\">Loading stories...</div>\n      </div>\n    );\n  }\n\n  const storyList = stories || [];\n  const hasTrackEditor = selectedStoryId && selectedStory && selectedStory.items.length > 0;\n\n  return (\n    <div className=\"h-full flex flex-col relative overflow-hidden\">\n      {/* Scroll Mask */}\n      <div className=\"absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-background to-transparent z-10 pointer-events-none\" />\n\n      {/* Fixed Header */}\n      <div className=\"absolute top-0 left-0 right-0 z-20\">\n        <div className=\"flex items-center justify-between mb-4 px-1\">\n          <h2 className=\"text-2xl font-bold\">Stories</h2>\n          <Button onClick={() => setCreateDialogOpen(true)} size=\"sm\">\n            <Plus className=\"mr-2 h-4 w-4\" />\n            New Story\n          </Button>\n        </div>\n      </div>\n\n      {/* Scrollable Story List */}\n      <div\n        className=\"flex-1 overflow-y-auto pt-14 relative z-0\"\n        style={{ paddingBottom: hasTrackEditor ? `${trackEditorHeight + 140}px` : '170px' }}\n      >\n        {storyList.length === 0 ? (\n          <div className=\"text-center py-12 px-5 border-2 border-dashed border-muted rounded-2xl text-muted-foreground\">\n            <BookOpen className=\"h-12 w-12 mx-auto mb-4 opacity-50\" />\n            <p className=\"text-sm\">No stories yet</p>\n            <p className=\"text-xs mt-2\">Create your first story to get started</p>\n          </div>\n        ) : (\n          <div className=\"space-y-0.5\">\n            {storyList.map((story) => (\n              <div\n                key={story.id}\n                role=\"button\"\n                tabIndex={0}\n                className={cn(\n                  'px-5 py-3 rounded-lg transition-colors group flex items-center cursor-pointer',\n                  selectedStoryId === story.id ? 'bg-muted' : 'hover:bg-muted/50',\n                )}\n                aria-label={`Story ${story.name}, ${story.item_count} ${story.item_count === 1 ? 'item' : 'items'}, ${formatDate(story.updated_at)}`}\n                aria-pressed={selectedStoryId === story.id}\n                onClick={() => setSelectedStoryId(story.id)}\n                onKeyDown={(e) => {\n                  if (e.target !== e.currentTarget) return;\n                  if (e.key === 'Enter' || e.key === ' ') {\n                    e.preventDefault();\n                    setSelectedStoryId(story.id);\n                  }\n                }}\n              >\n                <div className=\"flex items-start justify-between gap-2 w-full min-w-0\">\n                  <div className=\"flex-1 min-w-0 text-left overflow-hidden\">\n                    <h3 className=\"text-sm font-medium truncate\">{story.name}</h3>\n                    <div className=\"flex items-center gap-2 mt-1 text-xs text-muted-foreground\">\n                      <span>\n                        {story.item_count} {story.item_count === 1 ? 'item' : 'items'}\n                      </span>\n                      <span>·</span>\n                      <span>{formatDate(story.updated_at)}</span>\n                    </div>\n                  </div>\n                  <DropdownMenu>\n                    <DropdownMenuTrigger asChild>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"icon\"\n                        className=\"h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity\"\n                        onClick={(e) => e.stopPropagation()}\n                        aria-label={`Actions for ${story.name}`}\n                      >\n                        <MoreHorizontal className=\"h-3.5 w-3.5\" />\n                      </Button>\n                    </DropdownMenuTrigger>\n                    <DropdownMenuContent align=\"end\">\n                      <DropdownMenuItem onClick={() => handleEditClick(story)}>\n                        <Pencil className=\"mr-2 h-4 w-4\" />\n                        Edit\n                      </DropdownMenuItem>\n                      <DropdownMenuItem\n                        onClick={() => handleDeleteClick(story.id)}\n                        className=\"text-destructive focus:text-destructive\"\n                      >\n                        <Trash2 className=\"mr-2 h-4 w-4\" />\n                        Delete\n                      </DropdownMenuItem>\n                    </DropdownMenuContent>\n                  </DropdownMenu>\n                </div>\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n\n      {/* Create Story Dialog */}\n      <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Create New Story</DialogTitle>\n            <DialogDescription>\n              Create a new story to organize your voice generations into conversations.\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"space-y-4 py-4\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"story-name\">Name</Label>\n              <Input\n                id=\"story-name\"\n                placeholder=\"My Story\"\n                value={newStoryName}\n                onChange={(e) => setNewStoryName(e.target.value)}\n                onKeyDown={(e) => {\n                  if (e.key === 'Enter') {\n                    handleCreateStory();\n                  }\n                }}\n              />\n            </div>\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"story-description\">Description (optional)</Label>\n              <Textarea\n                id=\"story-description\"\n                placeholder=\"A conversation between...\"\n                value={newStoryDescription}\n                onChange={(e) => setNewStoryDescription(e.target.value)}\n                rows={3}\n              />\n            </div>\n          </div>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setCreateDialogOpen(false)}>\n              Cancel\n            </Button>\n            <Button onClick={handleCreateStory} disabled={createStory.isPending}>\n              {createStory.isPending ? 'Creating...' : 'Create'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Edit Story Dialog */}\n      <Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Edit Story</DialogTitle>\n            <DialogDescription>Update the story name and description.</DialogDescription>\n          </DialogHeader>\n          <div className=\"space-y-4 py-4\">\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"edit-story-name\">Name</Label>\n              <Input\n                id=\"edit-story-name\"\n                placeholder=\"My Story\"\n                value={newStoryName}\n                onChange={(e) => setNewStoryName(e.target.value)}\n                onKeyDown={(e) => {\n                  if (e.key === 'Enter') {\n                    handleUpdateStory();\n                  }\n                }}\n              />\n            </div>\n            <div className=\"space-y-2\">\n              <Label htmlFor=\"edit-story-description\">Description (optional)</Label>\n              <Textarea\n                id=\"edit-story-description\"\n                placeholder=\"A conversation between...\"\n                value={newStoryDescription}\n                onChange={(e) => setNewStoryDescription(e.target.value)}\n                rows={3}\n              />\n            </div>\n          </div>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setEditDialogOpen(false)}>\n              Cancel\n            </Button>\n            <Button onClick={handleUpdateStory} disabled={updateStory.isPending}>\n              {updateStory.isPending ? 'Saving...' : 'Save'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n\n      {/* Delete Story Confirmation Dialog */}\n      <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Are you sure?</AlertDialogTitle>\n            <AlertDialogDescription>\n              This will permanently delete the story and all its items. This action cannot be\n              undone.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel>Cancel</AlertDialogCancel>\n            <AlertDialogAction asChild>\n              <Button\n                onClick={handleDeleteConfirm}\n                disabled={deleteStory.isPending}\n                className=\"bg-destructive text-destructive-foreground hover:bg-destructive/90\"\n              >\n                {deleteStory.isPending ? 'Deleting...' : 'Delete'}\n              </Button>\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/StoriesTab/StoryTrackEditor.tsx",
    "content": "import {\n  Check,\n  Copy,\n  GalleryVerticalEnd,\n  GripHorizontal,\n  Minus,\n  Pause,\n  Play,\n  Plus,\n  Scissors,\n  Square,\n  Trash2,\n} from 'lucide-react';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport WaveSurfer from 'wavesurfer.js';\nimport { Button } from '@/components/ui/button';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { useToast } from '@/components/ui/use-toast';\nimport { apiClient } from '@/lib/api/client';\nimport type { StoryItemDetail } from '@/lib/api/types';\nimport {\n  useDuplicateStoryItem,\n  useMoveStoryItem,\n  useRemoveStoryItem,\n  useSetStoryItemVersion,\n  useSplitStoryItem,\n  useTrimStoryItem,\n} from '@/lib/hooks/useStories';\nimport { cn } from '@/lib/utils/cn';\nimport { useStoryStore } from '@/stores/storyStore';\n\n// Clip waveform component with trim support\nfunction ClipWaveform({\n  generationId,\n  versionId,\n  width,\n  trimStartMs,\n  trimEndMs,\n  duration,\n}: {\n  generationId: string;\n  versionId?: string;\n  width: number;\n  trimStartMs: number;\n  trimEndMs: number;\n  duration: number;\n}) {\n  const waveformRef = useRef<HTMLDivElement>(null);\n  const wavesurferRef = useRef<WaveSurfer | null>(null);\n\n  // Calculate the full waveform width based on the original duration\n  // The visible portion (width) represents the effective duration after trimming\n  const effectiveDurationMs = duration * 1000 - trimStartMs - trimEndMs;\n  const fullWaveformWidth =\n    effectiveDurationMs > 0 ? (width / effectiveDurationMs) * (duration * 1000) : width;\n\n  // Calculate how much to offset the waveform to hide the trimmed start\n  const offsetX =\n    effectiveDurationMs > 0 ? (trimStartMs / (duration * 1000)) * fullWaveformWidth : 0;\n\n  useEffect(() => {\n    if (!waveformRef.current || fullWaveformWidth < 20) return;\n\n    // Get CSS colors\n    const root = document.documentElement;\n    const getCSSVar = (varName: string) => {\n      const value = getComputedStyle(root).getPropertyValue(varName).trim();\n      return value ? `hsl(${value})` : '';\n    };\n\n    const waveColor = getCSSVar('--accent-foreground');\n\n    const wavesurfer = WaveSurfer.create({\n      container: waveformRef.current,\n      waveColor,\n      progressColor: waveColor,\n      cursorWidth: 0,\n      barWidth: 1,\n      barRadius: 1,\n      barGap: 1,\n      height: 28,\n      normalize: true,\n      interact: false,\n    });\n\n    wavesurferRef.current = wavesurfer;\n\n    const audioUrl = versionId\n      ? apiClient.getVersionAudioUrl(versionId)\n      : apiClient.getAudioUrl(generationId);\n    wavesurfer.load(audioUrl).catch(() => {\n      // Ignore load errors\n    });\n\n    return () => {\n      wavesurfer.destroy();\n      wavesurferRef.current = null;\n    };\n  }, [generationId, versionId, fullWaveformWidth]);\n\n  return (\n    <div className=\"w-full h-full opacity-60 overflow-hidden\">\n      {/* Inner container that holds the full waveform, offset to show only visible portion */}\n      <div\n        ref={waveformRef}\n        style={{\n          width: `${fullWaveformWidth}px`,\n          transform: `translateX(-${offsetX}px)`,\n        }}\n        className=\"h-full\"\n      />\n    </div>\n  );\n}\n\ninterface StoryTrackEditorProps {\n  storyId: string;\n  items: StoryItemDetail[];\n}\n\nconst TRACK_HEIGHT = 48;\nconst TIME_RULER_HEIGHT = 24; // h-6 = 1.5rem = 24px\nconst MIN_PIXELS_PER_SECOND = 10;\nconst MAX_PIXELS_PER_SECOND = 200;\nconst DEFAULT_PIXELS_PER_SECOND = 50;\nconst DEFAULT_TRACKS = [1, 0, -1]; // Default 3 tracks\nconst MIN_EDITOR_HEIGHT = 120;\nconst MAX_EDITOR_HEIGHT = 500;\n\nexport function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) {\n  const [pixelsPerSecond, setPixelsPerSecond] = useState(DEFAULT_PIXELS_PER_SECOND);\n  const [draggingItem, setDraggingItem] = useState<string | null>(null);\n  const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });\n  const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });\n  const [isResizing, setIsResizing] = useState(false);\n  const [containerWidth, setContainerWidth] = useState(0);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const tracksRef = useRef<HTMLDivElement>(null);\n  const resizeStartY = useRef(0);\n  const resizeStartHeight = useRef(0);\n  const moveItem = useMoveStoryItem();\n  const trimItem = useTrimStoryItem();\n  const splitItem = useSplitStoryItem();\n  const duplicateItem = useDuplicateStoryItem();\n  const removeItem = useRemoveStoryItem();\n  const setItemVersion = useSetStoryItemVersion();\n  const { toast } = useToast();\n\n  // Selection state\n  const selectedClipId = useStoryStore((state) => state.selectedClipId);\n  const setSelectedClipId = useStoryStore((state) => state.setSelectedClipId);\n\n  // Selected clip item (for version picker)\n  const selectedItem = useMemo(\n    () => (selectedClipId ? items.find((i) => i.id === selectedClipId) : undefined),\n    [selectedClipId, items],\n  );\n  const selectedItemVersions = selectedItem?.versions;\n  const hasMultipleVersions = selectedItemVersions && selectedItemVersions.length > 1;\n\n  // Determine which version label is active for the selected clip\n  const activeVersionLabel = useMemo(() => {\n    if (!selectedItem || !selectedItemVersions) return null;\n    // If the item has a pinned version_id, find its label\n    if (selectedItem.version_id) {\n      const pinned = selectedItemVersions.find((v) => v.id === selectedItem.version_id);\n      return pinned?.label ?? null;\n    }\n    // Otherwise use the generation's default version\n    const defaultVersion = selectedItemVersions.find((v) => v.is_default);\n    return defaultVersion?.label ?? null;\n  }, [selectedItem, selectedItemVersions]);\n\n  const handleSetVersion = useCallback(\n    (versionId: string | null) => {\n      if (!selectedClipId) return;\n      setItemVersion.mutate(\n        {\n          storyId,\n          itemId: selectedClipId,\n          data: { version_id: versionId },\n        },\n        {\n          onError: (error) => {\n            toast({\n              title: 'Failed to set version',\n              description: error instanceof Error ? error.message : String(error),\n              variant: 'destructive',\n            });\n          },\n        },\n      );\n    },\n    [selectedClipId, storyId, setItemVersion, toast],\n  );\n\n  // Trim state\n  const [trimmingItem, setTrimmingItem] = useState<string | null>(null);\n  const [trimSide, setTrimSide] = useState<'start' | 'end' | null>(null);\n  const [trimStartX, setTrimStartX] = useState(0);\n  const [tempTrimValues, setTempTrimValues] = useState<{\n    trim_start_ms: number;\n    trim_end_ms: number;\n  } | null>(null);\n\n  // Track editor height from store (shared with FloatingGenerateBox)\n  const editorHeight = useStoryStore((state) => state.trackEditorHeight);\n  const setEditorHeight = useStoryStore((state) => state.setTrackEditorHeight);\n\n  // Playback state\n  const isPlaying = useStoryStore((state) => state.isPlaying);\n  const currentTimeMs = useStoryStore((state) => state.currentTimeMs);\n  const playbackStoryId = useStoryStore((state) => state.playbackStoryId);\n  const play = useStoryStore((state) => state.play);\n  const pause = useStoryStore((state) => state.pause);\n  const stop = useStoryStore((state) => state.stop);\n  const seek = useStoryStore((state) => state.seek);\n  const setActiveStory = useStoryStore((state) => state.setActiveStory);\n\n  const isActiveStory = playbackStoryId === storyId;\n  const isCurrentlyPlaying = isPlaying && isActiveStory;\n\n  // Auto-activate this story when the editor is shown so playhead is visible\n  useEffect(() => {\n    if (items.length > 0 && !isActiveStory) {\n      const totalDuration = Math.max(\n        ...items.map((item) => {\n          const trimStart = item.trim_start_ms || 0;\n          const trimEnd = item.trim_end_ms || 0;\n          const effectiveDuration = item.duration * 1000 - trimStart - trimEnd;\n          return item.start_time_ms + effectiveDuration;\n        }),\n        0,\n      );\n      setActiveStory(storyId, items, totalDuration);\n    }\n  }, [storyId, items, isActiveStory, setActiveStory]);\n\n  // Sort items by start time for play\n  const sortedItems = useMemo(() => {\n    return [...items].sort((a, b) => a.start_time_ms - b.start_time_ms);\n  }, [items]);\n\n  const handlePlayPause = () => {\n    if (isCurrentlyPlaying) {\n      pause();\n    } else {\n      play(storyId, sortedItems);\n    }\n  };\n\n  const handleStop = () => {\n    stop();\n  };\n\n  // Calculate unique tracks from items, always showing at least 3 default tracks\n  const tracks = useMemo(() => {\n    const trackSet = new Set([...DEFAULT_TRACKS, ...items.map((item) => item.track)]);\n    return Array.from(trackSet).sort((a, b) => b - a); // Higher tracks on top\n  }, [items]);\n\n  // Track container width for full-width minimum\n  useEffect(() => {\n    const container = tracksRef.current;\n    if (!container) return;\n\n    const observer = new ResizeObserver((entries) => {\n      for (const entry of entries) {\n        setContainerWidth(entry.contentRect.width);\n      }\n    });\n\n    observer.observe(container);\n    // Set initial width\n    setContainerWidth(container.clientWidth);\n\n    return () => observer.disconnect();\n  }, []);\n\n  // Calculate effective duration (accounting for trims)\n  const getEffectiveDuration = (item: StoryItemDetail) => {\n    return item.duration * 1000 - (item.trim_start_ms || 0) - (item.trim_end_ms || 0);\n  };\n\n  // Calculate total duration (using effective durations)\n  const totalDurationMs = useMemo(() => {\n    if (items.length === 0) return 10000; // Default 10 seconds\n    return Math.max(...items.map((item) => item.start_time_ms + getEffectiveDuration(item)), 10000);\n  }, [items, getEffectiveDuration]);\n\n  // Calculate timeline width - at least full container width\n  const contentWidth = (totalDurationMs / 1000) * pixelsPerSecond + 200; // Content width with padding\n  const timelineWidth = Math.max(contentWidth, containerWidth);\n\n  // Generate time markers\n  const timeMarkers = useMemo(() => {\n    const markers: number[] = [];\n    // Determine interval based on zoom level\n    let intervalMs = 5000; // 5 seconds\n    if (pixelsPerSecond > 100) intervalMs = 1000;\n    else if (pixelsPerSecond > 50) intervalMs = 2000;\n    else if (pixelsPerSecond < 20) intervalMs = 10000;\n\n    for (let ms = 0; ms <= totalDurationMs + intervalMs; ms += intervalMs) {\n      markers.push(ms);\n    }\n    return markers;\n  }, [totalDurationMs, pixelsPerSecond]);\n\n  const formatTime = (ms: number): string => {\n    const totalSeconds = Math.floor(ms / 1000);\n    const minutes = Math.floor(totalSeconds / 60);\n    const seconds = totalSeconds % 60;\n    return `${minutes}:${seconds.toString().padStart(2, '0')}`;\n  };\n\n  const msToPixels = useCallback((ms: number) => (ms / 1000) * pixelsPerSecond, [pixelsPerSecond]);\n\n  const pixelsToMs = useCallback((px: number) => (px / pixelsPerSecond) * 1000, [pixelsPerSecond]);\n\n  const handleZoomIn = () => {\n    setPixelsPerSecond((prev) => Math.min(prev * 1.5, MAX_PIXELS_PER_SECOND));\n  };\n\n  const handleZoomOut = () => {\n    setPixelsPerSecond((prev) => Math.max(prev / 1.5, MIN_PIXELS_PER_SECOND));\n  };\n\n  // Resize handlers\n  const handleResizeStart = useCallback(\n    (e: React.MouseEvent) => {\n      e.preventDefault();\n      setIsResizing(true);\n      resizeStartY.current = e.clientY;\n      resizeStartHeight.current = editorHeight;\n    },\n    [editorHeight],\n  );\n\n  const handleResizeMove = useCallback(\n    (e: MouseEvent) => {\n      if (!isResizing) return;\n      const deltaY = resizeStartY.current - e.clientY;\n      const newHeight = Math.min(\n        MAX_EDITOR_HEIGHT,\n        Math.max(MIN_EDITOR_HEIGHT, resizeStartHeight.current + deltaY),\n      );\n      setEditorHeight(newHeight);\n    },\n    [isResizing, setEditorHeight],\n  );\n\n  const handleResizeEnd = useCallback(() => {\n    setIsResizing(false);\n  }, []);\n\n  // Add global mouse listeners for resizing\n  useEffect(() => {\n    if (isResizing) {\n      window.addEventListener('mousemove', handleResizeMove);\n      window.addEventListener('mouseup', handleResizeEnd);\n      return () => {\n        window.removeEventListener('mousemove', handleResizeMove);\n        window.removeEventListener('mouseup', handleResizeEnd);\n      };\n    }\n  }, [isResizing, handleResizeMove, handleResizeEnd]);\n\n  const handleTimelineClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (!tracksRef.current || draggingItem || trimmingItem) return;\n    const rect = tracksRef.current.getBoundingClientRect();\n    const x = e.clientX - rect.left + tracksRef.current.scrollLeft;\n    const timeMs = Math.max(0, pixelsToMs(x));\n    seek(timeMs);\n    // Deselect clip when clicking on timeline\n    setSelectedClipId(null);\n  };\n\n  const handleClipClick = (e: React.MouseEvent, item: StoryItemDetail) => {\n    e.stopPropagation();\n    if (draggingItem || trimmingItem) return;\n    setSelectedClipId(item.id);\n  };\n\n  const handleTrimStart = (e: React.MouseEvent, item: StoryItemDetail, side: 'start' | 'end') => {\n    e.stopPropagation();\n    if (!tracksRef.current) return;\n    setTrimmingItem(item.id);\n    setTrimSide(side);\n    setSelectedClipId(item.id);\n    setTrimStartX(e.clientX);\n    trimStartItemRef.current = {\n      item,\n      initialTrimStart: item.trim_start_ms || 0,\n      initialTrimEnd: item.trim_end_ms || 0,\n    };\n  };\n\n  const trimStartItemRef = useRef<{\n    item: StoryItemDetail;\n    initialTrimStart: number;\n    initialTrimEnd: number;\n  } | null>(null);\n\n  const handleTrimMove = useCallback(\n    (e: MouseEvent) => {\n      if (!trimmingItem || !trimSide || !trimStartItemRef.current) return;\n\n      const deltaX = e.clientX - trimStartX;\n      const deltaMs = pixelsToMs(deltaX); // Signed delta in milliseconds\n\n      const { item, initialTrimStart, initialTrimEnd } = trimStartItemRef.current;\n      const originalDurationMs = item.duration * 1000;\n\n      let newTrimStart = initialTrimStart;\n      let newTrimEnd = initialTrimEnd;\n\n      if (trimSide === 'start') {\n        // Moving right increases trim_start (trims more from start)\n        // Moving left decreases trim_start (restores from start)\n        newTrimStart = Math.round(\n          Math.max(\n            0,\n            Math.min(initialTrimStart + deltaMs, originalDurationMs - initialTrimEnd - 100),\n          ),\n        );\n      } else {\n        // Moving right decreases trim_end (restores from end)\n        // Moving left increases trim_end (trims more from end)\n        newTrimEnd = Math.round(\n          Math.max(\n            0,\n            Math.min(initialTrimEnd - deltaMs, originalDurationMs - initialTrimStart - 100),\n          ),\n        );\n      }\n\n      // Validate that we don't exceed duration\n      if (newTrimStart + newTrimEnd >= originalDurationMs - 100) {\n        return; // Don't allow trimming to less than 100ms\n      }\n\n      // Update temporary trim values for visual feedback\n      setTempTrimValues({\n        trim_start_ms: newTrimStart,\n        trim_end_ms: newTrimEnd,\n      });\n    },\n    [trimmingItem, trimSide, trimStartX, pixelsToMs],\n  );\n\n  const handleTrimEnd = useCallback(() => {\n    if (!trimmingItem || !trimSide || !trimStartItemRef.current) {\n      setTrimmingItem(null);\n      setTrimSide(null);\n      setTempTrimValues(null);\n      trimStartItemRef.current = null;\n      return;\n    }\n\n    const { initialTrimStart, initialTrimEnd } = trimStartItemRef.current;\n\n    // Use temporary trim values if available, otherwise use initial values\n    // Ensure values are integers for the backend\n    const finalTrimStart = Math.round(tempTrimValues?.trim_start_ms ?? initialTrimStart);\n    const finalTrimEnd = Math.round(tempTrimValues?.trim_end_ms ?? initialTrimEnd);\n\n    // Only update if values changed\n    if (finalTrimStart !== initialTrimStart || finalTrimEnd !== initialTrimEnd) {\n      trimItem.mutate(\n        {\n          storyId,\n          itemId: trimmingItem,\n          data: {\n            trim_start_ms: finalTrimStart,\n            trim_end_ms: finalTrimEnd,\n          },\n        },\n        {\n          onError: (error) => {\n            toast({\n              title: 'Failed to trim clip',\n              description: error instanceof Error ? error.message : String(error),\n              variant: 'destructive',\n            });\n          },\n        },\n      );\n    }\n\n    setTrimmingItem(null);\n    setTrimSide(null);\n    setTempTrimValues(null);\n    trimStartItemRef.current = null;\n  }, [trimmingItem, trimSide, tempTrimValues, storyId, trimItem, toast]);\n\n  const handleSplit = useCallback(() => {\n    if (!selectedClipId) return;\n\n    const item = items.find((i) => i.id === selectedClipId);\n    if (!item) return;\n\n    const splitTimeMs = currentTimeMs - item.start_time_ms;\n    const effectiveDuration = getEffectiveDuration(item);\n\n    if (splitTimeMs <= 0 || splitTimeMs >= effectiveDuration) {\n      toast({\n        title: 'Invalid split point',\n        description: 'Playhead must be within the selected clip',\n        variant: 'destructive',\n      });\n      return;\n    }\n\n    splitItem.mutate(\n      {\n        storyId,\n        itemId: selectedClipId,\n        data: { split_time_ms: splitTimeMs },\n      },\n      {\n        onSuccess: () => {\n          setSelectedClipId(null);\n        },\n        onError: (error) => {\n          toast({\n            title: 'Failed to split clip',\n            description: error instanceof Error ? error.message : String(error),\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  }, [\n    selectedClipId,\n    items,\n    currentTimeMs,\n    getEffectiveDuration,\n    storyId,\n    splitItem,\n    toast,\n    setSelectedClipId,\n  ]);\n\n  const handleDuplicate = useCallback(() => {\n    if (!selectedClipId) return;\n\n    duplicateItem.mutate(\n      {\n        storyId,\n        itemId: selectedClipId,\n      },\n      {\n        onError: (error) => {\n          toast({\n            title: 'Failed to duplicate clip',\n            description: error instanceof Error ? error.message : String(error),\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  }, [selectedClipId, storyId, duplicateItem, toast]);\n\n  const handleDelete = useCallback(() => {\n    if (!selectedClipId) return;\n\n    removeItem.mutate(\n      {\n        storyId,\n        itemId: selectedClipId,\n      },\n      {\n        onSuccess: () => {\n          setSelectedClipId(null);\n        },\n        onError: (error) => {\n          toast({\n            title: 'Failed to delete clip',\n            description: error instanceof Error ? error.message : String(error),\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  }, [selectedClipId, storyId, removeItem, toast, setSelectedClipId]);\n\n  // Keyboard shortcuts\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      // Only handle shortcuts when editor is focused or no input is focused\n      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {\n        return;\n      }\n\n      if (e.key === ' ') {\n        e.preventDefault();\n        handlePlayPause();\n      } else if (e.key === 'Escape') {\n        setSelectedClipId(null);\n      } else if (e.key === 's' || e.key === 'S') {\n        if (selectedClipId) {\n          e.preventDefault();\n          handleSplit();\n        }\n      } else if (e.key === 'd' || e.key === 'D') {\n        if (selectedClipId && (e.metaKey || e.ctrlKey)) {\n          e.preventDefault();\n          handleDuplicate();\n        }\n      } else if (e.key === 'Delete' || e.key === 'Backspace') {\n        if (selectedClipId) {\n          e.preventDefault();\n          handleDelete();\n        }\n      }\n    };\n\n    window.addEventListener('keydown', handleKeyDown);\n    return () => window.removeEventListener('keydown', handleKeyDown);\n  }, [\n    selectedClipId,\n    handleSplit,\n    handleDuplicate,\n    handleDelete,\n    setSelectedClipId,\n    handlePlayPause,\n  ]);\n\n  // Add global mouse listeners for trimming\n  useEffect(() => {\n    if (trimmingItem) {\n      window.addEventListener('mousemove', handleTrimMove);\n      window.addEventListener('mouseup', handleTrimEnd);\n      return () => {\n        window.removeEventListener('mousemove', handleTrimMove);\n        window.removeEventListener('mouseup', handleTrimEnd);\n      };\n    }\n  }, [trimmingItem, handleTrimMove, handleTrimEnd]);\n\n  const handleDragStart = (e: React.MouseEvent, item: StoryItemDetail) => {\n    e.stopPropagation();\n    if (!tracksRef.current) return;\n\n    const rect = e.currentTarget.getBoundingClientRect();\n    setDragOffset({\n      x: e.clientX - rect.left,\n      y: e.clientY - rect.top,\n    });\n    setDragPosition({\n      x: rect.left - tracksRef.current.getBoundingClientRect().left + tracksRef.current.scrollLeft,\n      // Subtract ruler height since clips are positioned relative to tracks area, not the scrollable container\n      y: rect.top - tracksRef.current.getBoundingClientRect().top - TIME_RULER_HEIGHT,\n    });\n    setDraggingItem(item.id);\n  };\n\n  const handleDragMove = useCallback(\n    (e: React.MouseEvent) => {\n      if (!draggingItem || !tracksRef.current) return;\n\n      const rect = tracksRef.current.getBoundingClientRect();\n      const x = e.clientX - rect.left + tracksRef.current.scrollLeft - dragOffset.x;\n      // Subtract ruler height since clips are positioned relative to tracks area\n      const y = e.clientY - rect.top - dragOffset.y - TIME_RULER_HEIGHT;\n\n      setDragPosition({ x: Math.max(0, x), y });\n    },\n    [draggingItem, dragOffset],\n  );\n\n  const handleDragEnd = useCallback(() => {\n    if (!draggingItem || !tracksRef.current) {\n      setDraggingItem(null);\n      return;\n    }\n\n    const item = items.find((i) => i.id === draggingItem);\n    if (!item) {\n      setDraggingItem(null);\n      return;\n    }\n\n    // Calculate new time from x position\n    const newTimeMs = Math.max(0, Math.round(pixelsToMs(dragPosition.x)));\n\n    // Calculate new track from y position\n    const trackIndex = Math.floor(dragPosition.y / TRACK_HEIGHT);\n    const clampedTrackIndex = Math.max(0, Math.min(trackIndex, tracks.length - 1));\n    const newTrack = tracks[clampedTrackIndex] ?? 0;\n\n    // Check if position changed\n    if (newTimeMs !== item.start_time_ms || newTrack !== item.track) {\n      moveItem.mutate(\n        {\n          storyId,\n          itemId: item.id,\n          data: {\n            start_time_ms: newTimeMs,\n            track: newTrack,\n          },\n        },\n        {\n          onError: (error) => {\n            toast({\n              title: 'Failed to move item',\n              description: error instanceof Error ? error.message : String(error),\n              variant: 'destructive',\n            });\n          },\n        },\n      );\n    }\n\n    setDraggingItem(null);\n  }, [draggingItem, dragPosition, items, tracks, pixelsToMs, storyId, moveItem, toast]);\n\n  // Get track index for rendering\n  const getTrackIndex = (trackNumber: number) => tracks.indexOf(trackNumber);\n\n  // Calculate clip position and dimensions\n  const getClipStyle = (item: StoryItemDetail) => {\n    const isDragging = draggingItem === item.id;\n    const trackIndex = getTrackIndex(item.track);\n    const effectiveDuration = getEffectiveDuration(item);\n    const width = msToPixels(effectiveDuration);\n    const left = isDragging ? dragPosition.x : msToPixels(item.start_time_ms);\n    const top = isDragging ? dragPosition.y : trackIndex * TRACK_HEIGHT;\n\n    return {\n      width: `${width}px`,\n      left: `${left}px`,\n      top: `${top}px`,\n      height: `${TRACK_HEIGHT - 4}px`,\n    };\n  };\n\n  // Playhead position\n  const playheadLeft = msToPixels(currentTimeMs);\n\n  // Auto-scroll timeline to follow playhead during playback\n  useEffect(() => {\n    if (!isCurrentlyPlaying || !tracksRef.current) return;\n\n    const container = tracksRef.current;\n    const containerWidth = container.clientWidth;\n    const scrollLeft = container.scrollLeft;\n    const halfwayPoint = scrollLeft + containerWidth / 2;\n\n    // If playhead is past the halfway point, scroll to keep it centered\n    if (playheadLeft > halfwayPoint) {\n      const targetScroll = playheadLeft - containerWidth / 2;\n      container.scrollLeft = targetScroll;\n    }\n  }, [isCurrentlyPlaying, playheadLeft]);\n\n  // Calculate tracks area height\n  const tracksAreaHeight = tracks.length * TRACK_HEIGHT;\n  const timelineContainerHeight = editorHeight - 40; // Subtract toolbar height\n\n  if (items.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"fixed bottom-0 left-0 right-0 border-t bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 z-50\">\n      <div\n        className=\"border-t bg-background/30 backdrop-blur-2xl overflow-hidden relative\"\n        ref={containerRef}\n      >\n        {/* Resize handle at top */}\n        <button\n          type=\"button\"\n          className=\"absolute top-0 left-0 right-0 h-2 cursor-ns-resize flex items-center justify-center hover:bg-muted/50 transition-colors z-20 group\"\n          onMouseDown={handleResizeStart}\n          aria-label=\"Resize track editor\"\n        >\n          <GripHorizontal className=\"h-3 w-3 text-muted-foreground/50 group-hover:text-muted-foreground\" />\n        </button>\n\n        {/* Toolbar */}\n        <div className=\"flex items-center justify-between px-3 py-2 border-b bg-muted/30 mt-2\">\n          {/* Play controls - left side */}\n          <div className=\"flex items-center gap-2\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"h-7 w-7\"\n              onClick={handlePlayPause}\n              title=\"Play/Pause (Space)\"\n              aria-label={isCurrentlyPlaying ? 'Pause' : 'Play'}\n            >\n              {isCurrentlyPlaying ? <Pause className=\"h-4 w-4\" /> : <Play className=\"h-4 w-4\" />}\n            </Button>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"h-7 w-7\"\n              onClick={handleStop}\n              disabled={!isCurrentlyPlaying}\n              aria-label=\"Stop\"\n            >\n              <Square className=\"h-3 w-3\" />\n            </Button>\n            <span className=\"text-xs text-muted-foreground tabular-nums ml-2\">\n              {formatTime(currentTimeMs)} / {formatTime(totalDurationMs)}\n            </span>\n          </div>\n\n          {/* Clip editing controls - center */}\n          {selectedClipId && (\n            <div className=\"flex items-center gap-1\">\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"h-7 w-7\"\n                onClick={handleSplit}\n                title=\"Split at playhead (S)\"\n                aria-label=\"Split at playhead\"\n              >\n                <Scissors className=\"h-4 w-4\" />\n              </Button>\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"h-7 w-7\"\n                onClick={handleDuplicate}\n                title=\"Duplicate (Cmd/Ctrl+D)\"\n                aria-label=\"Duplicate clip\"\n              >\n                <Copy className=\"h-4 w-4\" />\n              </Button>\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"h-7 w-7\"\n                onClick={handleDelete}\n                title=\"Delete (Delete/Backspace)\"\n                aria-label=\"Delete clip\"\n              >\n                <Trash2 className=\"h-4 w-4\" />\n              </Button>\n              {hasMultipleVersions && (\n                <>\n                  <div className=\"w-px h-4 bg-border mx-1\" />\n                  <DropdownMenu>\n                    <DropdownMenuTrigger asChild>\n                      <Button\n                        variant=\"ghost\"\n                        className=\"h-7 gap-1.5 px-2 text-xs\"\n                        title=\"Change version/take\"\n                      >\n                        <GalleryVerticalEnd className=\"h-3.5 w-3.5\" />\n                        <span className=\"max-w-[80px] truncate\">\n                          {activeVersionLabel ?? 'default'}\n                        </span>\n                      </Button>\n                    </DropdownMenuTrigger>\n                    <DropdownMenuContent align=\"center\" className=\"min-w-[160px]\">\n                      {selectedItemVersions.map((version) => {\n                        const isActive = selectedItem?.version_id\n                          ? version.id === selectedItem.version_id\n                          : version.is_default;\n                        return (\n                          <DropdownMenuItem\n                            key={version.id}\n                            onClick={() => handleSetVersion(version.id)}\n                            className=\"gap-2 text-xs\"\n                          >\n                            <Check\n                              className={cn('h-3 w-3', isActive ? 'opacity-100' : 'opacity-0')}\n                            />\n                            <span className=\"truncate\">{version.label}</span>\n                            {version.effects_chain && version.effects_chain.length > 0 && (\n                              <span className=\"text-muted-foreground ml-auto text-[10px]\">\n                                {version.effects_chain.length} fx\n                              </span>\n                            )}\n                          </DropdownMenuItem>\n                        );\n                      })}\n                    </DropdownMenuContent>\n                  </DropdownMenu>\n                </>\n              )}\n            </div>\n          )}\n\n          {/* Zoom controls - right side */}\n          <div className=\"flex items-center gap-2\">\n            <span className=\"text-xs text-muted-foreground\">Zoom:</span>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"h-6 w-6\"\n              onClick={handleZoomOut}\n              aria-label=\"Zoom out\"\n            >\n              <Minus className=\"h-3 w-3\" />\n            </Button>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"h-6 w-6\"\n              onClick={handleZoomIn}\n              aria-label=\"Zoom in\"\n            >\n              <Plus className=\"h-3 w-3\" />\n            </Button>\n          </div>\n        </div>\n\n        {/* Timeline container with track labels sidebar */}\n        <div className=\"flex\" style={{ height: `${timelineContainerHeight}px` }}>\n          {/* Track labels sidebar - fixed width */}\n          <div className=\"w-16 shrink-0 border-r bg-muted/20 overflow-hidden\">\n            {/* Spacer for time ruler */}\n            <div className=\"h-6 border-b bg-muted/30\" />\n            {/* Track labels */}\n            <div style={{ height: `${tracksAreaHeight}px` }}>\n              {tracks.map((trackNumber, index) => (\n                <div\n                  key={trackNumber}\n                  className={cn(\n                    'border-b flex items-center justify-center',\n                    index % 2 === 0 ? 'bg-background' : 'bg-muted/10',\n                  )}\n                  style={{ height: `${TRACK_HEIGHT}px` }}\n                >\n                  <span className=\"text-[10px] text-muted-foreground select-none\">\n                    {trackNumber}\n                  </span>\n                </div>\n              ))}\n            </div>\n          </div>\n\n          {/* Scrollable timeline area */}\n          {/* biome-ignore lint/a11y/noStaticElementInteractions: Container handles drag events for child clips */}\n          <div\n            ref={tracksRef}\n            className=\"overflow-auto relative flex-1\"\n            onMouseMove={draggingItem ? handleDragMove : undefined}\n            onMouseUp={draggingItem ? handleDragEnd : undefined}\n            onMouseLeave={draggingItem ? handleDragEnd : undefined}\n          >\n            {/* Time ruler - clickable to seek */}\n            <button\n              type=\"button\"\n              className=\"h-6 border-b bg-muted/20 sticky top-0 z-10 cursor-pointer text-left\"\n              style={{ width: `${timelineWidth}px` }}\n              onClick={handleTimelineClick}\n              aria-label=\"Seek timeline\"\n            >\n              {timeMarkers.map((ms) => (\n                <div\n                  key={ms}\n                  className=\"absolute top-0 h-full flex flex-col justify-end pointer-events-none\"\n                  style={{ left: `${msToPixels(ms)}px` }}\n                >\n                  <div className=\"h-2 w-px bg-border\" />\n                  <span className=\"text-[10px] text-muted-foreground ml-1 select-none\">\n                    {formatTime(ms)}\n                  </span>\n                </div>\n              ))}\n            </button>\n\n            {/* Tracks area */}\n            <div\n              className=\"relative\"\n              style={{ width: `${timelineWidth}px`, height: `${tracksAreaHeight}px` }}\n            >\n              {/* Track backgrounds - pointer-events-none to allow clicks to pass through */}\n              {tracks.map((trackNumber, index) => (\n                <div\n                  key={trackNumber}\n                  className={cn(\n                    'absolute left-0 right-0 border-b pointer-events-none',\n                    index % 2 === 0 ? 'bg-background' : 'bg-muted/10',\n                  )}\n                  style={{\n                    top: `${index * TRACK_HEIGHT}px`,\n                    height: `${TRACK_HEIGHT}px`,\n                  }}\n                />\n              ))}\n\n              {/* Click area for seeking - z-index lower than clips */}\n              <button\n                type=\"button\"\n                className=\"absolute inset-0 z-0 cursor-pointer\"\n                onClick={handleTimelineClick}\n                aria-label=\"Seek timeline\"\n              />\n\n              {/* Audio clips */}\n              {items.map((item) => {\n                const isDragging = draggingItem === item.id;\n                const isSelected = selectedClipId === item.id;\n                const isTrimming = trimmingItem === item.id;\n\n                // Use temporary trim values during trimming for visual feedback\n                const displayTrimStart =\n                  isTrimming && tempTrimValues\n                    ? tempTrimValues.trim_start_ms\n                    : item.trim_start_ms || 0;\n                const displayTrimEnd =\n                  isTrimming && tempTrimValues ? tempTrimValues.trim_end_ms : item.trim_end_ms || 0;\n                const effectiveDuration = item.duration * 1000 - displayTrimStart - displayTrimEnd;\n\n                const style = getClipStyle({\n                  ...item,\n                  trim_start_ms: displayTrimStart,\n                  trim_end_ms: displayTrimEnd,\n                });\n                const clipWidth = msToPixels(effectiveDuration);\n\n                return (\n                  <div\n                    key={item.id}\n                    className={cn(\n                      'absolute rounded select-none overflow-visible z-10',\n                      isSelected && 'ring-2 ring-primary ring-offset-1',\n                      isTrimming && 'ring-2 ring-accent',\n                    )}\n                    style={style}\n                  >\n                    <button\n                      type=\"button\"\n                      className={cn(\n                        'w-full h-full rounded cursor-move overflow-hidden',\n                        'bg-accent/80 hover:bg-accent border border-accent-foreground/20',\n                        'flex flex-col justify-center',\n                        isDragging && 'opacity-80 shadow-lg z-20',\n                        !isDragging && 'transition-all duration-100',\n                      )}\n                      onClick={(e) => handleClipClick(e, item)}\n                      onMouseDown={(e) => {\n                        // Only start drag if not clicking on trim handles\n                        if (!(e.target as HTMLElement).closest('.trim-handle')) {\n                          handleDragStart(e, item);\n                        }\n                      }}\n                    >\n                      {/* Clip label */}\n                      <div className=\"absolute top-0 left-1 right-1 z-10\">\n                        <p className=\"text-[9px] font-medium text-accent-foreground truncate\">\n                          {item.profile_name}\n                        </p>\n                      </div>\n                      {/* Waveform */}\n                      <div className=\"absolute inset-0 top-3\">\n                        <ClipWaveform\n                          generationId={item.generation_id}\n                          versionId={item.version_id}\n                          width={clipWidth}\n                          trimStartMs={displayTrimStart}\n                          trimEndMs={displayTrimEnd}\n                          duration={item.duration}\n                        />\n                      </div>\n                    </button>\n\n                    {/* Trim handles */}\n                    {isSelected && (\n                      <>\n                        {/* Left trim handle */}\n                        <button\n                          type=\"button\"\n                          className=\"trim-handle absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize hover:bg-primary/30 bg-primary/20 z-30 rounded-l\"\n                          onMouseDown={(e) => handleTrimStart(e, item, 'start')}\n                          aria-label=\"Trim start\"\n                        />\n                        {/* Right trim handle */}\n                        <button\n                          type=\"button\"\n                          className=\"trim-handle absolute right-0 top-0 bottom-0 w-2 cursor-ew-resize hover:bg-primary/30 bg-primary/20 z-30 rounded-r\"\n                          onMouseDown={(e) => handleTrimStart(e, item, 'end')}\n                          aria-label=\"Trim end\"\n                        />\n                      </>\n                    )}\n                  </div>\n                );\n              })}\n\n              {/* Playhead - always visible */}\n              <div\n                className=\"absolute top-0 bottom-0 w-1 bg-accent z-30 pointer-events-none rounded-full\"\n                style={{ left: `${playheadLeft}px` }}\n              >\n                <div className=\"absolute -top-1 left-1/2 -translate-x-1/2 w-3 h-3 bg-accent rounded-full\" />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/TitleBarDragRegion.tsx",
    "content": "const isWindows = navigator.userAgent.includes('Windows');\n\nexport function TitleBarDragRegion() {\n  if (isWindows) return null;\n\n  return <div data-tauri-drag-region className=\"fixed top-0 left-0 right-0 h-12 z-[9999]\" />;\n}\n"
  },
  {
    "path": "app/src/components/VoiceProfiles/AudioSampleRecording.tsx",
    "content": "import { Mic, Pause, Play, Square } from 'lucide-react';\nimport { memo, useEffect, useState } from 'react';\nimport { Visualizer } from 'react-sound-visualizer';\nimport { Button } from '@/components/ui/button';\nimport { FormControl, FormItem, FormMessage } from '@/components/ui/form';\nimport { formatAudioDuration } from '@/lib/utils/audio';\n\nconst MemoizedWaveform = memo(function MemoizedWaveform({\n  audioStream,\n}: {\n  audioStream: MediaStream;\n}) {\n  return (\n    <div className=\"absolute inset-0 pointer-events-none flex items-center justify-center opacity-30\">\n      <Visualizer audio={audioStream} autoStart strokeColor=\"#b39a3d\">\n        {({ canvasRef }) => (\n          <canvas\n            ref={canvasRef}\n            width={500}\n            height={150}\n            className=\"w-full h-full\"\n          />\n        )}\n      </Visualizer>\n    </div>\n  );\n});\n\ninterface AudioSampleRecordingProps {\n  file: File | null | undefined;\n  isRecording: boolean;\n  duration: number;\n  onStart: () => void;\n  onStop: () => void;\n  onCancel: () => void;\n  onTranscribe: () => void;\n  onPlayPause: () => void;\n  isPlaying: boolean;\n  isTranscribing?: boolean;\n  showWaveform?: boolean;\n}\n\nexport function AudioSampleRecording({\n  file,\n  isRecording,\n  duration,\n  onStart,\n  onStop,\n  onCancel,\n  onTranscribe,\n  onPlayPause,\n  isPlaying,\n  isTranscribing = false,\n  showWaveform = true,\n}: AudioSampleRecordingProps) {\n  const [audioStream, setAudioStream] = useState<MediaStream | null>(null);\n\n  // Request microphone access when component mounts\n  useEffect(() => {\n    if (!showWaveform) return;\n    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) return;\n\n    let stream: MediaStream | null = null;\n\n    navigator.mediaDevices\n      .getUserMedia({ audio: true, video: false })\n      .then((s) => {\n        stream = s;\n        setAudioStream(s);\n      })\n      .catch((err) => {\n        console.warn('Could not access microphone for visualization:', err);\n      });\n\n    return () => {\n      if (stream) {\n        stream.getTracks().forEach((track) => {\n          track.stop();\n        });\n      }\n    };\n  }, [showWaveform]);\n\n  return (\n    <FormItem>\n      <FormControl>\n        <div className=\"space-y-4\">\n          {!isRecording && !file && (\n            <div className=\"relative flex flex-col items-center justify-center gap-4 p-4 border-2 border-dashed rounded-lg min-h-[180px] overflow-hidden\">\n              {showWaveform && audioStream && (\n                <MemoizedWaveform audioStream={audioStream} />\n              )}\n              <Button\n                type=\"button\"\n                onClick={onStart}\n                size=\"lg\"\n                className=\"relative z-10 flex items-center gap-2\"\n              >\n                <Mic className=\"h-5 w-5\" />\n                Start Recording\n              </Button>\n              <p className=\"relative z-10 text-sm text-muted-foreground text-center\">\n                Click to start recording. Maximum duration: 30 seconds.\n              </p>\n            </div>\n          )}\n\n          {isRecording && (\n            <div className=\"relative flex flex-col items-center justify-center gap-4 p-4 border-2 border-accent rounded-lg bg-accent/5 min-h-[180px] overflow-hidden\">\n              {showWaveform && audioStream && (\n                <MemoizedWaveform audioStream={audioStream} />\n              )}\n              <div className=\"relative z-10 flex items-center gap-4\">\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"h-3 w-3 rounded-full bg-accent animate-pulse\" />\n                  <span className=\"text-lg font-mono font-semibold\">\n                    {formatAudioDuration(duration)}\n                  </span>\n                </div>\n              </div>\n              <Button\n                type=\"button\"\n                onClick={onStop}\n                className=\"relative z-10 flex items-center gap-2 bg-accent text-accent-foreground hover:bg-accent/90\"\n              >\n                <Square className=\"h-4 w-4\" />\n                Stop Recording\n              </Button>\n              <p className=\"relative z-10 text-sm text-muted-foreground text-center\">\n                {formatAudioDuration(30 - duration)} remaining\n              </p>\n            </div>\n          )}\n\n          {file && !isRecording && (\n            <div className=\"flex flex-col items-center justify-center gap-4 p-4 border-2 border-primary rounded-lg bg-primary/5 min-h-[180px]\">\n              <div className=\"flex items-center gap-2\">\n                <Mic className=\"h-5 w-5 text-primary\" />\n                <span className=\"font-medium\">Recording complete</span>\n              </div>\n              <p className=\"text-sm text-muted-foreground text-center\">File: {file.name}</p>\n              <div className=\"flex gap-2\">\n                <Button\n                  type=\"button\"\n                  size=\"icon\"\n                  variant=\"outline\"\n                  onClick={onPlayPause}\n                  aria-label={isPlaying ? 'Pause' : 'Play'}\n                >\n                  {isPlaying ? <Pause className=\"h-4 w-4\" /> : <Play className=\"h-4 w-4\" />}\n                </Button>\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  onClick={onTranscribe}\n                  disabled={isTranscribing}\n                  className=\"flex items-center gap-2\"\n                >\n                  <Mic className=\"h-4 w-4\" />\n                  {isTranscribing ? 'Transcribing...' : 'Transcribe'}\n                </Button>\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  onClick={onCancel}\n                  className=\"flex items-center gap-2\"\n                >\n                  Record Again\n                </Button>\n              </div>\n            </div>\n          )}\n        </div>\n      </FormControl>\n      <FormMessage />\n    </FormItem>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoiceProfiles/AudioSampleSystem.tsx",
    "content": "import { Mic, Monitor, Pause, Play, Square } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { FormControl, FormItem, FormMessage } from '@/components/ui/form';\nimport { formatAudioDuration } from '@/lib/utils/audio';\n\ninterface AudioSampleSystemProps {\n  file: File | null | undefined;\n  isRecording: boolean;\n  duration: number;\n  onStart: () => void;\n  onStop: () => void;\n  onCancel: () => void;\n  onTranscribe: () => void;\n  onPlayPause: () => void;\n  isPlaying: boolean;\n  isTranscribing?: boolean;\n}\n\nexport function AudioSampleSystem({\n  file,\n  isRecording,\n  duration,\n  onStart,\n  onStop,\n  onCancel,\n  onTranscribe,\n  onPlayPause,\n  isPlaying,\n  isTranscribing = false,\n}: AudioSampleSystemProps) {\n  return (\n    <FormItem>\n      <FormControl>\n        <div className=\"space-y-4\">\n          {!isRecording && !file && (\n            <div className=\"flex flex-col items-center justify-center gap-4 p-4 border-2 border-dashed rounded-lg min-h-[180px]\">\n              <Button type=\"button\" onClick={onStart} size=\"lg\" className=\"flex items-center gap-2\">\n                <Monitor className=\"h-5 w-5\" />\n                Start Capture\n              </Button>\n              <p className=\"text-sm text-muted-foreground text-center\">\n                Capture audio from your system. Maximum duration: 30 seconds.\n              </p>\n            </div>\n          )}\n\n          {isRecording && (\n            <div className=\"flex flex-col items-center justify-center gap-4 p-4 border-2 border-destructive rounded-lg bg-destructive/5 min-h-[180px]\">\n              <div className=\"flex items-center gap-4\">\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"h-3 w-3 rounded-full bg-destructive animate-pulse\" />\n                  <span className=\"text-lg font-mono font-semibold\">\n                    {formatAudioDuration(duration)}\n                  </span>\n                </div>\n              </div>\n              <Button\n                type=\"button\"\n                onClick={onStop}\n                variant=\"destructive\"\n                className=\"flex items-center gap-2\"\n              >\n                <Square className=\"h-4 w-4\" />\n                Stop Capture\n              </Button>\n              <p className=\"text-sm text-muted-foreground text-center\">\n                {formatAudioDuration(30 - duration)} remaining\n              </p>\n            </div>\n          )}\n\n          {file && !isRecording && (\n            <div className=\"flex flex-col items-center justify-center gap-4 p-4 border-2 border-primary rounded-lg bg-primary/5 min-h-[180px]\">\n              <div className=\"flex items-center gap-2\">\n                <Monitor className=\"h-5 w-5 text-primary\" />\n                <span className=\"font-medium\">Capture complete</span>\n              </div>\n              <p className=\"text-sm text-muted-foreground text-center\">File: {file.name}</p>\n              <div className=\"flex gap-2\">\n                <Button\n                  type=\"button\"\n                  size=\"icon\"\n                  variant=\"outline\"\n                  onClick={onPlayPause}\n                  aria-label={isPlaying ? 'Pause' : 'Play'}\n                >\n                  {isPlaying ? <Pause className=\"h-4 w-4\" /> : <Play className=\"h-4 w-4\" />}\n                </Button>\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  onClick={onTranscribe}\n                  disabled={isTranscribing}\n                  className=\"flex items-center gap-2\"\n                >\n                  <Mic className=\"h-4 w-4\" />\n                  {isTranscribing ? 'Transcribing...' : 'Transcribe'}\n                </Button>\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  onClick={onCancel}\n                  className=\"flex items-center gap-2\"\n                >\n                  Capture Again\n                </Button>\n              </div>\n            </div>\n          )}\n        </div>\n      </FormControl>\n      <FormMessage />\n    </FormItem>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoiceProfiles/AudioSampleUpload.tsx",
    "content": "import { Mic, Pause, Play, Upload } from 'lucide-react';\nimport { useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { FormControl, FormItem, FormMessage } from '@/components/ui/form';\n\ninterface AudioSampleUploadProps {\n  file: File | null | undefined;\n  onFileChange: (file: File | undefined) => void;\n  onTranscribe: () => void;\n  onPlayPause: () => void;\n  isPlaying: boolean;\n  isValidating?: boolean;\n  isTranscribing?: boolean;\n  isDisabled?: boolean;\n  fieldName: string;\n}\n\nexport function AudioSampleUpload({\n  file,\n  onFileChange,\n  onTranscribe,\n  onPlayPause,\n  isPlaying,\n  isValidating = false,\n  isTranscribing = false,\n  isDisabled = false,\n  fieldName,\n}: AudioSampleUploadProps) {\n  const [isDragging, setIsDragging] = useState(false);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n\n  return (\n    <FormItem>\n      <FormControl>\n        <div className=\"flex flex-col gap-2\">\n          <input\n            type=\"file\"\n            accept=\"audio/*\"\n            name={fieldName}\n            ref={fileInputRef}\n            onChange={(e) => {\n              const selectedFile = e.target.files?.[0];\n              if (selectedFile) {\n                onFileChange(selectedFile);\n              } else {\n                onFileChange(undefined);\n              }\n            }}\n            className=\"hidden\"\n          />\n          <div\n            role=\"button\"\n            tabIndex={0}\n            onDragOver={(e) => {\n              e.preventDefault();\n              setIsDragging(true);\n            }}\n            onDragLeave={(e) => {\n              e.preventDefault();\n              setIsDragging(false);\n            }}\n            onDrop={(e) => {\n              e.preventDefault();\n              setIsDragging(false);\n              const droppedFile = e.dataTransfer.files?.[0];\n              if (droppedFile?.type.startsWith('audio/')) {\n                onFileChange(droppedFile);\n              }\n            }}\n            onKeyDown={(e) => {\n              if (e.key === 'Enter' || e.key === ' ') {\n                e.preventDefault();\n                fileInputRef.current?.click();\n              }\n            }}\n            className={`flex flex-col items-center justify-center gap-4 p-4 border-2 rounded-lg transition-colors min-h-[180px] ${\n              file\n                ? 'border-primary bg-primary/5'\n                : isDragging\n                  ? 'border-primary bg-primary/5'\n                  : 'border-dashed border-muted-foreground/25 hover:border-muted-foreground/50'\n            }`}\n          >\n            {!file ? (\n              <>\n                <Button\n                  type=\"button\"\n                  size=\"lg\"\n                  onClick={() => fileInputRef.current?.click()}\n                  className=\"flex items-center gap-2\"\n                >\n                  <Upload className=\"h-5 w-5\" />\n                  Choose File\n                </Button>\n                <p className=\"text-sm text-muted-foreground text-center\">\n                  Click to choose a file or drag and drop. Maximum duration: 30 seconds.\n                </p>\n              </>\n            ) : (\n              <>\n                <div className=\"flex items-center gap-2\">\n                  <Upload className=\"h-5 w-5 text-primary\" />\n                  <span className=\"font-medium\">File uploaded</span>\n                </div>\n                <p className=\"text-sm text-muted-foreground text-center\">File: {file.name}</p>\n                <div className=\"flex gap-2\">\n                  <Button\n                    type=\"button\"\n                    size=\"icon\"\n                    variant=\"outline\"\n                    onClick={onPlayPause}\n                    disabled={isValidating}\n                    aria-label={isPlaying ? 'Pause' : 'Play'}\n                  >\n                    {isPlaying ? <Pause className=\"h-4 w-4\" /> : <Play className=\"h-4 w-4\" />}\n                  </Button>\n                  <Button\n                    type=\"button\"\n                    variant=\"outline\"\n                    onClick={onTranscribe}\n                    disabled={isTranscribing || isValidating || isDisabled}\n                    className=\"flex items-center gap-2\"\n                  >\n                    <Mic className=\"h-4 w-4\" />\n                    {isTranscribing ? 'Transcribing...' : 'Transcribe'}\n                  </Button>\n                  <Button\n                    type=\"button\"\n                    variant=\"outline\"\n                    onClick={() => {\n                      onFileChange(undefined);\n                      if (fileInputRef.current) {\n                        fileInputRef.current.value = '';\n                      }\n                    }}\n                  >\n                    Remove\n                  </Button>\n                </div>\n              </>\n            )}\n          </div>\n        </div>\n      </FormControl>\n      <FormMessage />\n    </FormItem>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoiceProfiles/ProfileCard.tsx",
    "content": "import { Download, Edit, Sparkles, Trash2 } from 'lucide-react';\nimport { useState } from 'react';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { CircleButton } from '@/components/ui/circle-button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport type { VoiceProfileResponse } from '@/lib/api/types';\nimport { useDeleteProfile, useExportProfile } from '@/lib/hooks/useProfiles';\nimport { cn } from '@/lib/utils/cn';\nimport { useUIStore } from '@/stores/uiStore';\n\ninterface ProfileCardProps {\n  profile: VoiceProfileResponse;\n}\n\nexport function ProfileCard({ profile }: ProfileCardProps) {\n  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);\n\n  const deleteProfile = useDeleteProfile();\n  const exportProfile = useExportProfile();\n  const setEditingProfileId = useUIStore((state) => state.setEditingProfileId);\n  const setProfileDialogOpen = useUIStore((state) => state.setProfileDialogOpen);\n  const selectedProfileId = useUIStore((state) => state.selectedProfileId);\n  const setSelectedProfileId = useUIStore((state) => state.setSelectedProfileId);\n\n  const isSelected = selectedProfileId === profile.id;\n\n  const handleSelect = () => {\n    setSelectedProfileId(isSelected ? null : profile.id);\n  };\n\n  const handleEdit = () => {\n    setEditingProfileId(profile.id);\n    setProfileDialogOpen(true);\n  };\n\n  const handleDeleteClick = (e: React.MouseEvent) => {\n    e.stopPropagation();\n    setDeleteDialogOpen(true);\n  };\n\n  const handleDeleteConfirm = () => {\n    deleteProfile.mutate(profile.id);\n    setDeleteDialogOpen(false);\n  };\n\n  const handleExport = (e: React.MouseEvent) => {\n    e.stopPropagation();\n    exportProfile.mutate(profile.id);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    const target = e.target as HTMLElement;\n    if (target.closest('button')) return;\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault();\n      handleSelect();\n    }\n  };\n\n  const selectLabel = isSelected\n    ? `${profile.name}, ${profile.language}. Selected as voice for generation.`\n    : `${profile.name}, ${profile.language}. Select as voice for generation.`;\n\n  return (\n    <>\n      <Card\n        className={cn(\n          'cursor-pointer hover:shadow-md transition-all flex flex-col h-[162px]',\n          isSelected && 'ring-2 ring-accent shadow-md',\n        )}\n        onClick={handleSelect}\n        tabIndex={0}\n        role=\"button\"\n        aria-label={selectLabel}\n        aria-pressed={isSelected}\n        onKeyDown={handleKeyDown}\n      >\n        <CardHeader className=\"p-3 pb-2\">\n          <CardTitle className=\"text-base font-medium\">\n            <span className=\"break-words\">{profile.name}</span>\n          </CardTitle>\n        </CardHeader>\n        <CardContent className=\"p-3 pt-0 flex flex-col flex-1\">\n          <p className=\"text-xs text-muted-foreground mb-1.5 line-clamp-2 leading-relaxed\">\n            {profile.description || 'No description'}\n          </p>\n          <div className=\"mb-2 flex items-center gap-1.5\">\n            <Badge variant=\"outline\" className=\"text-xs h-5 px-1.5 text-muted-foreground\">\n              {profile.language}\n            </Badge>\n            {profile.voice_type === 'preset' && (\n              <Badge variant=\"secondary\" className=\"text-xs h-5 px-1.5\">\n                {profile.preset_engine}\n              </Badge>\n            )}\n            {profile.voice_type === 'designed' && (\n              <Badge variant=\"secondary\" className=\"text-xs h-5 px-1.5\">\n                designed\n              </Badge>\n            )}\n            {profile.effects_chain && profile.effects_chain.length > 0 && (\n              <Sparkles className=\"h-3.5 w-3.5 text-accent fill-accent\" />\n            )}\n          </div>\n          <div className=\"flex gap-0.5 justify-end items-end mt-auto\">\n            <CircleButton\n              icon={Download}\n              onClick={handleExport}\n              disabled={exportProfile.isPending}\n              aria-label=\"Export profile\"\n            />\n            <CircleButton\n              icon={Edit}\n              onClick={(e) => {\n                e.stopPropagation();\n                handleEdit();\n              }}\n              aria-label=\"Edit profile\"\n            />\n            <CircleButton\n              icon={Trash2}\n              onClick={handleDeleteClick}\n              disabled={deleteProfile.isPending}\n              aria-label=\"Delete profile\"\n            />\n          </div>\n        </CardContent>\n      </Card>\n\n      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Delete Profile</DialogTitle>\n            <DialogDescription>\n              Are you sure you want to delete \"{profile.name}\"? This action cannot be undone.\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button variant=\"outline\" onClick={() => setDeleteDialogOpen(false)}>\n              Cancel\n            </Button>\n            <Button\n              variant=\"destructive\"\n              onClick={handleDeleteConfirm}\n              disabled={deleteProfile.isPending}\n            >\n              {deleteProfile.isPending ? 'Deleting...' : 'Delete'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoiceProfiles/ProfileForm.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod';\nimport { useQuery } from '@tanstack/react-query';\nimport { Edit2, Mic, Monitor, Music, Upload, X } from 'lucide-react';\nimport { useEffect, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport * as z from 'zod';\nimport { EffectsChainEditor } from '@/components/Effects/EffectsChainEditor';\nimport { Badge } from '@/components/ui/badge';\nimport { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from '@/components/ui/form';\nimport { Input } from '@/components/ui/input';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport { Textarea } from '@/components/ui/textarea';\nimport { useToast } from '@/components/ui/use-toast';\nimport { apiClient } from '@/lib/api/client';\nimport type { EffectConfig, PresetVoice, VoiceType } from '@/lib/api/types';\nimport { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';\nimport { useAudioPlayer } from '@/lib/hooks/useAudioPlayer';\nimport { useAudioRecording } from '@/lib/hooks/useAudioRecording';\nimport {\n  useAddSample,\n  useCreateProfile,\n  useDeleteAvatar,\n  useDeleteProfile,\n  useProfile,\n  useUpdateProfile,\n  useUploadAvatar,\n} from '@/lib/hooks/useProfiles';\nimport { useSystemAudioCapture } from '@/lib/hooks/useSystemAudioCapture';\nimport { useTranscription } from '@/lib/hooks/useTranscription';\nimport { convertToWav, formatAudioDuration, getAudioDuration } from '@/lib/utils/audio';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { useServerStore } from '@/stores/serverStore';\nimport { type ProfileFormDraft, useUIStore } from '@/stores/uiStore';\nimport { AudioSampleRecording } from './AudioSampleRecording';\nimport { AudioSampleSystem } from './AudioSampleSystem';\nimport { AudioSampleUpload } from './AudioSampleUpload';\nimport { SampleList } from './SampleList';\n\nconst MAX_AUDIO_DURATION_SECONDS = 30;\nconst PRESET_ONLY_ENGINES = new Set(['kokoro']);\nconst DEFAULT_ENGINE_OPTIONS = [\n  { value: 'qwen', label: 'Qwen3-TTS' },\n  { value: 'luxtts', label: 'LuxTTS' },\n  { value: 'chatterbox', label: 'Chatterbox' },\n  { value: 'chatterbox_turbo', label: 'Chatterbox Turbo' },\n  { value: 'tada', label: 'TADA' },\n  { value: 'kokoro', label: 'Kokoro 82M' },\n] as const;\n\nconst baseProfileSchema = z.object({\n  name: z.string().min(1, 'Name is required').max(100),\n  description: z.string().max(500).optional(),\n  language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),\n  sampleFile: z.instanceof(File).optional(),\n  referenceText: z.string().max(1000).optional(),\n  avatarFile: z.instanceof(File).optional(),\n});\n\nconst profileSchema = baseProfileSchema.refine(\n  (data) => {\n    // If sample file is provided, reference text is required\n    if (data.sampleFile && (!data.referenceText || data.referenceText.trim().length === 0)) {\n      return false;\n    }\n    return true;\n  },\n  {\n    message: 'Reference text is required when adding a sample',\n    path: ['referenceText'],\n  },\n);\n\ntype ProfileFormValues = z.infer<typeof profileSchema>;\n\n// Helper to convert File to base64\nasync function fileToBase64(file: File): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => resolve(reader.result as string);\n    reader.onerror = reject;\n    reader.readAsDataURL(file);\n  });\n}\n\n// Helper to convert base64 to File\nfunction base64ToFile(base64: string, fileName: string, fileType: string): File {\n  const arr = base64.split(',');\n  const bstr = atob(arr[1]);\n  let n = bstr.length;\n  const u8arr = new Uint8Array(n);\n  while (n--) {\n    u8arr[n] = bstr.charCodeAt(n);\n  }\n  return new File([u8arr], fileName, { type: fileType });\n}\n\nexport function ProfileForm() {\n  const platform = usePlatform();\n  const open = useUIStore((state) => state.profileDialogOpen);\n  const setOpen = useUIStore((state) => state.setProfileDialogOpen);\n  const editingProfileId = useUIStore((state) => state.editingProfileId);\n  const setEditingProfileId = useUIStore((state) => state.setEditingProfileId);\n  const profileFormDraft = useUIStore((state) => state.profileFormDraft);\n  const setProfileFormDraft = useUIStore((state) => state.setProfileFormDraft);\n  const { data: editingProfile } = useProfile(editingProfileId || '');\n  const createProfile = useCreateProfile();\n  const updateProfile = useUpdateProfile();\n  const addSample = useAddSample();\n  const deleteProfile = useDeleteProfile();\n  const uploadAvatar = useUploadAvatar();\n  const deleteAvatar = useDeleteAvatar();\n  const transcribe = useTranscription();\n  const { toast } = useToast();\n  const [voiceSource, setVoiceSource] = useState<'clone' | 'builtin'>('clone');\n  const [sampleMode, setSampleMode] = useState<'upload' | 'record' | 'system'>('record');\n  const [audioDuration, setAudioDuration] = useState<number | null>(null);\n  const [isValidatingAudio, setIsValidatingAudio] = useState(false);\n  const [avatarPreview, setAvatarPreview] = useState<string | null>(null);\n  const [selectedPresetEngine, setSelectedPresetEngine] = useState<string>('kokoro');\n  const [selectedPresetVoiceId, setSelectedPresetVoiceId] = useState<string>('');\n  const avatarInputRef = useRef<HTMLInputElement>(null);\n  const { isPlaying, playPause, cleanup: cleanupAudio } = useAudioPlayer();\n  const isCreating = !editingProfileId;\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const [profileEffectsChain, setProfileEffectsChain] = useState<EffectConfig[]>([]);\n  const [effectsDirty, setEffectsDirty] = useState(false);\n  const [defaultEngine, setDefaultEngine] = useState<string>('');\n\n  const form = useForm<ProfileFormValues>({\n    resolver: zodResolver(profileSchema),\n    defaultValues: {\n      name: '',\n      description: '',\n      language: 'en',\n      sampleFile: undefined,\n      referenceText: '',\n      avatarFile: undefined,\n    },\n  });\n\n  const selectedFile = form.watch('sampleFile');\n  const selectedAvatarFile = form.watch('avatarFile');\n\n  // Validate audio duration when file is selected\n  useEffect(() => {\n    if (selectedFile && selectedFile instanceof File) {\n      setIsValidatingAudio(true);\n      getAudioDuration(selectedFile as File & { recordedDuration?: number })\n        .then((duration) => {\n          setAudioDuration(duration);\n          if (duration > MAX_AUDIO_DURATION_SECONDS) {\n            form.setError('sampleFile', {\n              type: 'manual',\n              message: `Audio is too long (${formatAudioDuration(duration)}). Maximum duration is ${formatAudioDuration(MAX_AUDIO_DURATION_SECONDS)}.`,\n            });\n          } else {\n            form.clearErrors('sampleFile');\n          }\n        })\n        .catch((error) => {\n          console.error('Failed to get audio duration:', error);\n          setAudioDuration(null);\n          // For recordings, we auto-stop at max duration, so we can skip validation errors\n          const isRecordedFile =\n            selectedFile.name.startsWith('recording-') ||\n            selectedFile.name.startsWith('system-audio-');\n          if (!isRecordedFile) {\n            form.setError('sampleFile', {\n              type: 'manual',\n              message: 'Failed to validate audio file. Please try a different file.',\n            });\n          } else {\n            // Clear any existing errors for recorded files\n            form.clearErrors('sampleFile');\n          }\n        })\n        .finally(() => {\n          setIsValidatingAudio(false);\n        });\n    } else {\n      setAudioDuration(null);\n      form.clearErrors('sampleFile');\n    }\n  }, [selectedFile, form]);\n\n  const {\n    isRecording,\n    duration,\n    error: recordingError,\n    startRecording,\n    stopRecording,\n    cancelRecording,\n  } = useAudioRecording({\n    maxDurationSeconds: 29,\n    onRecordingComplete: (blob, recordedDuration) => {\n      const file = new File([blob], `recording-${Date.now()}.webm`, {\n        type: blob.type || 'audio/webm',\n      }) as File & { recordedDuration?: number };\n      // Store the actual recorded duration to bypass metadata reading issues on Windows\n      if (recordedDuration !== undefined) {\n        file.recordedDuration = recordedDuration;\n      }\n      form.setValue('sampleFile', file, { shouldValidate: true });\n      toast({\n        title: 'Recording complete',\n        description: 'Audio has been recorded successfully.',\n      });\n    },\n  });\n\n  const {\n    isRecording: isSystemRecording,\n    duration: systemDuration,\n    error: systemRecordingError,\n    isSupported: isSystemAudioSupported,\n    startRecording: startSystemRecording,\n    stopRecording: stopSystemRecording,\n    cancelRecording: cancelSystemRecording,\n  } = useSystemAudioCapture({\n    maxDurationSeconds: 29,\n    onRecordingComplete: (blob, recordedDuration) => {\n      const file = new File([blob], `system-audio-${Date.now()}.wav`, {\n        type: blob.type || 'audio/wav',\n      }) as File & { recordedDuration?: number };\n      // Store the actual recorded duration to bypass metadata reading issues on Windows\n      if (recordedDuration !== undefined) {\n        file.recordedDuration = recordedDuration;\n      }\n      form.setValue('sampleFile', file, { shouldValidate: true });\n      toast({\n        title: 'System audio captured',\n        description: 'Audio has been captured successfully.',\n      });\n    },\n  });\n\n  // Fetch available preset voices for the selected engine\n  const presetEngineToQuery = isCreating\n    ? selectedPresetEngine\n    : (editingProfile?.preset_engine ?? '');\n  const { data: presetVoicesData } = useQuery({\n    queryKey: ['presetVoices', presetEngineToQuery],\n    queryFn: () => apiClient.listPresetVoices(presetEngineToQuery),\n    enabled:\n      !!presetEngineToQuery &&\n      ((voiceSource === 'builtin' && isCreating) ||\n        (!isCreating && editingProfile?.voice_type === 'preset')),\n  });\n  const presetVoices = presetVoicesData?.voices ?? [];\n  const isSampleBasedProfile = isCreating\n    ? voiceSource === 'clone'\n    : editingProfile?.voice_type !== 'preset';\n  const availableDefaultEngines = DEFAULT_ENGINE_OPTIONS.filter(\n    (option) => !isSampleBasedProfile || !PRESET_ONLY_ENGINES.has(option.value),\n  );\n\n  // Show recording errors\n  useEffect(() => {\n    if (recordingError) {\n      toast({\n        title: 'Recording error',\n        description: recordingError,\n        variant: 'destructive',\n      });\n    }\n  }, [recordingError, toast]);\n\n  // Show system audio recording errors\n  useEffect(() => {\n    if (systemRecordingError) {\n      toast({\n        title: 'System audio capture error',\n        description: systemRecordingError,\n        variant: 'destructive',\n      });\n    }\n  }, [systemRecordingError, toast]);\n\n  // Handle avatar preview\n  useEffect(() => {\n    if (selectedAvatarFile instanceof File) {\n      const url = URL.createObjectURL(selectedAvatarFile);\n      setAvatarPreview(url);\n      return () => URL.revokeObjectURL(url);\n    } else if (editingProfile?.avatar_path) {\n      setAvatarPreview(`${serverUrl}/profiles/${editingProfile.id}/avatar`);\n    } else {\n      setAvatarPreview(null);\n    }\n  }, [selectedAvatarFile, editingProfile, serverUrl]);\n\n  // Restore form state from draft or editing profile\n  useEffect(() => {\n    if (editingProfile) {\n      form.reset({\n        name: editingProfile.name,\n        description: editingProfile.description || '',\n        language: editingProfile.language as LanguageCode,\n        sampleFile: undefined,\n        referenceText: undefined,\n        avatarFile: undefined,\n      });\n      setProfileEffectsChain(editingProfile.effects_chain ?? []);\n      setEffectsDirty(false);\n      setDefaultEngine(editingProfile.default_engine ?? '');\n    } else if (profileFormDraft && open) {\n      // Restore from draft when opening in create mode\n      form.reset({\n        name: profileFormDraft.name,\n        description: profileFormDraft.description,\n        language: profileFormDraft.language as LanguageCode,\n        referenceText: profileFormDraft.referenceText,\n        sampleFile: undefined,\n        avatarFile: undefined,\n      });\n      setSampleMode(profileFormDraft.sampleMode);\n      // Restore the file if we have it saved\n      if (\n        profileFormDraft.sampleFileData &&\n        profileFormDraft.sampleFileName &&\n        profileFormDraft.sampleFileType\n      ) {\n        const file = base64ToFile(\n          profileFormDraft.sampleFileData,\n          profileFormDraft.sampleFileName,\n          profileFormDraft.sampleFileType,\n        );\n        form.setValue('sampleFile', file);\n      }\n    } else if (!open) {\n      // Only reset to defaults when modal is closed and no draft\n      form.reset({\n        name: '',\n        description: '',\n        language: 'en',\n        sampleFile: undefined,\n        referenceText: undefined,\n        avatarFile: undefined,\n      });\n      setSampleMode('record');\n      setAvatarPreview(null);\n    }\n  }, [editingProfile, profileFormDraft, open, form]);\n\n  useEffect(() => {\n    if (\n      defaultEngine &&\n      !availableDefaultEngines.some((option) => option.value === defaultEngine)\n    ) {\n      setDefaultEngine('');\n    }\n  }, [availableDefaultEngines, defaultEngine]);\n\n  async function handleTranscribe() {\n    const file = form.getValues('sampleFile');\n    if (!file) {\n      toast({\n        title: 'No file selected',\n        description: 'Please select an audio file first.',\n        variant: 'destructive',\n      });\n      return;\n    }\n\n    try {\n      const language = form.getValues('language');\n      const result = await transcribe.mutateAsync({ file, language });\n\n      form.setValue('referenceText', result.text, { shouldValidate: true });\n    } catch (error) {\n      toast({\n        title: 'Transcription failed',\n        description: error instanceof Error ? error.message : 'Failed to transcribe audio',\n        variant: 'destructive',\n      });\n    }\n  }\n\n  function handleCancelRecording() {\n    if (sampleMode === 'record') {\n      cancelRecording();\n    } else if (sampleMode === 'system') {\n      cancelSystemRecording();\n    }\n    form.resetField('sampleFile');\n    cleanupAudio();\n  }\n\n  function handlePlayPause() {\n    const file = form.getValues('sampleFile');\n    playPause(file);\n  }\n\n  function handleAvatarFileChange(e: React.ChangeEvent<HTMLInputElement>) {\n    const file = e.target.files?.[0];\n    if (file) {\n      if (!file.type.startsWith('image/')) {\n        toast({\n          title: 'Invalid file type',\n          description: 'Please select an image file (PNG, JPG, or WebP)',\n          variant: 'destructive',\n        });\n        return;\n      }\n      if (file.size > 5 * 1024 * 1024) {\n        toast({\n          title: 'File too large',\n          description: 'Image must be less than 5MB',\n          variant: 'destructive',\n        });\n        return;\n      }\n      form.setValue('avatarFile', file);\n    }\n  }\n\n  async function handleRemoveAvatar() {\n    if (editingProfileId && editingProfile?.avatar_path) {\n      try {\n        await deleteAvatar.mutateAsync(editingProfileId);\n        toast({\n          title: 'Avatar removed',\n          description: 'Avatar image has been removed successfully.',\n        });\n      } catch (error) {\n        toast({\n          title: 'Failed to remove avatar',\n          description: error instanceof Error ? error.message : 'Unknown error',\n          variant: 'destructive',\n        });\n      }\n    }\n    form.setValue('avatarFile', undefined);\n    setAvatarPreview(null);\n    if (avatarInputRef.current) {\n      avatarInputRef.current.value = '';\n    }\n  }\n\n  async function onSubmit(data: ProfileFormValues) {\n    try {\n      if (editingProfileId) {\n        // Editing: update profile\n        await updateProfile.mutateAsync({\n          profileId: editingProfileId,\n          data: {\n            name: data.name,\n            description: data.description,\n            language: data.language,\n            default_engine: defaultEngine || undefined,\n          },\n        });\n\n        // Handle avatar upload/update if file changed\n        if (data.avatarFile) {\n          try {\n            await uploadAvatar.mutateAsync({\n              profileId: editingProfileId,\n              file: data.avatarFile,\n            });\n          } catch (avatarError) {\n            toast({\n              title: 'Avatar upload failed',\n              description:\n                avatarError instanceof Error ? avatarError.message : 'Failed to upload avatar',\n              variant: 'destructive',\n            });\n          }\n        }\n\n        // Save effects chain if changed\n        if (effectsDirty) {\n          try {\n            await apiClient.updateProfileEffects(\n              editingProfileId,\n              profileEffectsChain.length > 0 ? profileEffectsChain : null,\n            );\n          } catch (fxError) {\n            toast({\n              title: 'Effects update failed',\n              description:\n                fxError instanceof Error ? fxError.message : 'Failed to save effects chain',\n              variant: 'destructive',\n            });\n            return;\n          }\n        }\n\n        toast({\n          title: 'Voice updated',\n          description: `\"${data.name}\" has been updated successfully.`,\n        });\n      } else if (voiceSource === 'builtin') {\n        // Creating preset profile from built-in voice\n        if (!selectedPresetVoiceId) {\n          toast({\n            title: 'No voice selected',\n            description: 'Please select a built-in voice.',\n            variant: 'destructive',\n          });\n          return;\n        }\n\n        const profile = await createProfile.mutateAsync({\n          name: data.name,\n          description: data.description,\n          language: data.language,\n          voice_type: 'preset' as VoiceType,\n          preset_engine: selectedPresetEngine,\n          preset_voice_id: selectedPresetVoiceId,\n          default_engine: selectedPresetEngine,\n        });\n\n        // Handle avatar upload if provided\n        if (data.avatarFile) {\n          try {\n            await uploadAvatar.mutateAsync({\n              profileId: profile.id,\n              file: data.avatarFile,\n            });\n          } catch (avatarError) {\n            toast({\n              title: 'Avatar upload failed',\n              description:\n                avatarError instanceof Error ? avatarError.message : 'Failed to upload avatar',\n              variant: 'destructive',\n            });\n          }\n        }\n\n        toast({\n          title: 'Profile created',\n          description: `\"${data.name}\" has been created with a built-in voice.`,\n        });\n      } else {\n        // Creating cloned profile: require sample file and reference text\n        const sampleFile = form.getValues('sampleFile');\n        const referenceText = form.getValues('referenceText');\n\n        if (!sampleFile) {\n          form.setError('sampleFile', {\n            type: 'manual',\n            message: 'Audio sample is required',\n          });\n          toast({\n            title: 'Audio sample required',\n            description: 'Please provide an audio sample to create the voice profile.',\n            variant: 'destructive',\n          });\n          return;\n        }\n\n        if (!referenceText || referenceText.trim().length === 0) {\n          form.setError('referenceText', {\n            type: 'manual',\n            message: 'Reference text is required',\n          });\n          toast({\n            title: 'Reference text required',\n            description: 'Please provide the reference text for the audio sample.',\n            variant: 'destructive',\n          });\n          return;\n        }\n\n        // Validate audio duration before creating profile\n        try {\n          const duration = await getAudioDuration(sampleFile);\n          if (duration > MAX_AUDIO_DURATION_SECONDS) {\n            form.setError('sampleFile', {\n              type: 'manual',\n              message: `Audio is too long (${formatAudioDuration(duration)}). Maximum duration is ${formatAudioDuration(MAX_AUDIO_DURATION_SECONDS)}.`,\n            });\n            toast({\n              title: 'Invalid audio file',\n              description: `Audio duration is ${formatAudioDuration(duration)}, but maximum is ${formatAudioDuration(MAX_AUDIO_DURATION_SECONDS)}.`,\n              variant: 'destructive',\n            });\n            return; // Prevent form submission\n          }\n        } catch (error) {\n          form.setError('sampleFile', {\n            type: 'manual',\n            message: 'Failed to validate audio file. Please try a different file.',\n          });\n          toast({\n            title: 'Validation error',\n            description: error instanceof Error ? error.message : 'Failed to validate audio file',\n            variant: 'destructive',\n          });\n          return; // Prevent form submission\n        }\n\n        // Creating: create profile, then add sample\n        const profile = await createProfile.mutateAsync({\n          name: data.name,\n          description: data.description,\n          language: data.language,\n          default_engine: defaultEngine || undefined,\n        });\n\n        // Convert non-WAV uploads to WAV so the backend can always use soundfile.\n        // Recorded audio is already WAV (from useAudioRecording's convertToWav call).\n        let fileToUpload: File = sampleFile;\n        if (!sampleFile.type.includes('wav') && !sampleFile.name.toLowerCase().endsWith('.wav')) {\n          try {\n            const wavBlob = await convertToWav(sampleFile);\n            const wavName = sampleFile.name.replace(/\\.[^.]+$/, '.wav');\n            fileToUpload = new File([wavBlob], wavName, { type: 'audio/wav' });\n          } catch {\n            // If browser can't decode the format, send the original and let the backend try.\n          }\n        }\n\n        try {\n          await addSample.mutateAsync({\n            profileId: profile.id,\n            file: fileToUpload,\n            referenceText: referenceText,\n          });\n\n          // Handle avatar upload if provided\n          if (data.avatarFile) {\n            try {\n              await uploadAvatar.mutateAsync({\n                profileId: profile.id,\n                file: data.avatarFile,\n              });\n            } catch (avatarError) {\n              toast({\n                title: 'Avatar upload failed',\n                description:\n                  avatarError instanceof Error ? avatarError.message : 'Failed to upload avatar',\n                variant: 'destructive',\n              });\n            }\n          }\n\n          toast({\n            title: 'Profile created',\n            description: `\"${data.name}\" has been created with a sample.`,\n          });\n        } catch (sampleError) {\n          let rollbackSucceeded = false;\n          try {\n            await deleteProfile.mutateAsync(profile.id);\n            rollbackSucceeded = true;\n          } catch (rollbackError) {\n            toast({\n              title: 'Rollback failed',\n              description:\n                rollbackError instanceof Error\n                  ? rollbackError.message\n                  : 'Created profile could not be removed after sample upload failure.',\n              variant: 'destructive',\n            });\n          }\n\n          toast({\n            title: 'Failed to add sample',\n            description:\n              sampleError instanceof Error\n                ? `${sampleError.message}${rollbackSucceeded ? ' The profile was rolled back.' : ''}`\n                : rollbackSucceeded\n                  ? 'Failed to add sample. The profile was rolled back.'\n                  : 'Failed to add sample.',\n            variant: 'destructive',\n          });\n          return;\n        }\n      }\n\n      // Clear draft and reset form on success\n      setProfileFormDraft(null);\n      form.reset();\n      setEditingProfileId(null);\n      setOpen(false);\n    } catch (error) {\n      toast({\n        title: 'Error',\n        description: error instanceof Error ? error.message : 'Failed to save profile',\n        variant: 'destructive',\n      });\n    }\n  }\n\n  async function handleOpenChange(newOpen: boolean) {\n    if (!newOpen && isCreating) {\n      // Save draft when closing the create modal\n      const values = form.getValues();\n      const hasContent =\n        values.name || values.description || values.referenceText || values.sampleFile;\n\n      if (hasContent) {\n        const draft: ProfileFormDraft = {\n          name: values.name || '',\n          description: values.description || '',\n          language: values.language || 'en',\n          referenceText: values.referenceText || '',\n          sampleMode,\n        };\n\n        // Save file as base64 if present\n        if (values.sampleFile) {\n          try {\n            draft.sampleFileName = values.sampleFile.name;\n            draft.sampleFileType = values.sampleFile.type;\n            draft.sampleFileData = await fileToBase64(values.sampleFile);\n          } catch {\n            // If file conversion fails, just don't save the file\n          }\n        }\n\n        setProfileFormDraft(draft);\n      }\n    }\n\n    setOpen(newOpen);\n    if (!newOpen) {\n      setEditingProfileId(null);\n      // Don't reset form here - let the effect handle it based on draft state\n      if (isRecording) {\n        cancelRecording();\n      }\n      if (isSystemRecording) {\n        cancelSystemRecording();\n      }\n      cleanupAudio();\n    }\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={handleOpenChange}>\n      <DialogContent className=\"max-w-none w-screen h-screen left-0 top-0 translate-x-0 translate-y-0 rounded-none p-6 overflow-hidden\">\n        <div className=\"max-w-5xl h-[85vh] mx-auto my-auto w-full flex flex-col overflow-hidden\">\n          <DialogHeader>\n            <DialogTitle className=\"text-2xl\">\n              {editingProfileId ? 'Edit Voice' : 'Create Voice'}\n            </DialogTitle>\n            <DialogDescription>\n              {editingProfileId\n                ? 'Update your voice profile details and manage samples.'\n                : 'Create a new voice profile from an audio sample or a built-in voice.'}\n            </DialogDescription>\n            {isCreating && profileFormDraft && (\n              <div className=\"flex items-center gap-2 pt-2\">\n                <span className=\"text-xs text-muted-foreground\">Draft restored</span>\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  className=\"h-6 px-2 text-xs text-muted-foreground\"\n                  onClick={() => {\n                    setProfileFormDraft(null);\n                    form.reset({\n                      name: '',\n                      description: '',\n                      language: 'en',\n                      sampleFile: undefined,\n                      referenceText: '',\n                    });\n                    setSampleMode('record');\n                  }}\n                >\n                  <X className=\"h-3 w-3 mr-1\" />\n                  Discard\n                </Button>\n              </div>\n            )}\n          </DialogHeader>\n\n          <Form {...form}>\n            <form onSubmit={form.handleSubmit(onSubmit)} className=\"flex-1 min-h-0 flex flex-col\">\n              <div className=\"grid gap-6 grid-cols-2 flex-1 min-h-0 overflow-hidden\">\n                {/* Left column: Sample management */}\n                <div className=\"space-y-4 border-r pr-6 overflow-y-auto min-h-0\">\n                  {isCreating ? (\n                    <>\n                      {/* Voice source selector */}\n                      <div className=\"flex pt-4 pb-2\">\n                        <div className=\"inline-flex rounded-lg border border-border p-0.5 bg-muted/50\">\n                          <button\n                            type=\"button\"\n                            onClick={() => setVoiceSource('clone')}\n                            className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm rounded-md transition-colors ${\n                              voiceSource === 'clone'\n                                ? 'bg-accent text-accent-foreground shadow-sm'\n                                : 'text-muted-foreground hover:text-foreground'\n                            }`}\n                          >\n                            <Mic className=\"h-3.5 w-3.5\" />\n                            Clone from audio\n                          </button>\n                          <button\n                            type=\"button\"\n                            onClick={() => setVoiceSource('builtin')}\n                            className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm rounded-md transition-colors ${\n                              voiceSource === 'builtin'\n                                ? 'bg-accent text-accent-foreground shadow-sm'\n                                : 'text-muted-foreground hover:text-foreground'\n                            }`}\n                          >\n                            <Music className=\"h-3.5 w-3.5\" />\n                            Built-in voice\n                          </button>\n                        </div>\n                      </div>\n\n                      {voiceSource === 'builtin' ? (\n                        <div className=\"space-y-4\">\n                          <FormDescription>\n                            Choose a pre-built voice. These don't require an audio sample.\n                          </FormDescription>\n\n                          {/* Engine selector */}\n                          <FormItem>\n                            <FormLabel>Engine</FormLabel>\n                            <Select\n                              value={selectedPresetEngine}\n                              onValueChange={setSelectedPresetEngine}\n                            >\n                              <FormControl>\n                                <SelectTrigger>\n                                  <SelectValue />\n                                </SelectTrigger>\n                              </FormControl>\n                              <SelectContent>\n                                <SelectItem value=\"kokoro\">Kokoro 82M</SelectItem>\n                              </SelectContent>\n                            </Select>\n                          </FormItem>\n\n                          {/* Voice picker */}\n                          <FormItem>\n                            <FormLabel>Voice</FormLabel>\n                            <div className=\"grid grid-cols-2 gap-1.5 max-h-[340px] overflow-y-auto pr-1\">\n                              {presetVoices.map((voice: PresetVoice) => (\n                                <button\n                                  key={voice.voice_id}\n                                  type=\"button\"\n                                  onClick={() => {\n                                    setSelectedPresetVoiceId(voice.voice_id);\n                                    // Auto-set language from voice\n                                    if (voice.language) {\n                                      form.setValue('language', voice.language as LanguageCode);\n                                    }\n                                  }}\n                                  className={`text-left px-3 py-2 rounded-md border text-sm transition-colors ${\n                                    selectedPresetVoiceId === voice.voice_id\n                                      ? 'border-accent bg-accent/10 text-accent-foreground'\n                                      : 'border-border hover:bg-muted'\n                                  }`}\n                                >\n                                  <div className=\"font-medium\">{voice.name}</div>\n                                  <div className=\"flex gap-1.5 mt-0.5\">\n                                    <Badge variant=\"outline\" className=\"text-[10px] h-4 px-1\">\n                                      {voice.gender}\n                                    </Badge>\n                                    <Badge variant=\"outline\" className=\"text-[10px] h-4 px-1\">\n                                      {voice.language}\n                                    </Badge>\n                                  </div>\n                                </button>\n                              ))}\n                            </div>\n                          </FormItem>\n                        </div>\n                      ) : (\n                        <>\n                          <Tabs\n                            className=\"pt-0\"\n                            value={sampleMode}\n                            onValueChange={(v) => {\n                              const newMode = v as 'upload' | 'record' | 'system';\n                              // Cancel any active recordings when switching modes\n                              if (isRecording && newMode !== 'record') {\n                                cancelRecording();\n                              }\n                              if (isSystemRecording && newMode !== 'system') {\n                                cancelSystemRecording();\n                              }\n                              setSampleMode(newMode);\n                            }}\n                          >\n                            <TabsList\n                              className={`grid w-full ${platform.metadata.isTauri && isSystemAudioSupported ? 'grid-cols-3' : 'grid-cols-2'}`}\n                            >\n                              <TabsTrigger value=\"upload\" className=\"flex items-center gap-2\">\n                                <Upload className=\"h-4 w-4 shrink-0\" />\n                                Upload\n                              </TabsTrigger>\n                              <TabsTrigger value=\"record\" className=\"flex items-center gap-2\">\n                                <Mic className=\"h-4 w-4 shrink-0\" />\n                                Record\n                              </TabsTrigger>\n                              {platform.metadata.isTauri && isSystemAudioSupported && (\n                                <TabsTrigger value=\"system\" className=\"flex items-center gap-2\">\n                                  <Monitor className=\"h-4 w-4 shrink-0\" />\n                                  System Audio\n                                </TabsTrigger>\n                              )}\n                            </TabsList>\n\n                            <TabsContent value=\"upload\" className=\"space-y-4\">\n                              <FormField\n                                control={form.control}\n                                name=\"sampleFile\"\n                                render={({ field: { onChange, name } }) => (\n                                  <AudioSampleUpload\n                                    file={selectedFile}\n                                    onFileChange={onChange}\n                                    onTranscribe={handleTranscribe}\n                                    onPlayPause={handlePlayPause}\n                                    isPlaying={isPlaying}\n                                    isValidating={isValidatingAudio}\n                                    isTranscribing={transcribe.isPending}\n                                    isDisabled={\n                                      audioDuration !== null &&\n                                      audioDuration > MAX_AUDIO_DURATION_SECONDS\n                                    }\n                                    fieldName={name}\n                                  />\n                                )}\n                              />\n                            </TabsContent>\n\n                            <TabsContent value=\"record\" className=\"space-y-4\">\n                              <FormField\n                                control={form.control}\n                                name=\"sampleFile\"\n                                render={() => (\n                                  <AudioSampleRecording\n                                    file={selectedFile}\n                                    isRecording={isRecording}\n                                    duration={duration}\n                                    onStart={startRecording}\n                                    onStop={stopRecording}\n                                    onCancel={handleCancelRecording}\n                                    onTranscribe={handleTranscribe}\n                                    onPlayPause={handlePlayPause}\n                                    isPlaying={isPlaying}\n                                    isTranscribing={transcribe.isPending}\n                                  />\n                                )}\n                              />\n                            </TabsContent>\n\n                            {platform.metadata.isTauri && isSystemAudioSupported && (\n                              <TabsContent value=\"system\" className=\"space-y-4\">\n                                <FormField\n                                  control={form.control}\n                                  name=\"sampleFile\"\n                                  render={() => (\n                                    <AudioSampleSystem\n                                      file={selectedFile}\n                                      isRecording={isSystemRecording}\n                                      duration={systemDuration}\n                                      onStart={startSystemRecording}\n                                      onStop={stopSystemRecording}\n                                      onCancel={handleCancelRecording}\n                                      onTranscribe={handleTranscribe}\n                                      onPlayPause={handlePlayPause}\n                                      isPlaying={isPlaying}\n                                      isTranscribing={transcribe.isPending}\n                                    />\n                                  )}\n                                />\n                              </TabsContent>\n                            )}\n                          </Tabs>\n\n                          <FormField\n                            control={form.control}\n                            name=\"referenceText\"\n                            render={({ field }) => (\n                              <FormItem>\n                                <FormLabel>Reference Text</FormLabel>\n                                <FormControl>\n                                  <Textarea\n                                    placeholder=\"Enter the exact text spoken in the audio...\"\n                                    className=\"min-h-[100px]\"\n                                    {...field}\n                                  />\n                                </FormControl>\n                                <FormMessage />\n                              </FormItem>\n                            )}\n                          />\n                        </>\n                      )}\n                    </>\n                  ) : (\n                    // Editing mode\n                    editingProfileId &&\n                    editingProfile &&\n                    (editingProfile.voice_type === 'preset' ? (\n                      <div className=\"space-y-4 pt-4\">\n                        <div className=\"rounded-lg border border-border p-4 space-y-3\">\n                          <div className=\"text-sm font-medium text-muted-foreground\">\n                            Built-in Voice\n                          </div>\n                          <div className=\"flex items-center gap-3\">\n                            <div className=\"text-lg font-semibold\">\n                              {presetVoices.find(\n                                (v: PresetVoice) => v.voice_id === editingProfile.preset_voice_id,\n                              )?.name ?? editingProfile.preset_voice_id}\n                            </div>\n                            <Badge variant=\"secondary\" className=\"text-xs\">\n                              {editingProfile.preset_engine}\n                            </Badge>\n                          </div>\n                          {(() => {\n                            const voice = presetVoices.find(\n                              (v: PresetVoice) => v.voice_id === editingProfile.preset_voice_id,\n                            );\n                            return voice ? (\n                              <div className=\"flex gap-1.5\">\n                                <Badge variant=\"outline\" className=\"text-xs\">\n                                  {voice.gender}\n                                </Badge>\n                                <Badge variant=\"outline\" className=\"text-xs\">\n                                  {voice.language}\n                                </Badge>\n                              </div>\n                            ) : null;\n                          })()}\n                        </div>\n                        <p className=\"text-xs text-muted-foreground\">\n                          This profile uses a built-in voice. The voice cannot be changed after\n                          creation.\n                        </p>\n                      </div>\n                    ) : (\n                      <div>\n                        <SampleList profileId={editingProfileId} />\n                      </div>\n                    ))\n                  )}\n                </div>\n\n                {/* Right column: Profile info */}\n                <div className=\"space-y-4 overflow-y-auto min-h-0\">\n                  {/* Avatar Upload */}\n                  <FormField\n                    control={form.control}\n                    name=\"avatarFile\"\n                    render={() => (\n                      <FormItem>\n                        <FormControl>\n                          <div className=\"flex justify-center pt-4 pb-2\">\n                            <div className=\"relative group\">\n                              <div className=\"h-24 w-24 rounded-full bg-muted flex items-center justify-center shrink-0 overflow-hidden border-2 border-border\">\n                                {avatarPreview ? (\n                                  <img\n                                    src={avatarPreview}\n                                    alt=\"Avatar preview\"\n                                    className=\"h-full w-full object-cover\"\n                                  />\n                                ) : (\n                                  <Mic className=\"h-10 w-10 text-muted-foreground\" />\n                                )}\n                              </div>\n                              <button\n                                type=\"button\"\n                                onClick={() => avatarInputRef.current?.click()}\n                                className=\"absolute inset-0 rounded-full bg-accent/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer\"\n                              >\n                                <Edit2 className=\"h-6 w-6 text-accent-foreground\" />\n                              </button>\n                              {(avatarPreview || editingProfile?.avatar_path) && (\n                                <button\n                                  type=\"button\"\n                                  onClick={handleRemoveAvatar}\n                                  disabled={deleteAvatar.isPending}\n                                  className=\"absolute bottom-0 right-0 h-6 w-6 rounded-full bg-background/60 backdrop-blur-sm text-muted-foreground flex items-center justify-center hover:bg-background/80 hover:text-foreground transition-colors shadow-sm border border-border/50\"\n                                >\n                                  <X className=\"h-3.5 w-3.5\" />\n                                </button>\n                              )}\n                            </div>\n                            <input\n                              ref={avatarInputRef}\n                              type=\"file\"\n                              accept=\"image/png,image/jpeg,image/webp\"\n                              onChange={handleAvatarFileChange}\n                              className=\"hidden\"\n                            />\n                          </div>\n                        </FormControl>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n\n                  <FormField\n                    control={form.control}\n                    name=\"name\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>Name</FormLabel>\n                        <FormControl>\n                          <Input placeholder=\"My Voice\" {...field} />\n                        </FormControl>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n\n                  <FormField\n                    control={form.control}\n                    name=\"description\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>Description (Optional)</FormLabel>\n                        <FormControl>\n                          <Textarea placeholder=\"Describe this voice...\" {...field} />\n                        </FormControl>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n\n                  <FormField\n                    control={form.control}\n                    name=\"language\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>Language</FormLabel>\n                        <Select onValueChange={field.onChange} defaultValue={field.value}>\n                          <FormControl>\n                            <SelectTrigger>\n                              <SelectValue />\n                            </SelectTrigger>\n                          </FormControl>\n                          <SelectContent>\n                            {LANGUAGE_OPTIONS.map((lang) => (\n                              <SelectItem key={lang.value} value={lang.value}>\n                                {lang.label}\n                              </SelectItem>\n                            ))}\n                          </SelectContent>\n                        </Select>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n\n                  <FormItem>\n                    <FormLabel>Default Engine</FormLabel>\n                    <Select\n                      value={defaultEngine || '_none'}\n                      onValueChange={(v) => {\n                        setDefaultEngine(v === '_none' ? '' : v);\n                      }}\n                      disabled={\n                        voiceSource === 'builtin' || editingProfile?.voice_type === 'preset'\n                      }\n                    >\n                      <FormControl>\n                        <SelectTrigger>\n                          <SelectValue placeholder=\"No preference\" />\n                        </SelectTrigger>\n                      </FormControl>\n                      <SelectContent>\n                        <SelectItem value=\"_none\">No preference</SelectItem>\n                        {availableDefaultEngines.map((option) => (\n                          <SelectItem key={option.value} value={option.value}>\n                            {option.label}\n                          </SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                    <p className=\"text-xs text-muted-foreground\">\n                      Auto-selects this engine when the profile is chosen.\n                    </p>\n                  </FormItem>\n\n                  {editingProfileId && (\n                    <div className=\"space-y-2\">\n                      <FormLabel>Default Effects</FormLabel>\n                      <p className=\"text-xs text-muted-foreground\">\n                        Effects applied automatically to all new generations with this voice.\n                      </p>\n                      <EffectsChainEditor\n                        value={profileEffectsChain}\n                        onChange={(chain) => {\n                          setProfileEffectsChain(chain);\n                          setEffectsDirty(true);\n                        }}\n                        compact\n                      />\n                    </div>\n                  )}\n                </div>\n              </div>\n\n              <div className=\"flex gap-2 justify-end mt-6 pt-4 border-t\">\n                <Button type=\"button\" variant=\"outline\" onClick={() => handleOpenChange(false)}>\n                  Cancel\n                </Button>\n                <Button\n                  type=\"submit\"\n                  disabled={\n                    createProfile.isPending || updateProfile.isPending || addSample.isPending\n                  }\n                >\n                  {createProfile.isPending || updateProfile.isPending || addSample.isPending\n                    ? 'Saving...'\n                    : editingProfileId\n                      ? 'Save Changes'\n                      : 'Create Profile'}\n                </Button>\n              </div>\n            </form>\n          </Form>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoiceProfiles/ProfileList.tsx",
    "content": "import { Mic, Music, Sparkles } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { useProfiles } from '@/lib/hooks/useProfiles';\nimport { useUIStore } from '@/stores/uiStore';\nimport { ProfileCard } from './ProfileCard';\nimport { ProfileForm } from './ProfileForm';\n\n/** Engines that use preset (built-in) voices instead of cloned profiles. */\nconst PRESET_ENGINES = new Set(['kokoro']);\n\n/** Human-readable engine names for empty state messages. */\nconst ENGINE_NAMES: Record<string, string> = {\n  kokoro: 'Kokoro',\n};\n\nexport function ProfileList() {\n  const { data: profiles, isLoading, error } = useProfiles();\n  const setDialogOpen = useUIStore((state) => state.setProfileDialogOpen);\n  const selectedEngine = useUIStore((state) => state.selectedEngine);\n\n  if (isLoading) {\n    return null;\n  }\n\n  if (error) {\n    return (\n      <div className=\"flex items-center justify-center p-8\">\n        <div className=\"text-destructive\">Error loading profiles: {error.message}</div>\n      </div>\n    );\n  }\n\n  const allProfiles = profiles || [];\n  const isPresetEngine = PRESET_ENGINES.has(selectedEngine);\n\n  // Filter profiles based on selected engine\n  const filteredProfiles = isPresetEngine\n    ? allProfiles.filter((p) => p.voice_type === 'preset' && p.preset_engine === selectedEngine)\n    : allProfiles.filter((p) => p.voice_type !== 'preset');\n\n  return (\n    <div className=\"flex flex-col\">\n      <div className=\"shrink-0\">\n        {allProfiles.length === 0 ? (\n          <Card>\n            <CardContent className=\"flex flex-col items-center justify-center py-12\">\n              <Mic className=\"h-12 w-12 text-muted-foreground mb-4\" />\n              <p className=\"text-muted-foreground mb-4\">\n                No voice profiles yet. Create your first profile to get started.\n              </p>\n              <Button onClick={() => setDialogOpen(true)}>\n                <Sparkles className=\"mr-2 h-4 w-4\" />\n                Create Voice\n              </Button>\n            </CardContent>\n          </Card>\n        ) : filteredProfiles.length === 0 && isPresetEngine ? (\n          <Card>\n            <CardContent className=\"flex flex-col items-center justify-center py-12\">\n              <Music className=\"h-12 w-12 text-muted-foreground mb-4\" />\n              <p className=\"text-muted-foreground mb-2\">\n                No {ENGINE_NAMES[selectedEngine] ?? selectedEngine} voices created yet.\n              </p>\n              <p className=\"text-sm text-muted-foreground mb-4\">\n                Create a profile to choose a specific voice before generating.\n              </p>\n              <Button onClick={() => setDialogOpen(true)}>\n                <Sparkles className=\"mr-2 h-4 w-4\" />\n                Create {ENGINE_NAMES[selectedEngine] ?? selectedEngine} Voice\n              </Button>\n            </CardContent>\n          </Card>\n        ) : (\n          <div className=\"flex gap-4 overflow-x-auto p-1 pb-1 lg:grid lg:grid-cols-3 lg:auto-rows-auto lg:overflow-x-visible lg:pb-[150px]\">\n            {filteredProfiles.map((profile) => (\n              <div key={profile.id} className=\"shrink-0 w-[200px] lg:w-auto lg:shrink\">\n                <ProfileCard profile={profile} />\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n\n      <ProfileForm />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoiceProfiles/SampleList.tsx",
    "content": "import { Check, Edit, Pause, Play, Plus, Trash2, Volume2, X } from 'lucide-react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { CircleButton } from '@/components/ui/circle-button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport { Slider } from '@/components/ui/slider';\nimport { Textarea } from '@/components/ui/textarea';\nimport { useToast } from '@/components/ui/use-toast';\nimport { apiClient } from '@/lib/api/client';\nimport { useDeleteSample, useProfileSamples, useUpdateSample } from '@/lib/hooks/useProfiles';\nimport { formatAudioDuration } from '@/lib/utils/audio';\nimport { cn } from '@/lib/utils/cn';\nimport { SampleUpload } from './SampleUpload';\n\ninterface MiniSamplePlayerProps {\n  audioUrl: string;\n}\n\nfunction MiniSamplePlayer({ audioUrl }: MiniSamplePlayerProps) {\n  const audioRef = useRef<HTMLAudioElement | null>(null);\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [currentTime, setCurrentTime] = useState(0);\n  const [duration, setDuration] = useState(0);\n  const [isLoading, setIsLoading] = useState(true);\n\n  useEffect(() => {\n    const audio = new Audio(audioUrl);\n    audioRef.current = audio;\n\n    const handleLoadedMetadata = () => {\n      setDuration(audio.duration);\n      setIsLoading(false);\n    };\n\n    const handleTimeUpdate = () => {\n      setCurrentTime(audio.currentTime);\n    };\n\n    const handleEnded = () => {\n      setIsPlaying(false);\n      setCurrentTime(0);\n    };\n\n    const handlePlay = () => setIsPlaying(true);\n    const handlePause = () => setIsPlaying(false);\n\n    audio.addEventListener('loadedmetadata', handleLoadedMetadata);\n    audio.addEventListener('timeupdate', handleTimeUpdate);\n    audio.addEventListener('ended', handleEnded);\n    audio.addEventListener('play', handlePlay);\n    audio.addEventListener('pause', handlePause);\n\n    return () => {\n      audio.pause();\n      audio.removeEventListener('loadedmetadata', handleLoadedMetadata);\n      audio.removeEventListener('timeupdate', handleTimeUpdate);\n      audio.removeEventListener('ended', handleEnded);\n      audio.removeEventListener('play', handlePlay);\n      audio.removeEventListener('pause', handlePause);\n      audio.src = '';\n    };\n  }, [audioUrl]);\n\n  const handlePlayPause = () => {\n    if (!audioRef.current) return;\n    if (isPlaying) {\n      audioRef.current.pause();\n    } else {\n      audioRef.current.play();\n    }\n  };\n\n  const handleSeek = (value: number[]) => {\n    if (!audioRef.current || duration === 0) return;\n    const progress = value[0] / 100;\n    audioRef.current.currentTime = progress * duration;\n  };\n\n  const handleStop = () => {\n    if (audioRef.current) {\n      audioRef.current.pause();\n      audioRef.current.currentTime = 0;\n    }\n    setIsPlaying(false);\n    setCurrentTime(0);\n  };\n\n  return (\n    <div className=\"border-t bg-muted/30 px-3 py-2 mt-2\">\n      <div className=\"flex items-center gap-2\">\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"h-7 w-7 shrink-0\"\n          onClick={handlePlayPause}\n          disabled={isLoading}\n          aria-label={isPlaying ? 'Pause sample' : 'Play sample'}\n        >\n          {isPlaying ? <Pause className=\"h-3.5 w-3.5\" /> : <Play className=\"h-3.5 w-3.5 ml-0.5\" />}\n        </Button>\n\n        <div className=\"flex-1 min-w-0 flex items-center gap-2\">\n          <Slider\n            value={duration > 0 ? [(currentTime / duration) * 100] : [0]}\n            onValueChange={handleSeek}\n            max={100}\n            step={0.1}\n            className=\"flex-1\"\n            aria-label=\"Sample playback position\"\n            aria-valuetext={`${formatAudioDuration(currentTime)} of ${formatAudioDuration(duration)}`}\n          />\n          <div className=\"flex items-center gap-1 text-xs text-muted-foreground shrink-0 min-w-[70px]\">\n            <span className=\"font-mono\">{formatAudioDuration(currentTime)}</span>\n            <span>/</span>\n            <span className=\"font-mono\">{formatAudioDuration(duration)}</span>\n          </div>\n        </div>\n\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"h-7 w-7 shrink-0\"\n          onClick={handleStop}\n          title=\"Stop\"\n          aria-label=\"Stop playback\"\n        >\n          <X className=\"h-3.5 w-3.5\" />\n        </Button>\n      </div>\n    </div>\n  );\n}\n\ninterface SampleListProps {\n  profileId: string;\n}\n\nexport function SampleList({ profileId }: SampleListProps) {\n  const { data: samples, isLoading } = useProfileSamples(profileId);\n  const deleteSample = useDeleteSample();\n  const updateSample = useUpdateSample();\n  const { toast } = useToast();\n  const [uploadOpen, setUploadOpen] = useState(false);\n  const [editingSampleId, setEditingSampleId] = useState<string | null>(null);\n  const [editedText, setEditedText] = useState<string>('');\n  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);\n  const [sampleToDelete, setSampleToDelete] = useState<string | null>(null);\n\n  const handleDeleteClick = (sampleId: string) => {\n    setSampleToDelete(sampleId);\n    setDeleteDialogOpen(true);\n  };\n\n  const handleDeleteConfirm = () => {\n    if (sampleToDelete) {\n      deleteSample.mutate(sampleToDelete);\n      setDeleteDialogOpen(false);\n      setSampleToDelete(null);\n    }\n  };\n\n  const handleStartEdit = (sampleId: string, currentText: string) => {\n    setEditingSampleId(sampleId);\n    setEditedText(currentText);\n  };\n\n  const handleCancelEdit = () => {\n    setEditingSampleId(null);\n    setEditedText('');\n  };\n\n  const handleSaveEdit = async (sampleId: string) => {\n    if (!editedText.trim()) {\n      toast({\n        title: 'Invalid text',\n        description: 'Reference text cannot be empty.',\n        variant: 'destructive',\n      });\n      return;\n    }\n\n    try {\n      await updateSample.mutateAsync({ sampleId, referenceText: editedText.trim() });\n      toast({\n        title: 'Sample updated',\n        description: 'Reference text has been updated successfully.',\n      });\n      setEditingSampleId(null);\n      setEditedText('');\n    } catch (error) {\n      toast({\n        title: 'Update failed',\n        description: error instanceof Error ? error.message : 'Failed to update sample',\n        variant: 'destructive',\n      });\n    }\n  };\n\n  if (isLoading) {\n    return <div className=\"text-sm text-muted-foreground\">Loading samples...</div>;\n  }\n\n  return (\n    <div className=\"space-y-4 pt-4\">\n      {samples && samples.length === 0 ? (\n        <div className=\"flex flex-col items-center justify-center py-8 text-center border border-dashed rounded-lg\">\n          <Volume2 className=\"h-8 w-8 text-muted-foreground/50 mb-2\" />\n          <p className=\"text-sm text-muted-foreground\">No samples yet</p>\n          <p className=\"text-xs text-muted-foreground/70 mt-1\">\n            Add your first audio sample to get started\n          </p>\n        </div>\n      ) : (\n        <div className=\"space-y-2\">\n          {samples?.map((sample, index) => {\n            const isEditing = editingSampleId === sample.id;\n\n            return (\n              <div\n                key={sample.id}\n                className={cn(\n                  'group relative rounded-lg border bg-card transition-all duration-200',\n                  isEditing ? 'ring-2 ring-primary/20' : 'hover:border-primary/30',\n                )}\n              >\n                {isEditing ? (\n                  /* Edit Mode */\n                  <div className=\"p-4 space-y-3\">\n                    <div className=\"flex items-center gap-2 text-xs text-muted-foreground mb-2\">\n                      <Edit className=\"h-3 w-3\" />\n                      <span>Editing transcription</span>\n                    </div>\n                    <Textarea\n                      value={editedText}\n                      onChange={(e) => setEditedText(e.target.value)}\n                      className=\"min-h-[100px] text-sm resize-none\"\n                      placeholder=\"Enter reference text...\"\n                      autoFocus\n                    />\n                    <div className=\"flex items-center justify-end gap-2 pt-1\">\n                      <Button\n                        type=\"button\"\n                        size=\"sm\"\n                        variant=\"ghost\"\n                        onClick={handleCancelEdit}\n                        disabled={updateSample.isPending}\n                      >\n                        <X className=\"h-4 w-4 mr-1\" />\n                        Cancel\n                      </Button>\n                      <Button\n                        type=\"button\"\n                        size=\"sm\"\n                        onClick={() => handleSaveEdit(sample.id)}\n                        disabled={updateSample.isPending}\n                      >\n                        <Check className=\"h-4 w-4 mr-1\" />\n                        {updateSample.isPending ? 'Saving...' : 'Save'}\n                      </Button>\n                    </div>\n                  </div>\n                ) : (\n                  <>\n                    {/* View Mode */}\n                    <div className=\"flex items-center gap-3 p-3 h-[72px]\">\n                      {/* Text Content */}\n                      <div className=\"flex-1 min-w-0 py-0.5\">\n                        <p className=\"text-sm font-medium line-clamp-2 leading-snug\">\n                          {sample.reference_text}\n                        </p>\n                      </div>\n\n                      {/* Action Buttons */}\n                      <div className=\"shrink-0 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity\">\n                        <CircleButton\n                          icon={Edit}\n                          title=\"Edit transcription\"\n                          onClick={() => handleStartEdit(sample.id, sample.reference_text)}\n                        />\n                        <CircleButton\n                          icon={Trash2}\n                          title=\"Delete sample\"\n                          onClick={() => handleDeleteClick(sample.id)}\n                          disabled={deleteSample.isPending}\n                        />\n                      </div>\n\n                      {/* Sample Number Badge */}\n                      <div className=\"absolute top-1 right-2 text-[10px] text-muted-foreground/50 font-medium\">\n                        #{index + 1}\n                      </div>\n                    </div>\n\n                    {/* Mini Player - Always visible */}\n                    <MiniSamplePlayer audioUrl={apiClient.getSampleUrl(sample.id)} />\n                  </>\n                )}\n              </div>\n            );\n          })}\n        </div>\n      )}\n\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        className=\"w-full\"\n        onClick={() => setUploadOpen(true)}\n      >\n        <Plus className=\"mr-2 h-4 w-4\" />\n        Add Sample\n      </Button>\n\n      <p className=\"text-xs text-muted-foreground text-center px-2\">\n        Note: A single 30-second sample is the sweet spot. Quality may decrease with multiple\n        samples. In a future update samples might be interchangeable and tagged for varying styles\n        of the same voice.\n      </p>\n\n      <SampleUpload profileId={profileId} open={uploadOpen} onOpenChange={setUploadOpen} />\n\n      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Delete Sample</DialogTitle>\n            <DialogDescription>\n              Are you sure you want to delete this audio sample? This action cannot be undone.\n            </DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button\n              variant=\"outline\"\n              onClick={() => {\n                setDeleteDialogOpen(false);\n                setSampleToDelete(null);\n              }}\n            >\n              Cancel\n            </Button>\n            <Button\n              variant=\"destructive\"\n              onClick={handleDeleteConfirm}\n              disabled={deleteSample.isPending}\n            >\n              {deleteSample.isPending ? 'Deleting...' : 'Delete'}\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoiceProfiles/SampleUpload.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod';\nimport { Mic, Monitor, Upload } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport * as z from 'zod';\nimport { Button } from '@/components/ui/button';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog';\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from '@/components/ui/form';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport { Textarea } from '@/components/ui/textarea';\nimport { useToast } from '@/components/ui/use-toast';\nimport { useAudioPlayer } from '@/lib/hooks/useAudioPlayer';\nimport { useAudioRecording } from '@/lib/hooks/useAudioRecording';\nimport { useAddSample, useProfile } from '@/lib/hooks/useProfiles';\nimport { useSystemAudioCapture } from '@/lib/hooks/useSystemAudioCapture';\nimport { useTranscription } from '@/lib/hooks/useTranscription';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { AudioSampleRecording } from './AudioSampleRecording';\nimport { AudioSampleSystem } from './AudioSampleSystem';\nimport { AudioSampleUpload } from './AudioSampleUpload';\n\nconst sampleSchema = z.object({\n  file: z.instanceof(File, { message: 'Please select an audio file' }),\n  referenceText: z\n    .string()\n    .min(1, 'Reference text is required')\n    .max(1000, 'Reference text must be less than 1000 characters'),\n});\n\ntype SampleFormValues = z.infer<typeof sampleSchema>;\n\ninterface SampleUploadProps {\n  profileId: string;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProps) {\n  const platform = usePlatform();\n  const addSample = useAddSample();\n  const transcribe = useTranscription();\n  const { data: profile } = useProfile(profileId);\n  const { toast } = useToast();\n  const [mode, setMode] = useState<'upload' | 'record' | 'system'>('upload');\n  const { isPlaying, playPause, cleanup: cleanupAudio } = useAudioPlayer();\n\n  const form = useForm<SampleFormValues>({\n    resolver: zodResolver(sampleSchema),\n    defaultValues: {\n      referenceText: '',\n    },\n  });\n\n  const selectedFile = form.watch('file');\n\n  const {\n    isRecording,\n    duration,\n    error: recordingError,\n    startRecording,\n    stopRecording,\n    cancelRecording,\n  } = useAudioRecording({\n    maxDurationSeconds: 29,\n    onRecordingComplete: (blob, recordedDuration) => {\n      // Convert blob to File object\n      const file = new File([blob], `recording-${Date.now()}.webm`, {\n        type: blob.type || 'audio/webm',\n      }) as File & { recordedDuration?: number };\n      // Store the actual recorded duration to bypass metadata reading issues on Windows\n      if (recordedDuration !== undefined) {\n        file.recordedDuration = recordedDuration;\n      }\n      form.setValue('file', file, { shouldValidate: true });\n      toast({\n        title: 'Recording complete',\n        description: 'Audio has been recorded successfully.',\n      });\n    },\n  });\n\n  const {\n    isRecording: isSystemRecording,\n    duration: systemDuration,\n    error: systemRecordingError,\n    isSupported: isSystemAudioSupported,\n    startRecording: startSystemRecording,\n    stopRecording: stopSystemRecording,\n    cancelRecording: cancelSystemRecording,\n  } = useSystemAudioCapture({\n    maxDurationSeconds: 29,\n    onRecordingComplete: (blob, recordedDuration) => {\n      // Convert blob to File object\n      const file = new File([blob], `system-audio-${Date.now()}.wav`, {\n        type: blob.type || 'audio/wav',\n      }) as File & { recordedDuration?: number };\n      // Store the actual recorded duration to bypass metadata reading issues on Windows\n      if (recordedDuration !== undefined) {\n        file.recordedDuration = recordedDuration;\n      }\n      form.setValue('file', file, { shouldValidate: true });\n      toast({\n        title: 'System audio captured',\n        description: 'Audio has been captured successfully.',\n      });\n    },\n  });\n\n  // Show recording errors\n  useEffect(() => {\n    if (recordingError) {\n      toast({\n        title: 'Recording error',\n        description: recordingError,\n        variant: 'destructive',\n      });\n    }\n  }, [recordingError, toast]);\n\n  // Show system audio recording errors\n  useEffect(() => {\n    if (systemRecordingError) {\n      toast({\n        title: 'System audio capture error',\n        description: systemRecordingError,\n        variant: 'destructive',\n      });\n    }\n  }, [systemRecordingError, toast]);\n\n  async function handleTranscribe() {\n    const file = form.getValues('file');\n    if (!file) {\n      toast({\n        title: 'No file selected',\n        description: 'Please select an audio file first.',\n        variant: 'destructive',\n      });\n      return;\n    }\n\n    try {\n      const language = profile?.language as 'en' | 'zh' | undefined;\n      const result = await transcribe.mutateAsync({ file, language });\n\n      form.setValue('referenceText', result.text, { shouldValidate: true });\n    } catch (error) {\n      toast({\n        title: 'Transcription failed',\n        description: error instanceof Error ? error.message : 'Failed to transcribe audio',\n        variant: 'destructive',\n      });\n    }\n  }\n\n  async function onSubmit(data: SampleFormValues) {\n    try {\n      await addSample.mutateAsync({\n        profileId,\n        file: data.file,\n        referenceText: data.referenceText,\n      });\n\n      toast({\n        title: 'Sample added',\n        description: 'Audio sample has been added successfully.',\n      });\n\n      handleOpenChange(false);\n    } catch (error) {\n      toast({\n        title: 'Error',\n        description: error instanceof Error ? error.message : 'Failed to add sample',\n        variant: 'destructive',\n      });\n    }\n  }\n\n  function handleOpenChange(newOpen: boolean) {\n    if (!newOpen) {\n      form.reset();\n      setMode('upload');\n      if (isRecording) {\n        cancelRecording();\n      }\n      if (isSystemRecording) {\n        cancelSystemRecording();\n      }\n      cleanupAudio();\n    }\n    onOpenChange(newOpen);\n  }\n\n  function handleCancelRecording() {\n    if (mode === 'record') {\n      cancelRecording();\n    } else if (mode === 'system') {\n      cancelSystemRecording();\n    }\n    form.resetField('file');\n    cleanupAudio();\n  }\n\n  function handlePlayPause() {\n    const file = form.getValues('file');\n    playPause(file);\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={handleOpenChange}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Add Audio Sample</DialogTitle>\n          <DialogDescription>\n            Upload an audio file and provide the reference text that matches the audio.\n          </DialogDescription>\n        </DialogHeader>\n\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n            <Tabs value={mode} onValueChange={(v) => setMode(v as 'upload' | 'record' | 'system')}>\n              <TabsList\n                className={`grid w-full ${platform.metadata.isTauri && isSystemAudioSupported ? 'grid-cols-3' : 'grid-cols-2'}`}\n              >\n                <TabsTrigger value=\"upload\" className=\"flex items-center gap-2\">\n                  <Upload className=\"h-4 w-4 shrink-0\" />\n                  Upload\n                </TabsTrigger>\n                <TabsTrigger value=\"record\" className=\"flex items-center gap-2\">\n                  <Mic className=\"h-4 w-4 shrink-0\" />\n                  Record\n                </TabsTrigger>\n                {platform.metadata.isTauri && isSystemAudioSupported && (\n                  <TabsTrigger value=\"system\" className=\"flex items-center gap-2\">\n                    <Monitor className=\"h-4 w-4 shrink-0\" />\n                    System Audio\n                  </TabsTrigger>\n                )}\n              </TabsList>\n\n              <TabsContent value=\"upload\" className=\"space-y-4\">\n                <FormField\n                  control={form.control}\n                  name=\"file\"\n                  render={({ field: { onChange, name } }) => (\n                    <AudioSampleUpload\n                      file={selectedFile}\n                      onFileChange={onChange}\n                      onTranscribe={handleTranscribe}\n                      onPlayPause={handlePlayPause}\n                      isPlaying={isPlaying}\n                      isTranscribing={transcribe.isPending}\n                      fieldName={name}\n                    />\n                  )}\n                />\n              </TabsContent>\n\n              <TabsContent value=\"record\" className=\"space-y-4\">\n                <FormField\n                  control={form.control}\n                  name=\"file\"\n                  render={() => (\n                    <AudioSampleRecording\n                      file={selectedFile}\n                      isRecording={isRecording}\n                      duration={duration}\n                      onStart={startRecording}\n                      onStop={stopRecording}\n                      onCancel={handleCancelRecording}\n                      onTranscribe={handleTranscribe}\n                      onPlayPause={handlePlayPause}\n                      isPlaying={isPlaying}\n                      isTranscribing={transcribe.isPending}\n                    />\n                  )}\n                />\n              </TabsContent>\n\n              {platform.metadata.isTauri && isSystemAudioSupported && (\n                <TabsContent value=\"system\" className=\"space-y-4\">\n                  <FormField\n                    control={form.control}\n                    name=\"file\"\n                    render={() => (\n                      <AudioSampleSystem\n                        file={selectedFile}\n                        isRecording={isSystemRecording}\n                        duration={systemDuration}\n                        onStart={startSystemRecording}\n                        onStop={stopSystemRecording}\n                        onCancel={handleCancelRecording}\n                        onTranscribe={handleTranscribe}\n                        onPlayPause={handlePlayPause}\n                        isPlaying={isPlaying}\n                        isTranscribing={transcribe.isPending}\n                      />\n                    )}\n                  />\n                </TabsContent>\n              )}\n            </Tabs>\n\n            <FormField\n              control={form.control}\n              name=\"referenceText\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Reference Text</FormLabel>\n                  <FormControl>\n                    <Textarea\n                      placeholder=\"Enter the exact text spoken in the audio...\"\n                      className=\"min-h-[100px]\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <div className=\"flex gap-2 justify-end\">\n              <Button type=\"button\" variant=\"outline\" onClick={() => handleOpenChange(false)}>\n                Cancel\n              </Button>\n              <Button type=\"submit\" disabled={addSample.isPending}>\n                {addSample.isPending ? 'Uploading...' : 'Add Sample'}\n              </Button>\n            </div>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoicesTab/VoiceInspector.tsx",
    "content": "import { zodResolver } from '@hookform/resolvers/zod';\nimport { Edit2, Mic, X } from 'lucide-react';\nimport { useEffect, useRef, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport * as z from 'zod';\nimport { EffectsChainEditor } from '@/components/Effects/EffectsChainEditor';\nimport { Button } from '@/components/ui/button';\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from '@/components/ui/form';\nimport { Input } from '@/components/ui/input';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { Textarea } from '@/components/ui/textarea';\nimport { useToast } from '@/components/ui/use-toast';\nimport { SampleList } from '@/components/VoiceProfiles/SampleList';\nimport { apiClient } from '@/lib/api/client';\nimport type { EffectConfig } from '@/lib/api/types';\nimport { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';\nimport { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';\nimport {\n  useDeleteAvatar,\n  useProfile,\n  useUpdateProfile,\n  useUploadAvatar,\n} from '@/lib/hooks/useProfiles';\nimport { cn } from '@/lib/utils/cn';\nimport { usePlayerStore } from '@/stores/playerStore';\nimport { useServerStore } from '@/stores/serverStore';\n\nconst profileSchema = z.object({\n  name: z.string().min(1, 'Name is required').max(100),\n  description: z.string().max(500).optional(),\n  language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),\n});\n\ntype ProfileFormValues = z.infer<typeof profileSchema>;\n\ninterface VoiceInspectorProps {\n  profileId: string;\n}\n\nexport function VoiceInspector({ profileId }: VoiceInspectorProps) {\n  const { data: profile } = useProfile(profileId);\n  const audioUrl = usePlayerStore((state) => state.audioUrl);\n  const isPlayerVisible = !!audioUrl;\n  const updateProfile = useUpdateProfile();\n  const uploadAvatar = useUploadAvatar();\n  const deleteAvatar = useDeleteAvatar();\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const { toast } = useToast();\n\n  const [avatarPreview, setAvatarPreview] = useState<string | null>(null);\n  const [avatarError, setAvatarError] = useState(false);\n  const avatarInputRef = useRef<HTMLInputElement>(null);\n\n  const [effectsChain, setEffectsChain] = useState<EffectConfig[]>([]);\n  const [effectsDirty, setEffectsDirty] = useState(false);\n\n  const form = useForm<ProfileFormValues>({\n    resolver: zodResolver(profileSchema),\n    defaultValues: {\n      name: '',\n      description: '',\n      language: 'en',\n    },\n  });\n\n  // Populate form when profile loads\n  useEffect(() => {\n    if (profile) {\n      form.reset({\n        name: profile.name,\n        description: profile.description || '',\n        language: profile.language as LanguageCode,\n      });\n      setEffectsChain(profile.effects_chain ?? []);\n      setEffectsDirty(false);\n    }\n  }, [profile, form]);\n\n  // Avatar preview\n  useEffect(() => {\n    if (profile?.avatar_path) {\n      setAvatarPreview(`${serverUrl}/profiles/${profile.id}/avatar`);\n    } else {\n      setAvatarPreview(null);\n    }\n    setAvatarError(false);\n  }, [profile, serverUrl]);\n\n  function handleAvatarFileChange(e: React.ChangeEvent<HTMLInputElement>) {\n    const file = e.target.files?.[0];\n    if (!file) return;\n    if (!file.type.startsWith('image/')) {\n      toast({\n        title: 'Invalid file type',\n        description: 'Please select PNG, JPG, or WebP',\n        variant: 'destructive',\n      });\n      return;\n    }\n    if (file.size > 5 * 1024 * 1024) {\n      toast({\n        title: 'File too large',\n        description: 'Image must be less than 5MB',\n        variant: 'destructive',\n      });\n      return;\n    }\n    // Upload immediately\n    uploadAvatar.mutate(\n      { profileId, file },\n      {\n        onSuccess: () => {\n          setAvatarPreview(URL.createObjectURL(file));\n          toast({ title: 'Avatar updated' });\n        },\n        onError: (err) => {\n          toast({\n            title: 'Avatar upload failed',\n            description: err instanceof Error ? err.message : 'Unknown error',\n            variant: 'destructive',\n          });\n        },\n      },\n    );\n  }\n\n  async function handleRemoveAvatar() {\n    if (profile?.avatar_path) {\n      try {\n        await deleteAvatar.mutateAsync(profileId);\n        toast({ title: 'Avatar removed' });\n      } catch (err) {\n        toast({\n          title: 'Failed to remove avatar',\n          description: err instanceof Error ? err.message : 'Unknown error',\n          variant: 'destructive',\n        });\n      }\n    }\n    setAvatarPreview(null);\n    if (avatarInputRef.current) avatarInputRef.current.value = '';\n  }\n\n  async function onSubmit(data: ProfileFormValues) {\n    try {\n      await updateProfile.mutateAsync({\n        profileId,\n        data: {\n          name: data.name,\n          description: data.description,\n          language: data.language,\n        },\n      });\n\n      if (effectsDirty) {\n        try {\n          await apiClient.updateProfileEffects(\n            profileId,\n            effectsChain.length > 0 ? effectsChain : null,\n          );\n          setEffectsDirty(false);\n        } catch (fxError) {\n          toast({\n            title: 'Effects update failed',\n            description: fxError instanceof Error ? fxError.message : 'Failed to save effects',\n            variant: 'destructive',\n          });\n          return;\n        }\n      }\n\n      toast({ title: 'Voice updated', description: `\"${data.name}\" saved.` });\n    } catch (error) {\n      toast({\n        title: 'Error',\n        description: error instanceof Error ? error.message : 'Failed to save profile',\n        variant: 'destructive',\n      });\n    }\n  }\n\n  if (!profile) {\n    return (\n      <div className=\"flex items-center justify-center h-full text-muted-foreground text-sm\">\n        Loading...\n      </div>\n    );\n  }\n\n  const isDirty = form.formState.isDirty || effectsDirty;\n\n  return (\n    <div className=\"h-full flex flex-col overflow-hidden\">\n      <div className={cn('flex-1 overflow-y-auto', isPlayerVisible && BOTTOM_SAFE_AREA_PADDING)}>\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-0\">\n            {/* Avatar */}\n            <div className=\"flex justify-center pt-5 pb-3\">\n              <div className=\"relative group\">\n                <div className=\"h-20 w-20 rounded-full bg-muted flex items-center justify-center shrink-0 overflow-hidden border-2 border-border\">\n                  {avatarPreview && !avatarError ? (\n                    <img\n                      src={avatarPreview}\n                      alt={profile.name}\n                      className=\"h-full w-full object-cover\"\n                      onError={() => setAvatarError(true)}\n                    />\n                  ) : (\n                    <Mic className=\"h-8 w-8 text-muted-foreground\" />\n                  )}\n                </div>\n                <button\n                  type=\"button\"\n                  onClick={() => avatarInputRef.current?.click()}\n                  className=\"absolute inset-0 rounded-full bg-accent/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer\"\n                >\n                  <Edit2 className=\"h-5 w-5 text-accent-foreground\" />\n                </button>\n                {avatarPreview && (\n                  <button\n                    type=\"button\"\n                    onClick={handleRemoveAvatar}\n                    disabled={deleteAvatar.isPending}\n                    className=\"absolute bottom-0 right-0 h-5 w-5 rounded-full bg-background/60 backdrop-blur-sm text-muted-foreground flex items-center justify-center hover:bg-background/80 hover:text-foreground transition-colors shadow-sm border border-border/50\"\n                  >\n                    <X className=\"h-3 w-3\" />\n                  </button>\n                )}\n              </div>\n              <input\n                ref={avatarInputRef}\n                type=\"file\"\n                accept=\"image/png,image/jpeg,image/webp\"\n                onChange={handleAvatarFileChange}\n                className=\"hidden\"\n              />\n            </div>\n\n            {/* Fields */}\n            <div className=\"space-y-3 px-5\">\n              <FormField\n                control={form.control}\n                name=\"name\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Name</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"My Voice\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n\n              <FormField\n                control={form.control}\n                name=\"description\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Description</FormLabel>\n                    <FormControl>\n                      <Textarea placeholder=\"Describe this voice...\" rows={2} {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n\n              <FormField\n                control={form.control}\n                name=\"language\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Language</FormLabel>\n                    <Select onValueChange={field.onChange} value={field.value}>\n                      <FormControl>\n                        <SelectTrigger>\n                          <SelectValue />\n                        </SelectTrigger>\n                      </FormControl>\n                      <SelectContent>\n                        {LANGUAGE_OPTIONS.map((lang) => (\n                          <SelectItem key={lang.value} value={lang.value}>\n                            {lang.label}\n                          </SelectItem>\n                        ))}\n                      </SelectContent>\n                    </Select>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n\n              {/* Effects */}\n              <div className=\"space-y-2\">\n                <FormLabel>Default Effects</FormLabel>\n                <p className=\"text-xs text-muted-foreground\">\n                  Applied automatically to new generations with this voice.\n                </p>\n                <EffectsChainEditor\n                  value={effectsChain}\n                  onChange={(chain) => {\n                    setEffectsChain(chain);\n                    setEffectsDirty(true);\n                  }}\n                  compact\n                />\n              </div>\n\n              {/* Save */}\n              {isDirty && (\n                <Button type=\"submit\" className=\"w-full\" disabled={updateProfile.isPending}>\n                  {updateProfile.isPending ? 'Saving...' : 'Save Changes'}\n                </Button>\n              )}\n            </div>\n\n            {/* Samples */}\n            <div className=\"px-5 pb-5\">\n              <SampleList profileId={profileId} />\n            </div>\n          </form>\n        </Form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/src/components/VoicesTab/VoicesTab.tsx",
    "content": "import { useQuery, useQueryClient } from '@tanstack/react-query';\nimport { Mic, Plus, Search, Sparkles } from 'lucide-react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\n\nimport { MultiSelect } from '@/components/ui/multi-select';\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/components/ui/table';\nimport { ProfileForm } from '@/components/VoiceProfiles/ProfileForm';\nimport { apiClient } from '@/lib/api/client';\nimport type { VoiceProfileResponse } from '@/lib/api/types';\nimport { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';\nimport { useProfiles } from '@/lib/hooks/useProfiles';\nimport { cn } from '@/lib/utils/cn';\nimport { usePlayerStore } from '@/stores/playerStore';\nimport { useServerStore } from '@/stores/serverStore';\nimport { useUIStore } from '@/stores/uiStore';\nimport { VoiceInspector } from './VoiceInspector';\n\nexport function VoicesTab() {\n  const { data: profiles, isLoading } = useProfiles();\n  const queryClient = useQueryClient();\n  const setDialogOpen = useUIStore((state) => state.setProfileDialogOpen);\n  const selectedVoiceId = useUIStore((state) => state.selectedVoiceId);\n  const setSelectedVoiceId = useUIStore((state) => state.setSelectedVoiceId);\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const audioUrl = usePlayerStore((state) => state.audioUrl);\n  const isPlayerVisible = !!audioUrl;\n  const [search, setSearch] = useState('');\n\n  const filteredProfiles = useMemo(() => {\n    if (!profiles) return [];\n    if (!search.trim()) return profiles;\n    const q = search.toLowerCase();\n    return profiles.filter(\n      (p) =>\n        p.name.toLowerCase().includes(q) ||\n        p.description?.toLowerCase().includes(q) ||\n        p.language.toLowerCase().includes(q),\n    );\n  }, [profiles, search]);\n\n  // Auto-select first profile if none selected\n  useEffect(() => {\n    if (!selectedVoiceId && profiles && profiles.length > 0) {\n      setSelectedVoiceId(profiles[0].id);\n    }\n    // Clear selection if selected profile was deleted\n    if (selectedVoiceId && profiles && !profiles.find((p) => p.id === selectedVoiceId)) {\n      setSelectedVoiceId(profiles.length > 0 ? profiles[0].id : null);\n    }\n  }, [profiles, selectedVoiceId, setSelectedVoiceId]);\n\n  // Get channel assignments for each profile\n  const { data: channelAssignments } = useQuery({\n    queryKey: ['profile-channels'],\n    queryFn: async () => {\n      if (!profiles) return {};\n      const assignments: Record<string, string[]> = {};\n      for (const profile of profiles) {\n        try {\n          const result = await apiClient.getProfileChannels(profile.id);\n          assignments[profile.id] = result.channel_ids;\n        } catch {\n          assignments[profile.id] = [];\n        }\n      }\n      return assignments;\n    },\n    enabled: !!profiles,\n  });\n\n  // Get all channels\n  const { data: channels } = useQuery({\n    queryKey: ['channels'],\n    queryFn: () => apiClient.listChannels(),\n  });\n\n  const handleChannelChange = async (profileId: string, channelIds: string[]) => {\n    try {\n      await apiClient.setProfileChannels(profileId, channelIds);\n      queryClient.invalidateQueries({ queryKey: ['profile-channels'] });\n    } catch (error) {\n      console.error('Failed to update channels:', error);\n    }\n  };\n\n  if (isLoading) {\n    return (\n      <div className=\"flex items-center justify-center h-full\">\n        <div className=\"text-muted-foreground\">Loading voices...</div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"h-full flex gap-0 overflow-hidden -mx-8\">\n      {/* Left: Table */}\n      <div className=\"flex-1 min-w-0 flex flex-col relative overflow-hidden\">\n        {/* Scroll Mask */}\n        <div className=\"absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-background to-transparent z-10 pointer-events-none\" />\n\n        {/* Fixed Header */}\n        <div className=\"absolute top-0 left-0 right-0 z-20 pl-8 pr-8\">\n          <div className=\"flex items-center gap-3 mb-6\">\n            <h1 className=\"text-2xl font-bold\">Voices</h1>\n            <div className=\"flex-1\" />\n            <div className=\"relative w-[240px]\">\n              <Search className=\"absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground\" />\n              <Input\n                placeholder=\"Search voices...\"\n                value={search}\n                onChange={(e) => setSearch(e.target.value)}\n                className=\"h-10 pl-8 text-sm rounded-full focus-visible:ring-0 focus-visible:ring-offset-0\"\n              />\n            </div>\n            <Button onClick={() => setDialogOpen(true)}>\n              <Plus className=\"h-4 w-4 mr-2\" />\n              New Voice\n            </Button>\n          </div>\n        </div>\n\n        {/* Scrollable Content */}\n        <div\n          ref={scrollRef}\n          className={cn(\n            'flex-1 overflow-y-auto overflow-x-hidden pt-16 relative z-0',\n            isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,\n          )}\n        >\n          <Table className=\"table-fixed [&_td:first-child]:pl-8 [&_th:first-child]:pl-8\">\n            <TableHeader>\n              <TableRow>\n                <TableHead className=\"w-[30%]\">Name</TableHead>\n                <TableHead className=\"w-[10%]\">Language</TableHead>\n                <TableHead className=\"w-[10%]\">Generations</TableHead>\n                <TableHead className=\"w-[8%]\">Samples</TableHead>\n                <TableHead className=\"w-[8%]\">Effects</TableHead>\n                <TableHead className=\"w-[24%]\">Channels</TableHead>\n                <TableHead className=\"w-6\"></TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {filteredProfiles.map((profile) => (\n                <VoiceRow\n                  key={profile.id}\n                  profile={profile}\n                  isSelected={selectedVoiceId === profile.id}\n                  onSelect={() => setSelectedVoiceId(profile.id)}\n                  channelIds={channelAssignments?.[profile.id] || []}\n                  channels={channels || []}\n                  onChannelChange={(channelIds) => handleChannelChange(profile.id, channelIds)}\n                />\n              ))}\n            </TableBody>\n          </Table>\n        </div>\n      </div>\n\n      {/* Right: Inspector */}\n      {selectedVoiceId && (\n        <div className=\"w-[340px] shrink-0 border-l border-t rounded-tl-xl bg-muted/30\">\n          <VoiceInspector key={selectedVoiceId} profileId={selectedVoiceId} />\n        </div>\n      )}\n\n      <ProfileForm />\n    </div>\n  );\n}\n\ninterface VoiceRowProps {\n  profile: VoiceProfileResponse;\n  isSelected: boolean;\n  onSelect: () => void;\n  channelIds: string[];\n  channels: Array<{ id: string; name: string; is_default: boolean }>;\n  onChannelChange: (channelIds: string[]) => void;\n}\n\nfunction VoiceRow({\n  profile,\n  isSelected,\n  onSelect,\n  channelIds,\n  channels,\n  onChannelChange,\n}: VoiceRowProps) {\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const [avatarError, setAvatarError] = useState(false);\n  const avatarUrl = profile.avatar_path ? `${serverUrl}/profiles/${profile.id}/avatar` : null;\n\n  const enabledEffects = profile.effects_chain?.filter((e) => e.enabled) ?? [];\n  const effectsSummary = enabledEffects.map((e) => e.type).join(' → ');\n\n  return (\n    <TableRow\n      className={cn('cursor-pointer', isSelected ? 'bg-muted/50' : 'hover:bg-muted/50')}\n      onClick={onSelect}\n    >\n      <TableCell>\n        <div className=\"flex w-full min-w-0 items-center gap-2\">\n          <div className=\"h-8 w-8 rounded-full bg-muted flex items-center justify-center shrink-0 overflow-hidden\">\n            {avatarUrl && !avatarError ? (\n              <img\n                src={avatarUrl}\n                alt={`${profile.name} avatar`}\n                className=\"h-full w-full object-cover\"\n                onError={() => setAvatarError(true)}\n              />\n            ) : (\n              <Mic className=\"h-4 w-4 text-muted-foreground\" />\n            )}\n          </div>\n          <div className=\"min-w-0\">\n            <div className=\"font-medium truncate\">{profile.name}</div>\n            {profile.description && (\n              <div className=\"text-sm text-muted-foreground truncate\">{profile.description}</div>\n            )}\n          </div>\n        </div>\n      </TableCell>\n      <TableCell>{profile.language}</TableCell>\n      <TableCell>{profile.generation_count}</TableCell>\n      <TableCell>{profile.sample_count}</TableCell>\n      <TableCell>\n        {enabledEffects.length > 0 ? (\n          <span\n            className=\"inline-flex items-center gap-1 text-xs text-accent\"\n            title={effectsSummary}\n          >\n            <Sparkles className=\"h-3 w-3 fill-accent\" />\n            {enabledEffects.length}\n          </span>\n        ) : (\n          <span className=\"text-xs text-muted-foreground\">—</span>\n        )}\n      </TableCell>\n      <TableCell onClick={(e) => e.stopPropagation()}>\n        <MultiSelect\n          options={channels.map((ch) => ({\n            value: ch.id,\n            label: `${ch.name}${ch.is_default ? ' (Default)' : ''}`,\n          }))}\n          value={channelIds}\n          onChange={onChannelChange}\n          placeholder=\"Select channels...\"\n          className=\"w-full\"\n        />\n      </TableCell>\n      <TableCell />\n    </TableRow>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ui/alert-dialog.tsx",
    "content": "import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\nimport { buttonVariants } from './button';\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Overlay\n    className={cn(\n      'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',\n        className,\n      )}\n      {...props}\n    />\n  </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />\n);\nAlertDialogHeader.displayName = 'AlertDialogHeader';\n\nconst AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}\n    {...props}\n  />\n);\nAlertDialogFooter.displayName = 'AlertDialogFooter';\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title\n    ref={ref}\n    className={cn('text-lg font-semibold', className)}\n    {...props}\n  />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}\n    {...props}\n  />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n};"
  },
  {
    "path": "app/src/components/ui/badge.tsx",
    "content": "import { cva, type VariantProps } from 'class-variance-authority';\nimport type * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst badgeVariants = cva(\n  'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',\n  {\n    variants: {\n      variant: {\n        default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',\n        secondary:\n          'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        destructive:\n          'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',\n        outline: 'text-foreground',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  },\n);\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return <div className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "app/src/components/ui/button.tsx",
    "content": "import { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst buttonVariants = cva(\n  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        default: 'bg-accent text-accent-foreground hover:bg-accent/90',\n        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',\n        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        ghost: 'hover:bg-accent hover:text-accent-foreground',\n        link: 'text-accent underline-offset-4 hover:underline',\n      },\n      size: {\n        default: 'h-10 px-4 py-2',\n        sm: 'h-9 rounded-full px-3',\n        lg: 'h-11 rounded-full px-8',\n        icon: 'h-10 w-10 rounded-full',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />\n    );\n  },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "app/src/components/ui/card.tsx",
    "content": "import * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div\n      ref={ref}\n      className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}\n      {...props}\n    />\n  ),\n);\nCard.displayName = 'Card';\n\nconst CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />\n  ),\n);\nCardHeader.displayName = 'CardHeader';\n\nconst CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(\n  ({ className, ...props }, ref) => (\n    <h3\n      ref={ref}\n      className={cn('text-2xl font-semibold leading-none tracking-tight', className)}\n      {...props}\n    />\n  ),\n);\nCardTitle.displayName = 'CardTitle';\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />\n));\nCardDescription.displayName = 'CardDescription';\n\nconst CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />\n  ),\n);\nCardContent.displayName = 'CardContent';\n\nconst CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />\n  ),\n);\nCardFooter.displayName = 'CardFooter';\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };\n"
  },
  {
    "path": "app/src/components/ui/checkbox.tsx",
    "content": "import { Check } from 'lucide-react';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nexport interface CheckboxProps {\n  checked?: boolean;\n  onCheckedChange?: (checked: boolean) => void;\n  disabled?: boolean;\n  className?: string;\n  id?: string;\n}\n\nconst Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(\n  ({ checked = false, onCheckedChange, disabled = false, className, id, ...props }, ref) => {\n    return (\n      <button\n        type=\"button\"\n        ref={ref}\n        id={id}\n        role=\"checkbox\"\n        aria-checked={checked}\n        disabled={disabled}\n        onClick={() => {\n          if (!disabled && onCheckedChange) {\n            onCheckedChange(!checked);\n          }\n        }}\n        className={cn(\n          'h-4 w-4 rounded border-2 flex items-center justify-center shrink-0 transition-colors',\n          checked ? 'bg-accent border-accent' : 'border-muted-foreground/30',\n          disabled && 'opacity-50 cursor-not-allowed',\n          !disabled && 'cursor-pointer',\n          className,\n        )}\n        {...props}\n      >\n        {checked && <Check className=\"h-3 w-3 text-accent-foreground\" />}\n      </button>\n    );\n  },\n);\nCheckbox.displayName = 'Checkbox';\n\nexport { Checkbox };\n"
  },
  {
    "path": "app/src/components/ui/circle-button.tsx",
    "content": "import * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nexport interface CircleButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  icon: React.ComponentType<{ className?: string }>;\n}\n\nconst CircleButton = React.forwardRef<HTMLButtonElement, CircleButtonProps>(\n  ({ className, icon: Icon, type = 'button', ...props }, ref) => {\n    return (\n      <button\n        ref={ref}\n        type={type}\n        className={cn(\n          'h-7 w-7 rounded-full flex items-center justify-center flex-shrink-0',\n          'hover:bg-muted transition-colors',\n          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n          'disabled:pointer-events-none disabled:opacity-50',\n          className,\n        )}\n        {...props}\n      >\n        <Icon className=\"h-3.5 w-3.5 text-muted-foreground/60\" />\n      </button>\n    );\n  },\n);\nCircleButton.displayName = 'CircleButton';\n\nexport { CircleButton };\n"
  },
  {
    "path": "app/src/components/ui/dialog.tsx",
    "content": "import * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />\n);\nDialogHeader.displayName = 'DialogHeader';\n\nconst DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}\n    {...props}\n  />\n);\nDialogFooter.displayName = 'DialogFooter';\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn('text-lg font-semibold leading-none tracking-tight', className)}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn('text-sm text-muted-foreground', className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "app/src/components/ui/dropdown-menu.tsx",
    "content": "import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport { MoreHorizontal } from 'lucide-react';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <MoreHorizontal className=\"ml-auto h-4 w-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        className,\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <MoreHorizontal className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <MoreHorizontal className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />\n  );\n};\nDropdownMenuShortcut.displayName = 'DropdownMenuShortcut';\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "app/src/components/ui/form.tsx",
    "content": "import type * as LabelPrimitive from '@radix-ui/react-label';\nimport { Slot } from '@radix-ui/react-slot';\nimport * as React from 'react';\nimport {\n  Controller,\n  type ControllerProps,\n  type FieldPath,\n  type FieldValues,\n  FormProvider,\n  useFormContext,\n} from 'react-hook-form';\nimport { cn } from '@/lib/utils/cn';\nimport { Label } from './label';\n\nconst Form = FormProvider;\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error('useFormField should be used within <FormField>');\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);\n\nconst FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => {\n    const id = React.useId();\n\n    return (\n      <FormItemContext.Provider value={{ id }}>\n        <div ref={ref} className={cn('space-y-2', className)} {...props} />\n      </FormItemContext.Provider>\n    );\n  },\n);\nFormItem.displayName = 'FormItem';\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && 'text-destructive', className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n});\nFormLabel.displayName = 'FormLabel';\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } = useFormField();\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = 'FormControl';\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn('text-sm text-muted-foreground', className)}\n      {...props}\n    />\n  );\n});\nFormDescription.displayName = 'FormDescription';\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn('text-sm font-medium text-destructive', className)}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n});\nFormMessage.displayName = 'FormMessage';\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "app/src/components/ui/input.tsx",
    "content": "import * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nexport interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nInput.displayName = 'Input';\n\nexport { Input };\n"
  },
  {
    "path": "app/src/components/ui/label.tsx",
    "content": "import * as LabelPrimitive from '@radix-ui/react-label';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst labelVariants = cva(\n  'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "app/src/components/ui/multi-select.tsx",
    "content": "import * as React from 'react';\nimport { ChevronDown, Check } from 'lucide-react';\nimport { cn } from '@/lib/utils/cn';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\n\nexport interface MultiSelectOption {\n  value: string;\n  label: string;\n}\n\nexport interface MultiSelectProps {\n  options: MultiSelectOption[];\n  value: string[];\n  onChange: (value: string[]) => void;\n  placeholder?: string;\n  className?: string;\n}\n\nconst MultiSelectCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nMultiSelectCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;\n\nexport function MultiSelect({\n  options,\n  value,\n  onChange,\n  placeholder = 'Select...',\n  className,\n}: MultiSelectProps) {\n  const [open, setOpen] = React.useState(false);\n\n  const handleSelect = (optionValue: string) => {\n    const newValue = value.includes(optionValue)\n      ? value.filter((v) => v !== optionValue)\n      : [...value, optionValue];\n    onChange(newValue);\n  };\n\n  const displayText =\n    value.length === 0\n      ? placeholder\n      : value.length === 1\n        ? options.find((opt) => opt.value === value[0])?.label || placeholder\n        : `${value.length} selected`;\n\n  return (\n    <DropdownMenu open={open} onOpenChange={setOpen}>\n      <DropdownMenuTrigger asChild>\n        <button\n          type=\"button\"\n          className={cn(\n            'flex h-8 w-full items-center justify-between rounded-full border border-border bg-card px-3 py-2 text-xs ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 hover:bg-background/50 transition-all',\n            className,\n          )}\n        >\n          <span className=\"line-clamp-1\">{displayText}</span>\n          <ChevronDown className=\"h-4 w-4 opacity-50\" />\n        </button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        className=\"max-h-96 overflow-auto\"\n        align=\"start\"\n        onCloseAutoFocus={(e) => e.preventDefault()}\n      >\n        {options.map((option) => (\n          <MultiSelectCheckboxItem\n            key={option.value}\n            checked={value.includes(option.value)}\n            onSelect={() => handleSelect(option.value)}\n            onCheckedChange={() => handleSelect(option.value)}\n          >\n            {option.label}\n          </MultiSelectCheckboxItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ui/popover.tsx",
    "content": "import * as PopoverPrimitive from '@radix-ui/react-popover';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        className,\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
  },
  {
    "path": "app/src/components/ui/progress.tsx",
    "content": "import * as ProgressPrimitive from '@radix-ui/react-progress';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>\n>(({ className, value, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn('relative h-4 w-full overflow-hidden rounded-full bg-secondary', className)}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className=\"h-full w-full flex-1 bg-accent transition-all\"\n      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n));\nProgress.displayName = ProgressPrimitive.Root.displayName;\n\nexport { Progress };\n"
  },
  {
    "path": "app/src/components/ui/select.tsx",
    "content": "import * as SelectPrimitive from '@radix-ui/react-select';\nimport { Check, ChevronDown, ChevronUp } from 'lucide-react';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:bg-muted/50 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn('flex cursor-default items-center justify-center py-1', className)}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn('flex cursor-default items-center justify-center py-1', className)}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = 'popper', ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        position === 'popper' &&\n          'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          'p-1',\n          position === 'popper' &&\n            'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground focus:[&_*]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn('-mx-1 my-1 h-px bg-muted', className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "app/src/components/ui/separator.tsx",
    "content": "import * as SeparatorPrimitive from '@radix-ui/react-separator';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (\n  <SeparatorPrimitive.Root\n    ref={ref}\n    decorative={decorative}\n    orientation={orientation}\n    className={cn(\n      'shrink-0 bg-border',\n      orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',\n      className,\n    )}\n    {...props}\n  />\n));\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "app/src/components/ui/slider.tsx",
    "content": "import * as SliderPrimitive from '@radix-ui/react-slider';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Slider = React.forwardRef<\n  React.ElementRef<typeof SliderPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <SliderPrimitive.Root\n    ref={ref}\n    className={cn('relative flex w-full touch-none select-none items-center', className)}\n    {...props}\n  >\n    <SliderPrimitive.Track className=\"relative h-2 w-full grow overflow-hidden rounded-full bg-secondary\">\n      <SliderPrimitive.Range className=\"absolute h-full bg-primary\" />\n    </SliderPrimitive.Track>\n    <SliderPrimitive.Thumb className=\"block h-0 w-0 outline-none disabled:pointer-events-none disabled:opacity-50 after:block after:h-5 after:w-5 after:rounded-full after:border-2 after:border-primary after:bg-background after:ring-offset-background after:transition-colors after:absolute after:top-1/2 after:left-1/2 after:-translate-x-1/2 after:-translate-y-1/2 focus-visible:after:ring-2 focus-visible:after:ring-ring focus-visible:after:ring-offset-2\" />\n  </SliderPrimitive.Root>\n));\nSlider.displayName = SliderPrimitive.Root.displayName;\n\nexport { Slider };\n"
  },
  {
    "path": "app/src/components/ui/table.tsx",
    "content": "import * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(\n  ({ className, ...props }, ref) => (\n    <div className=\"relative w-full overflow-auto\">\n      <table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />\n    </div>\n  ),\n);\nTable.displayName = 'Table';\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead\n    ref={ref}\n    className={cn('[&_tr]:border-b [&_tr]:hover:bg-transparent', className)}\n    {...props}\n  />\n));\nTableHeader.displayName = 'TableHeader';\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />\n));\nTableBody.displayName = 'TableBody';\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}\n    {...props}\n  />\n));\nTableFooter.displayName = 'TableFooter';\n\nconst TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(\n  ({ className, ...props }, ref) => (\n    <tr\n      ref={ref}\n      className={cn('border-b hover:bg-muted/50 data-[state=selected]:bg-muted', className)}\n      {...props}\n    />\n  ),\n);\nTableRow.displayName = 'TableRow';\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',\n      className,\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = 'TableHead';\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)}\n    {...props}\n  />\n));\nTableCell.displayName = 'TableCell';\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />\n));\nTableCaption.displayName = 'TableCaption';\n\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };\n"
  },
  {
    "path": "app/src/components/ui/tabs.tsx",
    "content": "import * as TabsPrimitive from '@radix-ui/react-tabs';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',\n      className,\n    )}\n    {...props}\n  />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',\n      className,\n    )}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n      className,\n    )}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "app/src/components/ui/textarea.tsx",
    "content": "import * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nexport interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nTextarea.displayName = 'Textarea';\n\nexport { Textarea };\n"
  },
  {
    "path": "app/src/components/ui/toast.tsx",
    "content": "import * as ToastPrimitives from '@radix-ui/react-toast';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { X } from 'lucide-react';\nimport * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',\n      className,\n    )}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',\n  {\n    variants: {\n      variant: {\n        default: 'border bg-background text-foreground',\n        destructive:\n          'destructive group border-destructive bg-destructive text-destructive-foreground',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  },\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  );\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',\n      className,\n    )}\n    {...props}\n  />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',\n      className,\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <X className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title ref={ref} className={cn('text-sm font-semibold', className)} {...props} />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn('text-sm opacity-90', className)}\n    {...props}\n  />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n};\n"
  },
  {
    "path": "app/src/components/ui/toaster.tsx",
    "content": "import { usePlayerStore } from '@/stores/playerStore';\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from './toast';\nimport { useToast } from './use-toast';\n\nexport function Toaster() {\n  const { toasts } = useToast();\n  const isPlayerOpen = !!usePlayerStore((s) => s.audioUrl);\n\n  return (\n    <ToastProvider>\n      {toasts.map(({ id, title, description, action, ...props }) => (\n        <Toast key={id} {...props}>\n          <div className=\"grid gap-1 flex-1 min-w-0\">\n            {title && <ToastTitle>{title}</ToastTitle>}\n            {description && <ToastDescription>{description}</ToastDescription>}\n          </div>\n          {action}\n          <ToastClose />\n        </Toast>\n      ))}\n      <ToastViewport className={isPlayerOpen ? 'sm:bottom-44' : ''} />\n    </ToastProvider>\n  );\n}\n"
  },
  {
    "path": "app/src/components/ui/toggle.tsx",
    "content": "import * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\nexport interface ToggleProps {\n  checked?: boolean;\n  onCheckedChange?: (checked: boolean) => void;\n  disabled?: boolean;\n  className?: string;\n  id?: string;\n}\n\nconst Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(\n  ({ checked = false, onCheckedChange, disabled = false, className, id, ...props }, ref) => {\n    return (\n      <button\n        type=\"button\"\n        ref={ref}\n        id={id}\n        role=\"switch\"\n        aria-checked={checked}\n        disabled={disabled}\n        onClick={() => {\n          if (!disabled && onCheckedChange) {\n            onCheckedChange(!checked);\n          }\n        }}\n        className={cn(\n          'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors',\n          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n          checked ? 'bg-accent' : 'bg-muted-foreground/25',\n          disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',\n          className,\n        )}\n        {...props}\n      >\n        <span\n          className={cn(\n            'pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm transition-transform',\n            checked ? 'translate-x-[18px]' : 'translate-x-[2px]',\n          )}\n        />\n      </button>\n    );\n  },\n);\nToggle.displayName = 'Toggle';\n\nexport { Toggle };\n"
  },
  {
    "path": "app/src/components/ui/use-toast.ts",
    "content": "import * as React from 'react';\nimport type { ToastActionElement, ToastProps } from './toast';\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n  id: string;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  action?: ToastActionElement;\n};\n\nconst actionTypes = {\n  ADD_TOAST: 'ADD_TOAST',\n  UPDATE_TOAST: 'UPDATE_TOAST',\n  DISMISS_TOAST: 'DISMISS_TOAST',\n  REMOVE_TOAST: 'REMOVE_TOAST',\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER;\n  return count.toString();\n}\n\ntype ActionType = typeof actionTypes;\n\ntype Action =\n  | {\n      type: ActionType['ADD_TOAST'];\n      toast: ToasterToast;\n    }\n  | {\n      type: ActionType['UPDATE_TOAST'];\n      toast: Partial<ToasterToast>;\n    }\n  | {\n      type: ActionType['DISMISS_TOAST'];\n      toastId?: ToasterToast['id'];\n    }\n  | {\n      type: ActionType['REMOVE_TOAST'];\n      toastId?: ToasterToast['id'];\n    };\n\ninterface State {\n  toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return;\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId);\n    dispatch({\n      type: 'REMOVE_TOAST',\n      toastId: toastId,\n    });\n  }, TOAST_REMOVE_DELAY);\n\n  toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case 'ADD_TOAST':\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      };\n\n    case 'UPDATE_TOAST':\n      return {\n        ...state,\n        toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),\n      };\n\n    case 'DISMISS_TOAST': {\n      const { toastId } = action;\n\n      if (toastId) {\n        addToRemoveQueue(toastId);\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id);\n        });\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t,\n        ),\n      };\n    }\n    case 'REMOVE_TOAST':\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        };\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      };\n  }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action);\n  listeners.forEach((listener) => {\n    listener(memoryState);\n  });\n}\n\ntype Toast = Omit<ToasterToast, 'id'>;\n\nfunction toast({ ...props }: Toast) {\n  const id = genId();\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: 'UPDATE_TOAST',\n      toast: { ...props, id },\n    });\n  const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });\n\n  dispatch({\n    type: 'ADD_TOAST',\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss();\n      },\n    },\n  });\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  };\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState);\n\n  React.useEffect(() => {\n    listeners.push(setState);\n    return () => {\n      const index = listeners.indexOf(setState);\n      if (index > -1) {\n        listeners.splice(index, 1);\n      }\n    };\n  }, []);\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),\n  };\n}\n\nexport { useToast, toast };\n"
  },
  {
    "path": "app/src/global.d.ts",
    "content": "interface Window {\n  __voiceboxServerStartedByApp?: boolean;\n}\n\ndeclare module 'virtual:changelog' {\n  const raw: string;\n  export default raw;\n}\n"
  },
  {
    "path": "app/src/hooks/useAutoUpdater.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from 'react';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport type { UpdateStatus } from '@/platform/types';\n\n// Re-export UpdateStatus for backwards compatibility\nexport type { UpdateStatus };\n\nexport function useAutoUpdater(checkOnMount = false) {\n  const platform = usePlatform();\n  const [status, setStatus] = useState<UpdateStatus>(platform.updater.getStatus());\n  const hasCheckedRef = useRef(false);\n\n  // Subscribe to updater status changes\n  useEffect(() => {\n    const unsubscribe = platform.updater.subscribe((newStatus) => {\n      setStatus(newStatus);\n    });\n    return unsubscribe;\n    // Empty dependency array - platform is stable from context\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.updater.subscribe]);\n\n  const checkForUpdates = useCallback(async () => {\n    await platform.updater.checkForUpdates();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.updater.checkForUpdates]);\n\n  const downloadAndInstall = useCallback(async () => {\n    await platform.updater.downloadAndInstall();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.updater.downloadAndInstall]);\n\n  const restartAndInstall = useCallback(async () => {\n    await platform.updater.restartAndInstall();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.updater.restartAndInstall]);\n\n  useEffect(() => {\n    if (checkOnMount && platform.metadata.isTauri && !hasCheckedRef.current) {\n      hasCheckedRef.current = true;\n      checkForUpdates();\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.metadata.isTauricheckOnMountcheckForUpdates]);\n\n  return {\n    status,\n    checkForUpdates,\n    downloadAndInstall,\n    restartAndInstall,\n  };\n}\n"
  },
  {
    "path": "app/src/hooks/useAutoUpdater.tsx",
    "content": "import { Download, RefreshCw } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Progress } from '@/components/ui/progress';\nimport { ToastAction } from '@/components/ui/toast';\nimport { useToast } from '@/components/ui/use-toast';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport type { UpdateStatus } from '@/platform/types';\n\n// Re-export UpdateStatus for backwards compatibility\nexport type { UpdateStatus };\n\ninterface UseAutoUpdaterOptions {\n  checkOnMount?: boolean;\n  showToast?: boolean;\n}\n\nexport function useAutoUpdater(options: boolean | UseAutoUpdaterOptions = false) {\n  // Support both old boolean API and new options object\n  const { checkOnMount, showToast } =\n    typeof options === 'boolean'\n      ? { checkOnMount: options, showToast: false }\n      : { checkOnMount: options.checkOnMount ?? false, showToast: options.showToast ?? false };\n\n  const platform = usePlatform();\n  const { toast } = useToast();\n  const [status, setStatus] = useState<UpdateStatus>(platform.updater.getStatus());\n  const hasCheckedRef = useRef(false);\n  const toastIdRef = useRef<string | null>(null);\n  const toastUpdateRef = useRef<\n    | ((props: {\n        title?: React.ReactNode;\n        description?: React.ReactNode;\n        duration?: number;\n        variant?: 'default' | 'destructive';\n        open?: boolean;\n        action?: React.ReactElement<typeof ToastAction>;\n      }) => void)\n    | null\n  >(null);\n\n  // Subscribe to updater status changes\n  useEffect(() => {\n    const unsubscribe = platform.updater.subscribe((newStatus) => {\n      setStatus(newStatus);\n    });\n    return unsubscribe;\n    // Empty dependency array - platform is stable from context\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.updater.subscribe]);\n\n  const checkForUpdates = useCallback(async () => {\n    await platform.updater.checkForUpdates();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.updater.checkForUpdates]);\n\n  const downloadAndInstall = useCallback(async () => {\n    await platform.updater.downloadAndInstall();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.updater.downloadAndInstall]);\n\n  const restartAndInstall = useCallback(async () => {\n    await platform.updater.restartAndInstall();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.updater.restartAndInstall]);\n\n  // Check for updates on mount\n  useEffect(() => {\n    if (checkOnMount && platform.metadata.isTauri && !hasCheckedRef.current) {\n      hasCheckedRef.current = true;\n      checkForUpdates().catch((error) => {\n        console.error('Auto update check failed:', error);\n      });\n    }\n    // Empty dependency array - only run once on mount\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [platform.metadata.isTauricheckOnMountcheckForUpdates]);\n\n  // Show toast when update is available\n  useEffect(() => {\n    if (\n      !showToast ||\n      !status.available ||\n      status.downloading ||\n      status.readyToInstall ||\n      toastIdRef.current\n    ) {\n      return;\n    }\n\n    const handleUpdateNow = async () => {\n      await downloadAndInstall();\n    };\n\n    const toastResult = toast({\n      title: 'Update Available',\n      description: `Version ${status.version} is ready to download.`,\n      duration: Infinity,\n      action: (\n        <ToastAction altText=\"Update now\" onClick={handleUpdateNow}>\n          Update Now\n        </ToastAction>\n      ),\n    });\n\n    toastIdRef.current = toastResult.id;\n    // Type assertion needed because update function has broader type than our ref\n    toastUpdateRef.current = toastResult.update as typeof toastUpdateRef.current;\n  }, [\n    showToast,\n    status.available,\n    status.downloading,\n    status.readyToInstall,\n    status.version,\n    downloadAndInstall,\n    toast,\n  ]);\n\n  // Update toast when downloading\n  useEffect(() => {\n    if (!showToast || !status.downloading || !toastIdRef.current || !toastUpdateRef.current) {\n      return;\n    }\n\n    const progressPercent = status.downloadProgress || 0;\n    const progressText =\n      status.downloadedBytes !== undefined &&\n      status.totalBytes !== undefined &&\n      status.totalBytes > 0\n        ? `${(status.downloadedBytes / 1024 / 1024).toFixed(1)} MB / ${(status.totalBytes / 1024 / 1024).toFixed(1)} MB`\n        : '';\n\n    toastUpdateRef.current({\n      title: (\n        <div className=\"flex items-center gap-2\">\n          <Download className=\"h-4 w-4 animate-pulse\" />\n          <span>Downloading Update</span>\n        </div>\n      ),\n      description: (\n        <div className=\"space-y-2\">\n          <div className=\"text-sm\">Version {status.version}</div>\n          {progressPercent > 0 && (\n            <>\n              <Progress value={progressPercent} className=\"h-2\" />\n              {progressText && <div className=\"text-xs text-muted-foreground\">{progressText}</div>}\n            </>\n          )}\n        </div>\n      ),\n      duration: Infinity,\n    });\n  }, [\n    showToast,\n    status.downloading,\n    status.downloadProgress,\n    status.downloadedBytes,\n    status.totalBytes,\n    status.version,\n  ]);\n\n  // Update toast when ready to install\n  useEffect(() => {\n    if (!showToast || !status.readyToInstall || !toastIdRef.current || !toastUpdateRef.current) {\n      return;\n    }\n\n    const handleRestartNow = async () => {\n      await restartAndInstall();\n    };\n\n    toastUpdateRef.current({\n      title: 'Update Ready',\n      description: `Version ${status.version} has been downloaded and is ready to install.`,\n      duration: Infinity,\n      action: (\n        <ToastAction altText=\"Restart now\" onClick={handleRestartNow}>\n          <RefreshCw className=\"h-3 w-3 mr-1\" />\n          Restart Now\n        </ToastAction>\n      ),\n    });\n  }, [showToast, status.readyToInstall, status.version, restartAndInstall]);\n\n  // Handle errors in toast\n  useEffect(() => {\n    if (!showToast || !status.error || !toastIdRef.current || !toastUpdateRef.current) {\n      return;\n    }\n\n    toastUpdateRef.current({\n      title: 'Update Failed',\n      description: status.error,\n      variant: 'destructive',\n      duration: 5000,\n    });\n\n    setTimeout(() => {\n      toastIdRef.current = null;\n      toastUpdateRef.current = null;\n    }, 5000);\n  }, [showToast, status.error]);\n\n  return {\n    status,\n    checkForUpdates,\n    downloadAndInstall,\n    restartAndInstall,\n  };\n}\n"
  },
  {
    "path": "app/src/index.css",
    "content": "@import \"tailwindcss\" source(\".\");\n@import \"loaders.css/loaders.min.css\";\n\n@theme {\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n\n  --color-background: hsl(var(--background));\n  --color-foreground: hsl(var(--foreground));\n\n  --color-card: hsl(var(--card));\n  --color-card-foreground: hsl(var(--card-foreground));\n\n  --color-popover: hsl(var(--popover));\n  --color-popover-foreground: hsl(var(--popover-foreground));\n\n  --color-primary: hsl(var(--primary));\n  --color-primary-foreground: hsl(var(--primary-foreground));\n\n  --color-secondary: hsl(var(--secondary));\n  --color-secondary-foreground: hsl(var(--secondary-foreground));\n\n  --color-muted: hsl(var(--muted));\n  --color-muted-foreground: hsl(var(--muted-foreground));\n\n  --color-accent: hsl(var(--accent));\n  --color-accent-foreground: hsl(var(--accent-foreground));\n\n  --color-destructive: hsl(var(--destructive));\n  --color-destructive-foreground: hsl(var(--destructive-foreground));\n\n  --color-border: hsl(var(--border));\n  --color-input: hsl(var(--input));\n  --color-ring: hsl(var(--ring));\n  --color-sidebar: hsl(var(--sidebar));\n\n  --color-chart-1: hsl(var(--chart-1));\n  --color-chart-2: hsl(var(--chart-2));\n  --color-chart-3: hsl(var(--chart-3));\n  --color-chart-4: hsl(var(--chart-4));\n  --color-chart-5: hsl(var(--chart-5));\n}\n\n:root {\n  --background: 0 0% 95%;\n  --foreground: 222.2 84% 4.9%;\n  --card: 0 0% 97%;\n  --card-foreground: 222.2 84% 4.9%;\n  --popover: 0 0% 97%;\n  --popover-foreground: 222.2 84% 4.9%;\n  --primary: 222.2 47.4% 11.2%;\n  --primary-foreground: 210 40% 98%;\n  --secondary: 210 40% 92%;\n  --secondary-foreground: 222.2 47.4% 11.2%;\n  --muted: 210 40% 90%;\n  --muted-foreground: 215.4 16.3% 46.9%;\n  --accent: 43 50% 50%;\n  --accent-foreground: 222.2 47.4% 11.2%;\n  --destructive: 0 84.2% 60.2%;\n  --destructive-foreground: 210 40% 98%;\n  --border: 214.3 31.8% 85%;\n  --input: 214.3 31.8% 88%;\n  --ring: 222.2 84% 4.9%;\n  --sidebar: 0 0% 92%;\n  --radius: 0.5rem;\n  --chart-1: 12 76% 61%;\n  --chart-2: 173 58% 39%;\n  --chart-3: 197 37% 24%;\n  --chart-4: 43 74% 66%;\n  --chart-5: 27 87% 67%;\n}\n\n.dark {\n  --background: 0 0% 6%;\n  --foreground: 0 0% 95%;\n  --card: 0 0% 8%;\n  --card-foreground: 0 0% 95%;\n  --popover: 0 0% 8%;\n  --popover-foreground: 0 0% 95%;\n  --primary: 0 0% 18%;\n  --primary-foreground: 0 0% 95%;\n  --secondary: 0 0% 12%;\n  --secondary-foreground: 0 0% 95%;\n  --muted: 0 0% 12%;\n  --muted-foreground: 0 0% 60%;\n  --accent: 43 50% 45%;\n  --accent-foreground: 0 0% 95%;\n  --destructive: 0 62.8% 50%;\n  --destructive-foreground: 0 0% 95%;\n  --border: 0 0% 12%;\n  --input: 0 0% 12%;\n  --ring: 0 0% 40%;\n  --sidebar: 0 0% 4%;\n  --chart-1: 220 70% 50%;\n  --chart-2: 160 60% 45%;\n  --chart-3: 30 80% 55%;\n  --chart-4: 280 65% 60%;\n  --chart-5: 340 75% 55%;\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  html,\n  body {\n    @apply overflow-hidden;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n  /* Hide scrollbars globally */\n  * {\n    scrollbar-width: none; /* Firefox */\n    -ms-overflow-style: none; /* IE and Edge */\n  }\n  *::-webkit-scrollbar {\n    display: none; /* Chrome, Safari, Opera */\n  }\n}\n\n@layer utilities {\n  .writing-vertical {\n    writing-mode: vertical-rl;\n    text-orientation: mixed;\n    letter-spacing: 0.1em;\n  }\n}\n\n@keyframes fadeInScale {\n  from {\n    opacity: 0;\n    transform: scale(0.8);\n  }\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n.animate-fade-in-scale {\n  animation: fadeInScale 0.5s ease-out forwards;\n}\n\n.animate-fade-in-delayed {\n  animation: fadeIn 0.5s ease-out 0.15s forwards;\n  opacity: 0;\n}\n\n/* react-loaders */\n.line-scale-pulse-out-rapid > div,\n.line-scale > div {\n  background-color: hsl(var(--accent)) !important;\n}\n\n.loader-hidden {\n  display: block;\n}\n\n.loader-hidden > div > div {\n  animation-play-state: paused !important;\n  background-color: hsl(var(--muted-foreground)) !important;\n}\n"
  },
  {
    "path": "app/src/lib/api/.gitkeep",
    "content": "# Generated OpenAPI client will be placed here\n"
  },
  {
    "path": "app/src/lib/api/client.ts",
    "content": "import type { LanguageCode } from '@/lib/constants/languages';\nimport { useServerStore } from '@/stores/serverStore';\nimport type {\n  ActiveTasksResponse,\n  ApplyEffectsRequest,\n  AvailableEffectsResponse,\n  CudaStatus,\n  EffectConfig,\n  EffectPresetCreate,\n  EffectPresetResponse,\n  GenerationRequest,\n  GenerationResponse,\n  GenerationVersionResponse,\n  HealthResponse,\n  HistoryListResponse,\n  HistoryQuery,\n  HistoryResponse,\n  ModelDownloadRequest,\n  ModelStatusListResponse,\n  PresetVoice,\n  ProfileSampleResponse,\n  StoryCreate,\n  StoryDetailResponse,\n  StoryItemBatchUpdate,\n  StoryItemCreate,\n  StoryItemDetail,\n  StoryItemMove,\n  StoryItemReorder,\n  StoryItemSplit,\n  StoryItemTrim,\n  StoryItemVersionUpdate,\n  StoryResponse,\n  TranscriptionResponse,\n  VoiceProfileCreate,\n  VoiceProfileResponse,\n  WhisperModelSize,\n} from './types';\n\nfunction formatErrorDetail(detail: unknown, fallback: string): string {\n  if (typeof detail === 'string') return detail;\n  if (Array.isArray(detail)) {\n    return detail\n      .map((e: Record<string, unknown>) => e.msg || e.message || JSON.stringify(e))\n      .join('; ');\n  }\n  if (detail && typeof detail === 'object') {\n    const obj = detail as Record<string, unknown>;\n    if (typeof obj.message === 'string') return obj.message;\n    return JSON.stringify(detail);\n  }\n  return fallback;\n}\n\nclass ApiClient {\n  private getBaseUrl(): string {\n    const serverUrl = useServerStore.getState().serverUrl;\n    return serverUrl;\n  }\n\n  private async request<T>(endpoint: string, options?: RequestInit): Promise<T> {\n    const url = `${this.getBaseUrl()}${endpoint}`;\n    const response = await fetch(url, {\n      ...options,\n      headers: {\n        'Content-Type': 'application/json',\n        ...options?.headers,\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.json();\n  }\n\n  // Health\n  async getHealth(): Promise<HealthResponse> {\n    return this.request<HealthResponse>('/health');\n  }\n\n  // Profiles\n  async createProfile(data: VoiceProfileCreate): Promise<VoiceProfileResponse> {\n    return this.request<VoiceProfileResponse>('/profiles', {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async listProfiles(): Promise<VoiceProfileResponse[]> {\n    return this.request<VoiceProfileResponse[]>('/profiles');\n  }\n\n  async getProfile(profileId: string): Promise<VoiceProfileResponse> {\n    return this.request<VoiceProfileResponse>(`/profiles/${profileId}`);\n  }\n\n  async listPresetVoices(engine: string): Promise<{ engine: string; voices: PresetVoice[] }> {\n    return this.request<{ engine: string; voices: PresetVoice[] }>(`/profiles/presets/${engine}`);\n  }\n\n  async seedPresetProfiles(\n    engine: string,\n  ): Promise<{ engine: string; created: number; total_available: number }> {\n    return this.request(`/profiles/presets/${engine}/seed`, { method: 'POST' });\n  }\n\n  async updateProfile(profileId: string, data: VoiceProfileCreate): Promise<VoiceProfileResponse> {\n    return this.request<VoiceProfileResponse>(`/profiles/${profileId}`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async deleteProfile(profileId: string): Promise<void> {\n    await this.request<void>(`/profiles/${profileId}`, {\n      method: 'DELETE',\n    });\n  }\n\n  async addProfileSample(\n    profileId: string,\n    file: File,\n    referenceText: string,\n  ): Promise<ProfileSampleResponse> {\n    const url = `${this.getBaseUrl()}/profiles/${profileId}/samples`;\n    const formData = new FormData();\n    formData.append('file', file);\n    formData.append('reference_text', referenceText);\n\n    const response = await fetch(url, {\n      method: 'POST',\n      body: formData,\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.json();\n  }\n\n  async listProfileSamples(profileId: string): Promise<ProfileSampleResponse[]> {\n    return this.request<ProfileSampleResponse[]>(`/profiles/${profileId}/samples`);\n  }\n\n  async deleteProfileSample(sampleId: string): Promise<void> {\n    await this.request<void>(`/profiles/samples/${sampleId}`, {\n      method: 'DELETE',\n    });\n  }\n\n  async updateProfileSample(\n    sampleId: string,\n    referenceText: string,\n  ): Promise<ProfileSampleResponse> {\n    return this.request<ProfileSampleResponse>(`/profiles/samples/${sampleId}`, {\n      method: 'PUT',\n      body: JSON.stringify({ reference_text: referenceText }),\n    });\n  }\n\n  async exportProfile(profileId: string): Promise<Blob> {\n    const url = `${this.getBaseUrl()}/profiles/${profileId}/export`;\n    const response = await fetch(url);\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.blob();\n  }\n\n  async importProfile(file: File): Promise<VoiceProfileResponse> {\n    const url = `${this.getBaseUrl()}/profiles/import`;\n    const formData = new FormData();\n    formData.append('file', file);\n\n    const response = await fetch(url, {\n      method: 'POST',\n      body: formData,\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.json();\n  }\n\n  async uploadAvatar(profileId: string, file: File): Promise<VoiceProfileResponse> {\n    const url = `${this.getBaseUrl()}/profiles/${profileId}/avatar`;\n    const formData = new FormData();\n    formData.append('file', file);\n\n    const response = await fetch(url, {\n      method: 'POST',\n      body: formData,\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.json();\n  }\n\n  async deleteAvatar(profileId: string): Promise<void> {\n    await this.request<void>(`/profiles/${profileId}/avatar`, {\n      method: 'DELETE',\n    });\n  }\n\n  // Generation\n  async generateSpeech(data: GenerationRequest): Promise<GenerationResponse> {\n    return this.request<GenerationResponse>('/generate', {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async retryGeneration(generationId: string): Promise<GenerationResponse> {\n    return this.request<GenerationResponse>(`/generate/${generationId}/retry`, {\n      method: 'POST',\n    });\n  }\n\n  async regenerateGeneration(generationId: string): Promise<GenerationResponse> {\n    return this.request<GenerationResponse>(`/generate/${generationId}/regenerate`, {\n      method: 'POST',\n    });\n  }\n\n  async toggleFavorite(generationId: string): Promise<{ is_favorited: boolean }> {\n    return this.request<{ is_favorited: boolean }>(`/history/${generationId}/favorite`, {\n      method: 'POST',\n    });\n  }\n\n  // History\n  async listHistory(query?: HistoryQuery): Promise<HistoryListResponse> {\n    const params = new URLSearchParams();\n    if (query?.profile_id) params.append('profile_id', query.profile_id);\n    if (query?.search) params.append('search', query.search);\n    if (query?.limit) params.append('limit', query.limit.toString());\n    if (query?.offset) params.append('offset', query.offset.toString());\n\n    const queryString = params.toString();\n    const endpoint = queryString ? `/history?${queryString}` : '/history';\n\n    return this.request<HistoryListResponse>(endpoint);\n  }\n\n  async getGeneration(generationId: string): Promise<HistoryResponse> {\n    return this.request<HistoryResponse>(`/history/${generationId}`);\n  }\n\n  async deleteGeneration(generationId: string): Promise<void> {\n    await this.request<void>(`/history/${generationId}`, {\n      method: 'DELETE',\n    });\n  }\n\n  async exportGeneration(generationId: string): Promise<Blob> {\n    const url = `${this.getBaseUrl()}/history/${generationId}/export`;\n    const response = await fetch(url);\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.blob();\n  }\n\n  async exportGenerationAudio(generationId: string): Promise<Blob> {\n    const url = `${this.getBaseUrl()}/history/${generationId}/export-audio`;\n    const response = await fetch(url);\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.blob();\n  }\n\n  async importGeneration(file: File): Promise<{\n    id: string;\n    profile_id: string;\n    profile_name: string;\n    text: string;\n    message: string;\n  }> {\n    const url = `${this.getBaseUrl()}/history/import`;\n    const formData = new FormData();\n    formData.append('file', file);\n\n    const response = await fetch(url, {\n      method: 'POST',\n      body: formData,\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.json();\n  }\n\n  // Generation status SSE\n  getGenerationStatusUrl(generationId: string): string {\n    return `${this.getBaseUrl()}/generate/${generationId}/status`;\n  }\n\n  // Audio\n  getAudioUrl(audioId: string): string {\n    return `${this.getBaseUrl()}/audio/${audioId}`;\n  }\n\n  getSampleUrl(sampleId: string): string {\n    return `${this.getBaseUrl()}/samples/${sampleId}`;\n  }\n\n  // Transcription\n  async transcribeAudio(\n    file: File,\n    language?: LanguageCode,\n    model?: WhisperModelSize,\n  ): Promise<TranscriptionResponse> {\n    const formData = new FormData();\n    formData.append('file', file);\n    if (language) {\n      formData.append('language', language);\n    }\n    if (model) {\n      formData.append('model', model);\n    }\n\n    const url = `${this.getBaseUrl()}/transcribe`;\n    const response = await fetch(url, {\n      method: 'POST',\n      body: formData,\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.json();\n  }\n\n  // Model Management\n  async getModelStatus(): Promise<ModelStatusListResponse> {\n    return this.request<ModelStatusListResponse>('/models/status');\n  }\n\n  async getModelsCacheDir(): Promise<{ path: string }> {\n    return this.request<{ path: string }>('/models/cache-dir');\n  }\n\n  async migrateModels(destination: string): Promise<{ source: string; destination: string }> {\n    return this.request('/models/migrate', {\n      method: 'POST',\n      body: JSON.stringify({ destination }),\n    });\n  }\n\n  getMigrationProgressUrl(): string {\n    return `${this.getBaseUrl()}/models/migrate/progress`;\n  }\n\n  async triggerModelDownload(modelName: string): Promise<{ message: string }> {\n    console.log(\n      '[API] triggerModelDownload called for:',\n      modelName,\n      'at',\n      new Date().toISOString(),\n    );\n    const result = await this.request<{ message: string }>('/models/download', {\n      method: 'POST',\n      body: JSON.stringify({ model_name: modelName } as ModelDownloadRequest),\n    });\n    console.log('[API] triggerModelDownload response:', result);\n    return result;\n  }\n\n  async deleteModel(modelName: string): Promise<{ message: string }> {\n    return this.request<{ message: string }>(`/models/${modelName}`, {\n      method: 'DELETE',\n    });\n  }\n\n  async unloadModel(modelName: string): Promise<{ message: string }> {\n    return this.request<{ message: string }>(`/models/${modelName}/unload`, {\n      method: 'POST',\n    });\n  }\n\n  async cancelDownload(modelName: string): Promise<{ message: string }> {\n    return this.request<{ message: string }>('/models/download/cancel', {\n      method: 'POST',\n      body: JSON.stringify({ model_name: modelName } as ModelDownloadRequest),\n    });\n  }\n\n  // Task Management\n  async getActiveTasks(): Promise<ActiveTasksResponse> {\n    return this.request<ActiveTasksResponse>('/tasks/active');\n  }\n\n  async clearAllTasks(): Promise<{ message: string }> {\n    return this.request<{ message: string }>('/tasks/clear', { method: 'POST' });\n  }\n\n  // Audio Channels\n  async listChannels(): Promise<\n    Array<{\n      id: string;\n      name: string;\n      is_default: boolean;\n      device_ids: string[];\n      created_at: string;\n    }>\n  > {\n    return this.request('/channels');\n  }\n\n  async createChannel(data: { name: string; device_ids: string[] }): Promise<{\n    id: string;\n    name: string;\n    is_default: boolean;\n    device_ids: string[];\n    created_at: string;\n  }> {\n    return this.request('/channels', {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async updateChannel(\n    channelId: string,\n    data: {\n      name?: string;\n      device_ids?: string[];\n    },\n  ): Promise<{\n    id: string;\n    name: string;\n    is_default: boolean;\n    device_ids: string[];\n    created_at: string;\n  }> {\n    return this.request(`/channels/${channelId}`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async deleteChannel(channelId: string): Promise<{ message: string }> {\n    return this.request(`/channels/${channelId}`, {\n      method: 'DELETE',\n    });\n  }\n\n  async getChannelVoices(channelId: string): Promise<{ profile_ids: string[] }> {\n    return this.request(`/channels/${channelId}/voices`);\n  }\n\n  async setChannelVoices(channelId: string, profileIds: string[]): Promise<{ message: string }> {\n    return this.request(`/channels/${channelId}/voices`, {\n      method: 'PUT',\n      body: JSON.stringify({ profile_ids: profileIds }),\n    });\n  }\n\n  async getProfileChannels(profileId: string): Promise<{ channel_ids: string[] }> {\n    return this.request(`/profiles/${profileId}/channels`);\n  }\n\n  async setProfileChannels(profileId: string, channelIds: string[]): Promise<{ message: string }> {\n    return this.request(`/profiles/${profileId}/channels`, {\n      method: 'PUT',\n      body: JSON.stringify({ channel_ids: channelIds }),\n    });\n  }\n\n  // CUDA Backend Management\n  async getCudaStatus(): Promise<CudaStatus> {\n    return this.request<CudaStatus>('/backend/cuda-status');\n  }\n\n  async downloadCudaBackend(): Promise<{ message: string; progress_key: string }> {\n    return this.request<{ message: string; progress_key: string }>('/backend/download-cuda', {\n      method: 'POST',\n    });\n  }\n\n  async deleteCudaBackend(): Promise<{ message: string }> {\n    return this.request<{ message: string }>('/backend/cuda', {\n      method: 'DELETE',\n    });\n  }\n\n  // Stories\n  async listStories(): Promise<StoryResponse[]> {\n    return this.request<StoryResponse[]>('/stories');\n  }\n\n  async createStory(data: StoryCreate): Promise<StoryResponse> {\n    return this.request<StoryResponse>('/stories', {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async getStory(storyId: string): Promise<StoryDetailResponse> {\n    return this.request<StoryDetailResponse>(`/stories/${storyId}`);\n  }\n\n  async updateStory(storyId: string, data: StoryCreate): Promise<StoryResponse> {\n    return this.request<StoryResponse>(`/stories/${storyId}`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async deleteStory(storyId: string): Promise<void> {\n    await this.request<void>(`/stories/${storyId}`, {\n      method: 'DELETE',\n    });\n  }\n\n  async addStoryItem(storyId: string, data: StoryItemCreate): Promise<StoryItemDetail> {\n    return this.request<StoryItemDetail>(`/stories/${storyId}/items`, {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async removeStoryItem(storyId: string, itemId: string): Promise<void> {\n    await this.request<void>(`/stories/${storyId}/items/${itemId}`, {\n      method: 'DELETE',\n    });\n  }\n\n  async updateStoryItemTimes(storyId: string, data: StoryItemBatchUpdate): Promise<void> {\n    await this.request<void>(`/stories/${storyId}/items/times`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async reorderStoryItems(storyId: string, data: StoryItemReorder): Promise<StoryItemDetail[]> {\n    return this.request<StoryItemDetail[]>(`/stories/${storyId}/items/reorder`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async moveStoryItem(\n    storyId: string,\n    itemId: string,\n    data: StoryItemMove,\n  ): Promise<StoryItemDetail> {\n    return this.request<StoryItemDetail>(`/stories/${storyId}/items/${itemId}/move`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async trimStoryItem(\n    storyId: string,\n    itemId: string,\n    data: StoryItemTrim,\n  ): Promise<StoryItemDetail> {\n    return this.request<StoryItemDetail>(`/stories/${storyId}/items/${itemId}/trim`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async splitStoryItem(\n    storyId: string,\n    itemId: string,\n    data: StoryItemSplit,\n  ): Promise<StoryItemDetail[]> {\n    return this.request<StoryItemDetail[]>(`/stories/${storyId}/items/${itemId}/split`, {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async duplicateStoryItem(storyId: string, itemId: string): Promise<StoryItemDetail> {\n    return this.request<StoryItemDetail>(`/stories/${storyId}/items/${itemId}/duplicate`, {\n      method: 'POST',\n    });\n  }\n\n  async setStoryItemVersion(\n    storyId: string,\n    itemId: string,\n    data: StoryItemVersionUpdate,\n  ): Promise<StoryItemDetail> {\n    return this.request<StoryItemDetail>(`/stories/${storyId}/items/${itemId}/version`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async exportStoryAudio(storyId: string): Promise<Blob> {\n    const url = `${this.getBaseUrl()}/stories/${storyId}/export-audio`;\n    const response = await fetch(url);\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.blob();\n  }\n\n  // Effects & Versions\n  async getAvailableEffects(): Promise<AvailableEffectsResponse> {\n    return this.request<AvailableEffectsResponse>('/effects/available');\n  }\n\n  async listEffectPresets(): Promise<EffectPresetResponse[]> {\n    return this.request<EffectPresetResponse[]>('/effects/presets');\n  }\n\n  async createEffectPreset(data: EffectPresetCreate): Promise<EffectPresetResponse> {\n    return this.request<EffectPresetResponse>('/effects/presets', {\n      method: 'POST',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async updateEffectPreset(\n    presetId: string,\n    data: { name?: string; description?: string; effects_chain?: EffectConfig[] },\n  ): Promise<EffectPresetResponse> {\n    return this.request<EffectPresetResponse>(`/effects/presets/${presetId}`, {\n      method: 'PUT',\n      body: JSON.stringify(data),\n    });\n  }\n\n  async deleteEffectPreset(presetId: string): Promise<void> {\n    await this.request<void>(`/effects/presets/${presetId}`, {\n      method: 'DELETE',\n    });\n  }\n\n  async listGenerationVersions(generationId: string): Promise<GenerationVersionResponse[]> {\n    return this.request<GenerationVersionResponse[]>(`/generations/${generationId}/versions`);\n  }\n\n  async applyEffectsToGeneration(\n    generationId: string,\n    data: ApplyEffectsRequest,\n  ): Promise<GenerationVersionResponse> {\n    return this.request<GenerationVersionResponse>(\n      `/generations/${generationId}/versions/apply-effects`,\n      {\n        method: 'POST',\n        body: JSON.stringify(data),\n      },\n    );\n  }\n\n  async setDefaultVersion(\n    generationId: string,\n    versionId: string,\n  ): Promise<GenerationVersionResponse> {\n    return this.request<GenerationVersionResponse>(\n      `/generations/${generationId}/versions/${versionId}/set-default`,\n      { method: 'PUT' },\n    );\n  }\n\n  async deleteGenerationVersion(generationId: string, versionId: string): Promise<void> {\n    await this.request<void>(`/generations/${generationId}/versions/${versionId}`, {\n      method: 'DELETE',\n    });\n  }\n\n  getVersionAudioUrl(versionId: string): string {\n    return `${this.getBaseUrl()}/audio/version/${versionId}`;\n  }\n\n  async updateProfileEffects(\n    profileId: string,\n    effectsChain: EffectConfig[] | null,\n  ): Promise<VoiceProfileResponse> {\n    return this.request<VoiceProfileResponse>(`/profiles/${profileId}/effects`, {\n      method: 'PUT',\n      body: JSON.stringify({ effects_chain: effectsChain }),\n    });\n  }\n\n  async previewEffects(generationId: string, effectsChain: EffectConfig[]): Promise<Blob> {\n    const url = `${this.getBaseUrl()}/effects/preview/${generationId}`;\n    const response = await fetch(url, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ effects_chain: effectsChain }),\n    });\n\n    if (!response.ok) {\n      const error = await response.json().catch(() => ({\n        detail: response.statusText,\n      }));\n      throw new Error(formatErrorDetail(error.detail, `HTTP error! status: ${response.status}`));\n    }\n\n    return response.blob();\n  }\n}\n\nexport const apiClient = new ApiClient();\n"
  },
  {
    "path": "app/src/lib/api/core/ApiError.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nimport type { ApiRequestOptions } from './ApiRequestOptions';\nimport type { ApiResult } from './ApiResult';\n\nexport class ApiError extends Error {\n  public readonly url: string;\n  public readonly status: number;\n  public readonly statusText: string;\n  public readonly body: any;\n  public readonly request: ApiRequestOptions;\n\n  constructor(request: ApiRequestOptions, response: ApiResult, message: string) {\n    super(message);\n\n    this.name = 'ApiError';\n    this.url = response.url;\n    this.status = response.status;\n    this.statusText = response.statusText;\n    this.body = response.body;\n    this.request = request;\n  }\n}\n"
  },
  {
    "path": "app/src/lib/api/core/ApiRequestOptions.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport type ApiRequestOptions = {\n  readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';\n  readonly url: string;\n  readonly path?: Record<string, any>;\n  readonly cookies?: Record<string, any>;\n  readonly headers?: Record<string, any>;\n  readonly query?: Record<string, any>;\n  readonly formData?: Record<string, any>;\n  readonly body?: any;\n  readonly mediaType?: string;\n  readonly responseHeader?: string;\n  readonly errors?: Record<number, string>;\n};\n"
  },
  {
    "path": "app/src/lib/api/core/ApiResult.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport type ApiResult = {\n  readonly url: string;\n  readonly ok: boolean;\n  readonly status: number;\n  readonly statusText: string;\n  readonly body: any;\n};\n"
  },
  {
    "path": "app/src/lib/api/core/CancelablePromise.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport class CancelError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = 'CancelError';\n  }\n\n  public get isCancelled(): boolean {\n    return true;\n  }\n}\n\nexport interface OnCancel {\n  readonly isResolved: boolean;\n  readonly isRejected: boolean;\n  readonly isCancelled: boolean;\n\n  (cancelHandler: () => void): void;\n}\n\nexport class CancelablePromise<T> implements Promise<T> {\n  #isResolved: boolean;\n  #isRejected: boolean;\n  #isCancelled: boolean;\n  readonly #cancelHandlers: (() => void)[];\n  readonly #promise: Promise<T>;\n  #resolve?: (value: T | PromiseLike<T>) => void;\n  #reject?: (reason?: any) => void;\n\n  constructor(\n    executor: (\n      resolve: (value: T | PromiseLike<T>) => void,\n      reject: (reason?: any) => void,\n      onCancel: OnCancel,\n    ) => void,\n  ) {\n    this.#isResolved = false;\n    this.#isRejected = false;\n    this.#isCancelled = false;\n    this.#cancelHandlers = [];\n    this.#promise = new Promise<T>((resolve, reject) => {\n      this.#resolve = resolve;\n      this.#reject = reject;\n\n      const onResolve = (value: T | PromiseLike<T>): void => {\n        if (this.#isResolved || this.#isRejected || this.#isCancelled) {\n          return;\n        }\n        this.#isResolved = true;\n        if (this.#resolve) this.#resolve(value);\n      };\n\n      const onReject = (reason?: any): void => {\n        if (this.#isResolved || this.#isRejected || this.#isCancelled) {\n          return;\n        }\n        this.#isRejected = true;\n        if (this.#reject) this.#reject(reason);\n      };\n\n      const onCancel = (cancelHandler: () => void): void => {\n        if (this.#isResolved || this.#isRejected || this.#isCancelled) {\n          return;\n        }\n        this.#cancelHandlers.push(cancelHandler);\n      };\n\n      Object.defineProperty(onCancel, 'isResolved', {\n        get: (): boolean => this.#isResolved,\n      });\n\n      Object.defineProperty(onCancel, 'isRejected', {\n        get: (): boolean => this.#isRejected,\n      });\n\n      Object.defineProperty(onCancel, 'isCancelled', {\n        get: (): boolean => this.#isCancelled,\n      });\n\n      return executor(onResolve, onReject, onCancel as OnCancel);\n    });\n  }\n\n  get [Symbol.toStringTag]() {\n    return 'Cancellable Promise';\n  }\n\n  public then<TResult1 = T, TResult2 = never>(\n    onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,\n    onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,\n  ): Promise<TResult1 | TResult2> {\n    return this.#promise.then(onFulfilled, onRejected);\n  }\n\n  public catch<TResult = never>(\n    onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null,\n  ): Promise<T | TResult> {\n    return this.#promise.catch(onRejected);\n  }\n\n  public finally(onFinally?: (() => void) | null): Promise<T> {\n    return this.#promise.finally(onFinally);\n  }\n\n  public cancel(): void {\n    if (this.#isResolved || this.#isRejected || this.#isCancelled) {\n      return;\n    }\n    this.#isCancelled = true;\n    if (this.#cancelHandlers.length) {\n      try {\n        for (const cancelHandler of this.#cancelHandlers) {\n          cancelHandler();\n        }\n      } catch (error) {\n        console.warn('Cancellation threw an error', error);\n        return;\n      }\n    }\n    this.#cancelHandlers.length = 0;\n    if (this.#reject) this.#reject(new CancelError('Request aborted'));\n  }\n\n  public get isCancelled(): boolean {\n    return this.#isCancelled;\n  }\n}\n"
  },
  {
    "path": "app/src/lib/api/core/OpenAPI.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nimport type { ApiRequestOptions } from './ApiRequestOptions';\n\ntype Resolver<T> = (options: ApiRequestOptions) => Promise<T>;\ntype Headers = Record<string, string>;\n\nexport type OpenAPIConfig = {\n  BASE: string;\n  VERSION: string;\n  WITH_CREDENTIALS: boolean;\n  CREDENTIALS: 'include' | 'omit' | 'same-origin';\n  TOKEN?: string | Resolver<string> | undefined;\n  USERNAME?: string | Resolver<string> | undefined;\n  PASSWORD?: string | Resolver<string> | undefined;\n  HEADERS?: Headers | Resolver<Headers> | undefined;\n  ENCODE_PATH?: ((path: string) => string) | undefined;\n};\n\nexport const OpenAPI: OpenAPIConfig = {\n  BASE: '',\n  VERSION: '0.1.0',\n  WITH_CREDENTIALS: false,\n  CREDENTIALS: 'include',\n  TOKEN: undefined,\n  USERNAME: undefined,\n  PASSWORD: undefined,\n  HEADERS: undefined,\n  ENCODE_PATH: undefined,\n};\n"
  },
  {
    "path": "app/src/lib/api/core/request.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nimport { ApiError } from './ApiError';\nimport type { ApiRequestOptions } from './ApiRequestOptions';\nimport type { ApiResult } from './ApiResult';\nimport { CancelablePromise } from './CancelablePromise';\nimport type { OnCancel } from './CancelablePromise';\nimport type { OpenAPIConfig } from './OpenAPI';\n\nexport const isDefined = <T>(\n  value: T | null | undefined,\n): value is Exclude<T, null | undefined> => {\n  return value !== undefined && value !== null;\n};\n\nexport const isString = (value: any): value is string => {\n  return typeof value === 'string';\n};\n\nexport const isStringWithValue = (value: any): value is string => {\n  return isString(value) && value !== '';\n};\n\nexport const isBlob = (value: any): value is Blob => {\n  return (\n    typeof value === 'object' &&\n    typeof value.type === 'string' &&\n    typeof value.stream === 'function' &&\n    typeof value.arrayBuffer === 'function' &&\n    typeof value.constructor === 'function' &&\n    typeof value.constructor.name === 'string' &&\n    /^(Blob|File)$/.test(value.constructor.name) &&\n    /^(Blob|File)$/.test(value[Symbol.toStringTag])\n  );\n};\n\nexport const isFormData = (value: any): value is FormData => {\n  return value instanceof FormData;\n};\n\nexport const base64 = (str: string): string => {\n  try {\n    return btoa(str);\n  } catch (err) {\n    // @ts-ignore\n    return Buffer.from(str).toString('base64');\n  }\n};\n\nexport const getQueryString = (params: Record<string, any>): string => {\n  const qs: string[] = [];\n\n  const append = (key: string, value: any) => {\n    qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);\n  };\n\n  const process = (key: string, value: any) => {\n    if (isDefined(value)) {\n      if (Array.isArray(value)) {\n        value.forEach((v) => {\n          process(key, v);\n        });\n      } else if (typeof value === 'object') {\n        Object.entries(value).forEach(([k, v]) => {\n          process(`${key}[${k}]`, v);\n        });\n      } else {\n        append(key, value);\n      }\n    }\n  };\n\n  Object.entries(params).forEach(([key, value]) => {\n    process(key, value);\n  });\n\n  if (qs.length > 0) {\n    return `?${qs.join('&')}`;\n  }\n\n  return '';\n};\n\nconst getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {\n  const encoder = config.ENCODE_PATH || encodeURI;\n\n  const path = options.url\n    .replace('{api-version}', config.VERSION)\n    .replace(/{(.*?)}/g, (substring: string, group: string) => {\n      if (options.path?.hasOwnProperty(group)) {\n        return encoder(String(options.path[group]));\n      }\n      return substring;\n    });\n\n  const url = `${config.BASE}${path}`;\n  if (options.query) {\n    return `${url}${getQueryString(options.query)}`;\n  }\n  return url;\n};\n\nexport const getFormData = (options: ApiRequestOptions): FormData | undefined => {\n  if (options.formData) {\n    const formData = new FormData();\n\n    const process = (key: string, value: any) => {\n      if (isString(value) || isBlob(value)) {\n        formData.append(key, value);\n      } else {\n        formData.append(key, JSON.stringify(value));\n      }\n    };\n\n    Object.entries(options.formData)\n      .filter(([_, value]) => isDefined(value))\n      .forEach(([key, value]) => {\n        if (Array.isArray(value)) {\n          value.forEach((v) => process(key, v));\n        } else {\n          process(key, value);\n        }\n      });\n\n    return formData;\n  }\n  return undefined;\n};\n\ntype Resolver<T> = (options: ApiRequestOptions) => Promise<T>;\n\nexport const resolve = async <T>(\n  options: ApiRequestOptions,\n  resolver?: T | Resolver<T>,\n): Promise<T | undefined> => {\n  if (typeof resolver === 'function') {\n    return (resolver as Resolver<T>)(options);\n  }\n  return resolver;\n};\n\nexport const getHeaders = async (\n  config: OpenAPIConfig,\n  options: ApiRequestOptions,\n): Promise<Headers> => {\n  const [token, username, password, additionalHeaders] = await Promise.all([\n    resolve(options, config.TOKEN),\n    resolve(options, config.USERNAME),\n    resolve(options, config.PASSWORD),\n    resolve(options, config.HEADERS),\n  ]);\n\n  const headers = Object.entries({\n    Accept: 'application/json',\n    ...additionalHeaders,\n    ...options.headers,\n  })\n    .filter(([_, value]) => isDefined(value))\n    .reduce(\n      (headers, [key, value]) => ({\n        ...headers,\n        [key]: String(value),\n      }),\n      {} as Record<string, string>,\n    );\n\n  if (isStringWithValue(token)) {\n    headers['Authorization'] = `Bearer ${token}`;\n  }\n\n  if (isStringWithValue(username) && isStringWithValue(password)) {\n    const credentials = base64(`${username}:${password}`);\n    headers['Authorization'] = `Basic ${credentials}`;\n  }\n\n  if (options.body !== undefined) {\n    if (options.mediaType) {\n      headers['Content-Type'] = options.mediaType;\n    } else if (isBlob(options.body)) {\n      headers['Content-Type'] = options.body.type || 'application/octet-stream';\n    } else if (isString(options.body)) {\n      headers['Content-Type'] = 'text/plain';\n    } else if (!isFormData(options.body)) {\n      headers['Content-Type'] = 'application/json';\n    }\n  }\n\n  return new Headers(headers);\n};\n\nexport const getRequestBody = (options: ApiRequestOptions): any => {\n  if (options.body !== undefined) {\n    if (options.mediaType?.includes('/json')) {\n      return JSON.stringify(options.body);\n    } else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {\n      return options.body;\n    } else {\n      return JSON.stringify(options.body);\n    }\n  }\n  return undefined;\n};\n\nexport const sendRequest = async (\n  config: OpenAPIConfig,\n  options: ApiRequestOptions,\n  url: string,\n  body: any,\n  formData: FormData | undefined,\n  headers: Headers,\n  onCancel: OnCancel,\n): Promise<Response> => {\n  const controller = new AbortController();\n\n  const request: RequestInit = {\n    headers,\n    body: body ?? formData,\n    method: options.method,\n    signal: controller.signal,\n  };\n\n  if (config.WITH_CREDENTIALS) {\n    request.credentials = config.CREDENTIALS;\n  }\n\n  onCancel(() => controller.abort());\n\n  return await fetch(url, request);\n};\n\nexport const getResponseHeader = (\n  response: Response,\n  responseHeader?: string,\n): string | undefined => {\n  if (responseHeader) {\n    const content = response.headers.get(responseHeader);\n    if (isString(content)) {\n      return content;\n    }\n  }\n  return undefined;\n};\n\nexport const getResponseBody = async (response: Response): Promise<any> => {\n  if (response.status !== 204) {\n    try {\n      const contentType = response.headers.get('Content-Type');\n      if (contentType) {\n        const jsonTypes = ['application/json', 'application/problem+json'];\n        const isJSON = jsonTypes.some((type) => contentType.toLowerCase().startsWith(type));\n        if (isJSON) {\n          return await response.json();\n        } else {\n          return await response.text();\n        }\n      }\n    } catch (error) {\n      console.error(error);\n    }\n  }\n  return undefined;\n};\n\nexport const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {\n  const errors: Record<number, string> = {\n    400: 'Bad Request',\n    401: 'Unauthorized',\n    403: 'Forbidden',\n    404: 'Not Found',\n    500: 'Internal Server Error',\n    502: 'Bad Gateway',\n    503: 'Service Unavailable',\n    ...options.errors,\n  };\n\n  const error = errors[result.status];\n  if (error) {\n    throw new ApiError(options, result, error);\n  }\n\n  if (!result.ok) {\n    const errorStatus = result.status ?? 'unknown';\n    const errorStatusText = result.statusText ?? 'unknown';\n    const errorBody = (() => {\n      try {\n        return JSON.stringify(result.body, null, 2);\n      } catch (e) {\n        return undefined;\n      }\n    })();\n\n    throw new ApiError(\n      options,\n      result,\n      `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`,\n    );\n  }\n};\n\n/**\n * Request method\n * @param config The OpenAPI configuration object\n * @param options The request options from the service\n * @returns CancelablePromise<T>\n * @throws ApiError\n */\nexport const request = <T>(\n  config: OpenAPIConfig,\n  options: ApiRequestOptions,\n): CancelablePromise<T> => {\n  return new CancelablePromise(async (resolve, reject, onCancel) => {\n    try {\n      const url = getUrl(config, options);\n      const formData = getFormData(options);\n      const body = getRequestBody(options);\n      const headers = await getHeaders(config, options);\n\n      if (!onCancel.isCancelled) {\n        const response = await sendRequest(config, options, url, body, formData, headers, onCancel);\n        const responseBody = await getResponseBody(response);\n        const responseHeader = getResponseHeader(response, options.responseHeader);\n\n        const result: ApiResult = {\n          url,\n          ok: response.ok,\n          status: response.status,\n          statusText: response.statusText,\n          body: responseHeader ?? responseBody,\n        };\n\n        catchErrorCodes(options, result);\n\n        resolve(result.body);\n      }\n    } catch (error) {\n      reject(error);\n    }\n  });\n};\n"
  },
  {
    "path": "app/src/lib/api/index.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport { ApiError } from './core/ApiError';\nexport { CancelablePromise, CancelError } from './core/CancelablePromise';\nexport { OpenAPI } from './core/OpenAPI';\nexport type { OpenAPIConfig } from './core/OpenAPI';\n\nexport type { Body_add_profile_sample_profiles__profile_id__samples_post } from './models/Body_add_profile_sample_profiles__profile_id__samples_post';\nexport type { Body_transcribe_audio_transcribe_post } from './models/Body_transcribe_audio_transcribe_post';\nexport type { GenerationRequest } from './models/GenerationRequest';\nexport type { GenerationResponse } from './models/GenerationResponse';\nexport type { HealthResponse } from './models/HealthResponse';\nexport type { HistoryListResponse } from './models/HistoryListResponse';\nexport type { HistoryResponse } from './models/HistoryResponse';\nexport type { HTTPValidationError } from './models/HTTPValidationError';\nexport type { ModelDownloadRequest } from './models/ModelDownloadRequest';\nexport type { ModelStatus } from './models/ModelStatus';\nexport type { ModelStatusListResponse } from './models/ModelStatusListResponse';\nexport type { ProfileSampleResponse } from './models/ProfileSampleResponse';\nexport type { TranscriptionResponse } from './models/TranscriptionResponse';\nexport type { ValidationError } from './models/ValidationError';\nexport type { VoiceProfileCreate } from './models/VoiceProfileCreate';\nexport type { VoiceProfileResponse } from './models/VoiceProfileResponse';\n\nexport { $Body_add_profile_sample_profiles__profile_id__samples_post } from './schemas/$Body_add_profile_sample_profiles__profile_id__samples_post';\nexport { $Body_transcribe_audio_transcribe_post } from './schemas/$Body_transcribe_audio_transcribe_post';\nexport { $GenerationRequest } from './schemas/$GenerationRequest';\nexport { $GenerationResponse } from './schemas/$GenerationResponse';\nexport { $HealthResponse } from './schemas/$HealthResponse';\nexport { $HistoryListResponse } from './schemas/$HistoryListResponse';\nexport { $HistoryResponse } from './schemas/$HistoryResponse';\nexport { $HTTPValidationError } from './schemas/$HTTPValidationError';\nexport { $ModelDownloadRequest } from './schemas/$ModelDownloadRequest';\nexport { $ModelStatus } from './schemas/$ModelStatus';\nexport { $ModelStatusListResponse } from './schemas/$ModelStatusListResponse';\nexport { $ProfileSampleResponse } from './schemas/$ProfileSampleResponse';\nexport { $TranscriptionResponse } from './schemas/$TranscriptionResponse';\nexport { $ValidationError } from './schemas/$ValidationError';\nexport { $VoiceProfileCreate } from './schemas/$VoiceProfileCreate';\nexport { $VoiceProfileResponse } from './schemas/$VoiceProfileResponse';\n\nexport { DefaultService } from './services/DefaultService';\n"
  },
  {
    "path": "app/src/lib/api/models/Body_add_profile_sample_profiles__profile_id__samples_post.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport type Body_add_profile_sample_profiles__profile_id__samples_post = {\n  file: Blob;\n  reference_text: string;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/Body_transcribe_audio_transcribe_post.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport type Body_transcribe_audio_transcribe_post = {\n  file: Blob;\n  language?: string | null;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/GenerationRequest.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Request model for voice generation.\n */\nexport type GenerationRequest = {\n  profile_id: string;\n  text: string;\n  language?: string;\n  seed?: number | null;\n  model_size?: string | null;\n  instruct?: string | null;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/GenerationResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Response model for voice generation.\n */\nexport type GenerationResponse = {\n  id: string;\n  profile_id: string;\n  text: string;\n  language: string;\n  audio_path: string;\n  duration: number;\n  seed: number | null;\n  instruct: string | null;\n  created_at: string;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/HTTPValidationError.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nimport type { ValidationError } from './ValidationError';\nexport type HTTPValidationError = {\n  detail?: Array<ValidationError>;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/HealthResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Response model for health check.\n */\nexport type HealthResponse = {\n  status: string;\n  model_loaded: boolean;\n  model_downloaded?: boolean | null;\n  model_size?: string | null;\n  gpu_available: boolean;\n  vram_used_mb?: number | null;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/HistoryListResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nimport type { HistoryResponse } from './HistoryResponse';\n/**\n * Response model for history list.\n */\nexport type HistoryListResponse = {\n  items: Array<HistoryResponse>;\n  total: number;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/HistoryResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Response model for history entry (includes profile name).\n */\nexport type HistoryResponse = {\n  id: string;\n  profile_id: string;\n  profile_name: string;\n  text: string;\n  language: string;\n  audio_path: string;\n  duration: number;\n  seed: number | null;\n  instruct: string | null;\n  created_at: string;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/ModelDownloadRequest.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Request model for triggering model download.\n */\nexport type ModelDownloadRequest = {\n  model_name: string;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/ModelStatus.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Response model for model status.\n */\nexport type ModelStatus = {\n  model_name: string;\n  display_name: string;\n  downloaded: boolean;\n  downloading?: boolean;  // True if download is in progress\n  size_mb?: number | null;\n  loaded?: boolean;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/ModelStatusListResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nimport type { ModelStatus } from './ModelStatus';\n/**\n * Response model for model status list.\n */\nexport type ModelStatusListResponse = {\n  models: Array<ModelStatus>;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/ProfileSampleResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Response model for profile sample.\n */\nexport type ProfileSampleResponse = {\n  id: string;\n  profile_id: string;\n  audio_path: string;\n  reference_text: string;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/TranscriptionResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Response model for transcription.\n */\nexport type TranscriptionResponse = {\n  text: string;\n  duration: number;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/ValidationError.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport type ValidationError = {\n  loc: Array<string | number>;\n  msg: string;\n  type: string;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/VoiceProfileCreate.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Request model for creating a voice profile.\n */\nexport type VoiceProfileCreate = {\n  name: string;\n  description?: string | null;\n  language?: string;\n};\n"
  },
  {
    "path": "app/src/lib/api/models/VoiceProfileResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\n/**\n * Response model for voice profile.\n */\nexport type VoiceProfileResponse = {\n  id: string;\n  name: string;\n  description: string | null;\n  language: string;\n  created_at: string;\n  updated_at: string;\n};\n"
  },
  {
    "path": "app/src/lib/api/schemas/$Body_add_profile_sample_profiles__profile_id__samples_post.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $Body_add_profile_sample_profiles__profile_id__samples_post = {\n  properties: {\n    file: {\n      type: 'binary',\n      isRequired: true,\n      format: 'binary',\n    },\n    reference_text: {\n      type: 'string',\n      isRequired: true,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$Body_transcribe_audio_transcribe_post.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $Body_transcribe_audio_transcribe_post = {\n  properties: {\n    file: {\n      type: 'binary',\n      isRequired: true,\n      format: 'binary',\n    },\n    language: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'string',\n        },\n        {\n          type: 'null',\n        },\n      ],\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$GenerationRequest.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $GenerationRequest = {\n  description: `Request model for voice generation.`,\n  properties: {\n    profile_id: {\n      type: 'string',\n      isRequired: true,\n    },\n    text: {\n      type: 'string',\n      isRequired: true,\n      maxLength: 5000,\n      minLength: 1,\n    },\n    language: {\n      type: 'string',\n      pattern: '^(en|zh)$',\n    },\n    seed: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'number',\n        },\n        {\n          type: 'null',\n        },\n      ],\n    },\n    model_size: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'string',\n          pattern: '^(1\\\\.7B|0\\\\.6B)$',\n        },\n        {\n          type: 'null',\n        },\n      ],\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$GenerationResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $GenerationResponse = {\n  description: `Response model for voice generation.`,\n  properties: {\n    id: {\n      type: 'string',\n      isRequired: true,\n    },\n    profile_id: {\n      type: 'string',\n      isRequired: true,\n    },\n    text: {\n      type: 'string',\n      isRequired: true,\n    },\n    language: {\n      type: 'string',\n      isRequired: true,\n    },\n    audio_path: {\n      type: 'string',\n      isRequired: true,\n    },\n    duration: {\n      type: 'number',\n      isRequired: true,\n    },\n    seed: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'number',\n        },\n        {\n          type: 'null',\n        },\n      ],\n      isRequired: true,\n    },\n    created_at: {\n      type: 'string',\n      isRequired: true,\n      format: 'date-time',\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$HTTPValidationError.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $HTTPValidationError = {\n  properties: {\n    detail: {\n      type: 'array',\n      contains: {\n        type: 'ValidationError',\n      },\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$HealthResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $HealthResponse = {\n  description: `Response model for health check.`,\n  properties: {\n    status: {\n      type: 'string',\n      isRequired: true,\n    },\n    model_loaded: {\n      type: 'boolean',\n      isRequired: true,\n    },\n    model_downloaded: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'boolean',\n        },\n        {\n          type: 'null',\n        },\n      ],\n    },\n    model_size: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'string',\n        },\n        {\n          type: 'null',\n        },\n      ],\n    },\n    gpu_available: {\n      type: 'boolean',\n      isRequired: true,\n    },\n    vram_used_mb: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'number',\n        },\n        {\n          type: 'null',\n        },\n      ],\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$HistoryListResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $HistoryListResponse = {\n  description: `Response model for history list.`,\n  properties: {\n    items: {\n      type: 'array',\n      contains: {\n        type: 'HistoryResponse',\n      },\n      isRequired: true,\n    },\n    total: {\n      type: 'number',\n      isRequired: true,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$HistoryResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $HistoryResponse = {\n  description: `Response model for history entry (includes profile name).`,\n  properties: {\n    id: {\n      type: 'string',\n      isRequired: true,\n    },\n    profile_id: {\n      type: 'string',\n      isRequired: true,\n    },\n    profile_name: {\n      type: 'string',\n      isRequired: true,\n    },\n    text: {\n      type: 'string',\n      isRequired: true,\n    },\n    language: {\n      type: 'string',\n      isRequired: true,\n    },\n    audio_path: {\n      type: 'string',\n      isRequired: true,\n    },\n    duration: {\n      type: 'number',\n      isRequired: true,\n    },\n    seed: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'number',\n        },\n        {\n          type: 'null',\n        },\n      ],\n      isRequired: true,\n    },\n    created_at: {\n      type: 'string',\n      isRequired: true,\n      format: 'date-time',\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$ModelDownloadRequest.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $ModelDownloadRequest = {\n  description: `Request model for triggering model download.`,\n  properties: {\n    model_name: {\n      type: 'string',\n      isRequired: true,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$ModelStatus.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $ModelStatus = {\n  description: `Response model for model status.`,\n  properties: {\n    model_name: {\n      type: 'string',\n      isRequired: true,\n    },\n    display_name: {\n      type: 'string',\n      isRequired: true,\n    },\n    downloaded: {\n      type: 'boolean',\n      isRequired: true,\n    },\n    size_mb: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'number',\n        },\n        {\n          type: 'null',\n        },\n      ],\n    },\n    loaded: {\n      type: 'boolean',\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$ModelStatusListResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $ModelStatusListResponse = {\n  description: `Response model for model status list.`,\n  properties: {\n    models: {\n      type: 'array',\n      contains: {\n        type: 'ModelStatus',\n      },\n      isRequired: true,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$ProfileSampleResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $ProfileSampleResponse = {\n  description: `Response model for profile sample.`,\n  properties: {\n    id: {\n      type: 'string',\n      isRequired: true,\n    },\n    profile_id: {\n      type: 'string',\n      isRequired: true,\n    },\n    audio_path: {\n      type: 'string',\n      isRequired: true,\n    },\n    reference_text: {\n      type: 'string',\n      isRequired: true,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$TranscriptionResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $TranscriptionResponse = {\n  description: `Response model for transcription.`,\n  properties: {\n    text: {\n      type: 'string',\n      isRequired: true,\n    },\n    duration: {\n      type: 'number',\n      isRequired: true,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$ValidationError.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $ValidationError = {\n  properties: {\n    loc: {\n      type: 'array',\n      contains: {\n        type: 'any-of',\n        contains: [\n          {\n            type: 'string',\n          },\n          {\n            type: 'number',\n          },\n        ],\n      },\n      isRequired: true,\n    },\n    msg: {\n      type: 'string',\n      isRequired: true,\n    },\n    type: {\n      type: 'string',\n      isRequired: true,\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$VoiceProfileCreate.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $VoiceProfileCreate = {\n  description: `Request model for creating a voice profile.`,\n  properties: {\n    name: {\n      type: 'string',\n      isRequired: true,\n      maxLength: 100,\n      minLength: 1,\n    },\n    description: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'string',\n          maxLength: 500,\n        },\n        {\n          type: 'null',\n        },\n      ],\n    },\n    language: {\n      type: 'string',\n      pattern: '^(en|zh)$',\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/schemas/$VoiceProfileResponse.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nexport const $VoiceProfileResponse = {\n  description: `Response model for voice profile.`,\n  properties: {\n    id: {\n      type: 'string',\n      isRequired: true,\n    },\n    name: {\n      type: 'string',\n      isRequired: true,\n    },\n    description: {\n      type: 'any-of',\n      contains: [\n        {\n          type: 'string',\n        },\n        {\n          type: 'null',\n        },\n      ],\n      isRequired: true,\n    },\n    language: {\n      type: 'string',\n      isRequired: true,\n    },\n    created_at: {\n      type: 'string',\n      isRequired: true,\n      format: 'date-time',\n    },\n    updated_at: {\n      type: 'string',\n      isRequired: true,\n      format: 'date-time',\n    },\n  },\n} as const;\n"
  },
  {
    "path": "app/src/lib/api/services/DefaultService.ts",
    "content": "/* generated using openapi-typescript-codegen -- do not edit */\n/* istanbul ignore file */\n/* tslint:disable */\n/* eslint-disable */\nimport type { Body_add_profile_sample_profiles__profile_id__samples_post } from '../models/Body_add_profile_sample_profiles__profile_id__samples_post';\nimport type { Body_transcribe_audio_transcribe_post } from '../models/Body_transcribe_audio_transcribe_post';\nimport type { GenerationRequest } from '../models/GenerationRequest';\nimport type { GenerationResponse } from '../models/GenerationResponse';\nimport type { HealthResponse } from '../models/HealthResponse';\nimport type { HistoryListResponse } from '../models/HistoryListResponse';\nimport type { HistoryResponse } from '../models/HistoryResponse';\nimport type { ModelDownloadRequest } from '../models/ModelDownloadRequest';\nimport type { ModelStatusListResponse } from '../models/ModelStatusListResponse';\nimport type { ProfileSampleResponse } from '../models/ProfileSampleResponse';\nimport type { TranscriptionResponse } from '../models/TranscriptionResponse';\nimport type { VoiceProfileCreate } from '../models/VoiceProfileCreate';\nimport type { VoiceProfileResponse } from '../models/VoiceProfileResponse';\nimport type { CancelablePromise } from '../core/CancelablePromise';\nimport { OpenAPI } from '../core/OpenAPI';\nimport { request as __request } from '../core/request';\nexport class DefaultService {\n  /**\n   * Root\n   * Root endpoint.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static rootGet(): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/',\n    });\n  }\n  /**\n   * Health\n   * Health check endpoint.\n   * @returns HealthResponse Successful Response\n   * @throws ApiError\n   */\n  public static healthHealthGet(): CancelablePromise<HealthResponse> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/health',\n    });\n  }\n  /**\n   * List Profiles\n   * List all voice profiles.\n   * @returns VoiceProfileResponse Successful Response\n   * @throws ApiError\n   */\n  public static listProfilesProfilesGet(): CancelablePromise<Array<VoiceProfileResponse>> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/profiles',\n    });\n  }\n  /**\n   * Create Profile\n   * Create a new voice profile.\n   * @returns VoiceProfileResponse Successful Response\n   * @throws ApiError\n   */\n  public static createProfileProfilesPost({\n    requestBody,\n  }: {\n    requestBody: VoiceProfileCreate;\n  }): CancelablePromise<VoiceProfileResponse> {\n    return __request(OpenAPI, {\n      method: 'POST',\n      url: '/profiles',\n      body: requestBody,\n      mediaType: 'application/json',\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Get Profile\n   * Get a voice profile by ID.\n   * @returns VoiceProfileResponse Successful Response\n   * @throws ApiError\n   */\n  public static getProfileProfilesProfileIdGet({\n    profileId,\n  }: {\n    profileId: string;\n  }): CancelablePromise<VoiceProfileResponse> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/profiles/{profile_id}',\n      path: {\n        profile_id: profileId,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Update Profile\n   * Update a voice profile.\n   * @returns VoiceProfileResponse Successful Response\n   * @throws ApiError\n   */\n  public static updateProfileProfilesProfileIdPut({\n    profileId,\n    requestBody,\n  }: {\n    profileId: string;\n    requestBody: VoiceProfileCreate;\n  }): CancelablePromise<VoiceProfileResponse> {\n    return __request(OpenAPI, {\n      method: 'PUT',\n      url: '/profiles/{profile_id}',\n      path: {\n        profile_id: profileId,\n      },\n      body: requestBody,\n      mediaType: 'application/json',\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Delete Profile\n   * Delete a voice profile.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static deleteProfileProfilesProfileIdDelete({\n    profileId,\n  }: {\n    profileId: string;\n  }): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'DELETE',\n      url: '/profiles/{profile_id}',\n      path: {\n        profile_id: profileId,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Add Profile Sample\n   * Add a sample to a voice profile.\n   * @returns ProfileSampleResponse Successful Response\n   * @throws ApiError\n   */\n  public static addProfileSampleProfilesProfileIdSamplesPost({\n    profileId,\n    formData,\n  }: {\n    profileId: string;\n    formData: Body_add_profile_sample_profiles__profile_id__samples_post;\n  }): CancelablePromise<ProfileSampleResponse> {\n    return __request(OpenAPI, {\n      method: 'POST',\n      url: '/profiles/{profile_id}/samples',\n      path: {\n        profile_id: profileId,\n      },\n      formData: formData,\n      mediaType: 'multipart/form-data',\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Get Profile Samples\n   * Get all samples for a profile.\n   * @returns ProfileSampleResponse Successful Response\n   * @throws ApiError\n   */\n  public static getProfileSamplesProfilesProfileIdSamplesGet({\n    profileId,\n  }: {\n    profileId: string;\n  }): CancelablePromise<Array<ProfileSampleResponse>> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/profiles/{profile_id}/samples',\n      path: {\n        profile_id: profileId,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Delete Profile Sample\n   * Delete a profile sample.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static deleteProfileSampleProfilesSamplesSampleIdDelete({\n    sampleId,\n  }: {\n    sampleId: string;\n  }): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'DELETE',\n      url: '/profiles/samples/{sample_id}',\n      path: {\n        sample_id: sampleId,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Generate Speech\n   * Generate speech from text using a voice profile.\n   * @returns GenerationResponse Successful Response\n   * @throws ApiError\n   */\n  public static generateSpeechGeneratePost({\n    requestBody,\n  }: {\n    requestBody: GenerationRequest;\n  }): CancelablePromise<GenerationResponse> {\n    return __request(OpenAPI, {\n      method: 'POST',\n      url: '/generate',\n      body: requestBody,\n      mediaType: 'application/json',\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * List History\n   * List generation history with optional filters.\n   * @returns HistoryListResponse Successful Response\n   * @throws ApiError\n   */\n  public static listHistoryHistoryGet({\n    profileId,\n    search,\n    limit = 50,\n    offset,\n  }: {\n    profileId?: string | null;\n    search?: string | null;\n    limit?: number;\n    offset?: number;\n  }): CancelablePromise<HistoryListResponse> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/history',\n      query: {\n        profile_id: profileId,\n        search: search,\n        limit: limit,\n        offset: offset,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Get Generation\n   * Get a generation by ID.\n   * @returns HistoryResponse Successful Response\n   * @throws ApiError\n   */\n  public static getGenerationHistoryGenerationIdGet({\n    generationId,\n  }: {\n    generationId: string;\n  }): CancelablePromise<HistoryResponse> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/history/{generation_id}',\n      path: {\n        generation_id: generationId,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Delete Generation\n   * Delete a generation.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static deleteGenerationHistoryGenerationIdDelete({\n    generationId,\n  }: {\n    generationId: string;\n  }): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'DELETE',\n      url: '/history/{generation_id}',\n      path: {\n        generation_id: generationId,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Get Stats\n   * Get generation statistics.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static getStatsHistoryStatsGet(): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/history/stats',\n    });\n  }\n  /**\n   * Transcribe Audio\n   * Transcribe audio file to text.\n   * @returns TranscriptionResponse Successful Response\n   * @throws ApiError\n   */\n  public static transcribeAudioTranscribePost({\n    formData,\n  }: {\n    formData: Body_transcribe_audio_transcribe_post;\n  }): CancelablePromise<TranscriptionResponse> {\n    return __request(OpenAPI, {\n      method: 'POST',\n      url: '/transcribe',\n      formData: formData,\n      mediaType: 'multipart/form-data',\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Get Audio\n   * Serve generated audio file.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static getAudioAudioGenerationIdGet({\n    generationId,\n  }: {\n    generationId: string;\n  }): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/audio/{generation_id}',\n      path: {\n        generation_id: generationId,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Load Model\n   * Manually load TTS model.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static loadModelModelsLoadPost({\n    modelSize = '1.7B',\n  }: {\n    modelSize?: string;\n  }): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'POST',\n      url: '/models/load',\n      query: {\n        model_size: modelSize,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Unload Model\n   * Unload TTS model to free memory.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static unloadModelModelsUnloadPost(): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'POST',\n      url: '/models/unload',\n    });\n  }\n  /**\n   * Get Model Progress\n   * Get model download progress via Server-Sent Events.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static getModelProgressModelsProgressModelNameGet({\n    modelName,\n  }: {\n    modelName: string;\n  }): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/models/progress/{model_name}',\n      path: {\n        model_name: modelName,\n      },\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n  /**\n   * Get Model Status\n   * Get status of all available models.\n   * @returns ModelStatusListResponse Successful Response\n   * @throws ApiError\n   */\n  public static getModelStatusModelsStatusGet(): CancelablePromise<ModelStatusListResponse> {\n    return __request(OpenAPI, {\n      method: 'GET',\n      url: '/models/status',\n    });\n  }\n  /**\n   * Trigger Model Download\n   * Trigger download of a specific model.\n   * @returns any Successful Response\n   * @throws ApiError\n   */\n  public static triggerModelDownloadModelsDownloadPost({\n    requestBody,\n  }: {\n    requestBody: ModelDownloadRequest;\n  }): CancelablePromise<any> {\n    return __request(OpenAPI, {\n      method: 'POST',\n      url: '/models/download',\n      body: requestBody,\n      mediaType: 'application/json',\n      errors: {\n        422: `Validation Error`,\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "app/src/lib/api/types.ts",
    "content": "// API Types matching backend Pydantic models\nimport type { LanguageCode } from '@/lib/constants/languages';\n\nexport type VoiceType = 'cloned' | 'preset' | 'designed';\n\nexport interface VoiceProfileCreate {\n  name: string;\n  description?: string;\n  language: LanguageCode;\n  voice_type?: VoiceType;\n  preset_engine?: string;\n  preset_voice_id?: string;\n  design_prompt?: string;\n  default_engine?: string;\n}\n\nexport interface VoiceProfileResponse {\n  id: string;\n  name: string;\n  description?: string;\n  language: string;\n  avatar_path?: string;\n  effects_chain?: EffectConfig[];\n  voice_type: VoiceType;\n  preset_engine?: string;\n  preset_voice_id?: string;\n  design_prompt?: string;\n  default_engine?: string;\n  generation_count: number;\n  sample_count: number;\n  created_at: string;\n  updated_at: string;\n}\n\nexport interface PresetVoice {\n  voice_id: string;\n  name: string;\n  gender: 'male' | 'female';\n  language: string;\n}\n\nexport interface ProfileSampleCreate {\n  reference_text: string;\n}\n\nexport interface ProfileSampleResponse {\n  id: string;\n  profile_id: string;\n  audio_path: string;\n  reference_text: string;\n}\n\nexport interface EffectConfig {\n  type: string;\n  enabled: boolean;\n  params: Record<string, number>;\n}\n\nexport interface GenerationRequest {\n  profile_id: string;\n  text: string;\n  language: LanguageCode;\n  seed?: number;\n  model_size?: '1.7B' | '0.6B' | '1B' | '3B';\n  engine?: 'qwen' | 'luxtts' | 'chatterbox' | 'chatterbox_turbo' | 'tada' | 'kokoro';\n  instruct?: string;\n  max_chunk_chars?: number;\n  crossfade_ms?: number;\n  normalize?: boolean;\n  effects_chain?: EffectConfig[];\n}\n\nexport interface GenerationVersionResponse {\n  id: string;\n  generation_id: string;\n  label: string;\n  audio_path: string;\n  effects_chain?: EffectConfig[];\n  source_version_id?: string;\n  is_default: boolean;\n  created_at: string;\n}\n\nexport interface GenerationResponse {\n  id: string;\n  profile_id: string;\n  text: string;\n  language: string;\n  audio_path?: string;\n  duration?: number;\n  seed?: number;\n  instruct?: string;\n  engine?: string;\n  model_size?: string;\n  status: 'loading_model' | 'generating' | 'completed' | 'failed';\n  error?: string;\n  is_favorited?: boolean;\n  created_at: string;\n  versions?: GenerationVersionResponse[];\n  active_version_id?: string;\n}\n\nexport interface HistoryQuery {\n  profile_id?: string;\n  search?: string;\n  limit?: number;\n  offset?: number;\n}\n\nexport interface HistoryResponse extends GenerationResponse {\n  profile_name: string;\n  versions?: GenerationVersionResponse[];\n  active_version_id?: string;\n}\n\nexport interface HistoryListResponse {\n  items: HistoryResponse[];\n  total: number;\n}\n\nexport type WhisperModelSize = 'base' | 'small' | 'medium' | 'large' | 'turbo';\n\nexport interface TranscriptionRequest {\n  language?: LanguageCode;\n  model?: WhisperModelSize;\n}\n\nexport interface TranscriptionResponse {\n  text: string;\n  duration: number;\n}\n\nexport interface HealthResponse {\n  status: string;\n  model_loaded: boolean;\n  model_downloaded?: boolean;\n  model_size?: string;\n  gpu_available: boolean;\n  gpu_type?: string;\n  vram_used_mb?: number;\n  backend_type?: string;\n  backend_variant?: string; // \"cpu\" or \"cuda\"\n}\n\nexport interface CudaDownloadProgress {\n  model_name: string;\n  current: number;\n  total: number;\n  progress: number;\n  filename?: string;\n  status: 'downloading' | 'extracting' | 'complete' | 'error';\n  timestamp: string;\n  error?: string;\n}\n\nexport interface CudaStatus {\n  available: boolean; // CUDA binary exists on disk\n  active: boolean; // Currently running the CUDA binary\n  binary_path?: string;\n  downloading: boolean; // Download in progress\n  download_progress?: CudaDownloadProgress;\n}\n\nexport interface ModelProgress {\n  model_name: string;\n  current: number;\n  total: number;\n  progress: number;\n  filename?: string;\n  status: 'downloading' | 'extracting' | 'complete' | 'error';\n  timestamp: string;\n  error?: string;\n}\n\nexport interface ModelStatus {\n  model_name: string;\n  display_name: string;\n  hf_repo_id?: string; // HuggingFace repository ID\n  downloaded: boolean;\n  downloading: boolean; // True if download is in progress\n  size_mb?: number;\n  loaded: boolean;\n}\n\nexport interface HuggingFaceModelInfo {\n  id: string;\n  author: string;\n  lastModified: string;\n  pipeline_tag?: string;\n  library_name?: string;\n  downloads: number;\n  likes: number;\n  tags: string[];\n  cardData?: {\n    license?: string;\n    language?: string[];\n    pipeline_tag?: string;\n  };\n}\n\nexport interface ModelStatusListResponse {\n  models: ModelStatus[];\n}\n\nexport interface ModelDownloadRequest {\n  model_name: string;\n}\n\nexport interface ActiveDownloadTask {\n  model_name: string;\n  status: string;\n  started_at: string;\n  error?: string;\n  progress?: number; // 0-100 percentage\n  current?: number; // bytes downloaded\n  total?: number; // total bytes\n  filename?: string; // current file being downloaded\n}\n\nexport interface ActiveGenerationTask {\n  task_id: string;\n  profile_id: string;\n  text_preview: string;\n  started_at: string;\n}\n\nexport interface ActiveTasksResponse {\n  downloads: ActiveDownloadTask[];\n  generations: ActiveGenerationTask[];\n}\n\nexport interface StoryCreate {\n  name: string;\n  description?: string;\n}\n\nexport interface StoryResponse {\n  id: string;\n  name: string;\n  description?: string;\n  created_at: string;\n  updated_at: string;\n  item_count: number;\n}\n\nexport interface StoryItemDetail {\n  id: string;\n  story_id: string;\n  generation_id: string;\n  version_id?: string;\n  start_time_ms: number;\n  track: number;\n  trim_start_ms: number;\n  trim_end_ms: number;\n  created_at: string;\n  profile_id: string;\n  profile_name: string;\n  text: string;\n  language: string;\n  audio_path: string;\n  duration: number;\n  seed?: number;\n  instruct?: string;\n  generation_created_at: string;\n  versions?: GenerationVersionResponse[];\n  active_version_id?: string;\n}\n\nexport interface StoryItemVersionUpdate {\n  version_id: string | null;\n}\n\nexport interface StoryDetailResponse {\n  id: string;\n  name: string;\n  description?: string;\n  created_at: string;\n  updated_at: string;\n  items: StoryItemDetail[];\n}\n\nexport interface StoryItemCreate {\n  generation_id: string;\n  start_time_ms?: number;\n  track?: number;\n}\n\nexport interface StoryItemUpdateTime {\n  generation_id: string;\n  start_time_ms: number;\n}\n\nexport interface StoryItemBatchUpdate {\n  updates: StoryItemUpdateTime[];\n}\n\nexport interface StoryItemReorder {\n  generation_ids: string[];\n}\n\nexport interface StoryItemMove {\n  start_time_ms: number;\n  track: number;\n}\n\nexport interface StoryItemTrim {\n  trim_start_ms: number;\n  trim_end_ms: number;\n}\n\nexport interface StoryItemSplit {\n  split_time_ms: number;\n}\n\n// Effects\n\nexport interface EffectPresetResponse {\n  id: string;\n  name: string;\n  description?: string;\n  effects_chain: EffectConfig[];\n  is_builtin: boolean;\n  created_at: string;\n}\n\nexport interface EffectPresetCreate {\n  name: string;\n  description?: string;\n  effects_chain: EffectConfig[];\n}\n\nexport interface EffectPresetUpdate {\n  name?: string;\n  description?: string;\n  effects_chain?: EffectConfig[];\n}\n\nexport interface AvailableEffectParam {\n  default: number;\n  min: number;\n  max: number;\n  step: number;\n  description: string;\n}\n\nexport interface AvailableEffect {\n  type: string;\n  label: string;\n  description: string;\n  params: Record<string, AvailableEffectParam>;\n}\n\nexport interface AvailableEffectsResponse {\n  effects: AvailableEffect[];\n}\n\nexport interface ApplyEffectsRequest {\n  effects_chain: EffectConfig[];\n  source_version_id?: string;\n  label?: string;\n  set_as_default?: boolean;\n}\n"
  },
  {
    "path": "app/src/lib/constants/languages.ts",
    "content": "/**\n * Supported languages for voice generation, per engine.\n *\n * Qwen3-TTS supports 10 languages.\n * LuxTTS is English-only.\n * Chatterbox Multilingual supports 23 languages.\n * Chatterbox Turbo is English-only.\n * Kokoro supports 8 languages.\n */\n\n/** All languages that any engine supports. */\nexport const ALL_LANGUAGES = {\n  ar: 'Arabic',\n  da: 'Danish',\n  de: 'German',\n  el: 'Greek',\n  en: 'English',\n  es: 'Spanish',\n  fi: 'Finnish',\n  fr: 'French',\n  he: 'Hebrew',\n  hi: 'Hindi',\n  it: 'Italian',\n  ja: 'Japanese',\n  ko: 'Korean',\n  ms: 'Malay',\n  nl: 'Dutch',\n  no: 'Norwegian',\n  pl: 'Polish',\n  pt: 'Portuguese',\n  ru: 'Russian',\n  sv: 'Swedish',\n  sw: 'Swahili',\n  tr: 'Turkish',\n  zh: 'Chinese',\n} as const;\n\nexport type LanguageCode = keyof typeof ALL_LANGUAGES;\n\n/** Per-engine supported language codes. */\nexport const ENGINE_LANGUAGES: Record<string, readonly LanguageCode[]> = {\n  qwen: ['zh', 'en', 'ja', 'ko', 'de', 'fr', 'ru', 'pt', 'es', 'it'],\n  luxtts: ['en'],\n  chatterbox: [\n    'ar',\n    'da',\n    'de',\n    'el',\n    'en',\n    'es',\n    'fi',\n    'fr',\n    'he',\n    'hi',\n    'it',\n    'ja',\n    'ko',\n    'ms',\n    'nl',\n    'no',\n    'pl',\n    'pt',\n    'ru',\n    'sv',\n    'sw',\n    'tr',\n    'zh',\n  ],\n  chatterbox_turbo: ['en'],\n  tada: ['en', 'ar', 'zh', 'de', 'es', 'fr', 'it', 'ja', 'pl', 'pt'],\n  kokoro: ['en', 'es', 'fr', 'hi', 'it', 'pt', 'ja', 'zh'],\n} as const;\n\n/** Helper: get language options for a given engine. */\nexport function getLanguageOptionsForEngine(engine: string) {\n  const codes = ENGINE_LANGUAGES[engine] ?? ENGINE_LANGUAGES.qwen;\n  return codes.map((code) => ({\n    value: code,\n    label: ALL_LANGUAGES[code],\n  }));\n}\n\n// ── Backwards-compatible exports used elsewhere ──────────────────────\nexport const SUPPORTED_LANGUAGES = ALL_LANGUAGES;\nexport const LANGUAGE_CODES = Object.keys(ALL_LANGUAGES) as LanguageCode[];\nexport const LANGUAGE_OPTIONS = LANGUAGE_CODES.map((code) => ({\n  value: code,\n  label: ALL_LANGUAGES[code],\n}));\n"
  },
  {
    "path": "app/src/lib/constants/ui.ts",
    "content": "/**\n * UI layout constants for safe area padding\n */\n\nconst isWindows = typeof navigator !== 'undefined' && navigator.userAgent.includes('Windows');\n\n/**\n * Top safe area padding - height of the drag region bar\n * On macOS this accounts for the overlay titlebar (48px).\n * On Windows the native title bar is outside the webview, so no padding is needed.\n */\nexport const TOP_SAFE_AREA_PADDING = isWindows ? 'pt-8' : 'pt-12';\n\n/**\n * Bottom safe area padding - height of the audio player\n * Corresponds to Tailwind's pb-32 (8rem / 128px)\n */\nexport const BOTTOM_SAFE_AREA_PADDING = 'pb-32';\n"
  },
  {
    "path": "app/src/lib/hooks/useAudioPlayer.ts",
    "content": "import { useRef, useState } from 'react';\nimport { useToast } from '@/components/ui/use-toast';\n\nexport function useAudioPlayer() {\n  const [isPlaying, setIsPlaying] = useState(false);\n  const audioRef = useRef<HTMLAudioElement | null>(null);\n  const { toast } = useToast();\n\n  const playPause = (file: File | null | undefined) => {\n    if (!file) return;\n\n    if (audioRef.current) {\n      if (isPlaying) {\n        audioRef.current.pause();\n        setIsPlaying(false);\n      } else {\n        audioRef.current.play();\n        setIsPlaying(true);\n      }\n    } else {\n      const audio = new Audio(URL.createObjectURL(file));\n      audioRef.current = audio;\n\n      audio.addEventListener('ended', () => {\n        setIsPlaying(false);\n        if (audioRef.current) {\n          URL.revokeObjectURL(audioRef.current.src);\n        }\n        audioRef.current = null;\n      });\n\n      audio.addEventListener('error', () => {\n        setIsPlaying(false);\n        toast({\n          title: 'Playback error',\n          description: 'Failed to play audio file',\n          variant: 'destructive',\n        });\n        if (audioRef.current) {\n          URL.revokeObjectURL(audioRef.current.src);\n        }\n        audioRef.current = null;\n      });\n\n      audio.play();\n      setIsPlaying(true);\n    }\n  };\n\n  const cleanup = () => {\n    if (audioRef.current) {\n      audioRef.current.pause();\n      if (audioRef.current.src.startsWith('blob:')) {\n        URL.revokeObjectURL(audioRef.current.src);\n      }\n      audioRef.current = null;\n    }\n    setIsPlaying(false);\n  };\n\n  return {\n    isPlaying,\n    playPause,\n    cleanup,\n  };\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useAudioRecording.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from 'react';\nimport { usePlatform } from '@/platform/PlatformContext';\nimport { convertToWav } from '@/lib/utils/audio';\n\ninterface UseAudioRecordingOptions {\n  maxDurationSeconds?: number;\n  onRecordingComplete?: (blob: Blob, duration?: number) => void;\n}\n\nexport function useAudioRecording({\n  maxDurationSeconds = 29,\n  onRecordingComplete,\n}: UseAudioRecordingOptions = {}) {\n  const platform = usePlatform();\n  const [isRecording, setIsRecording] = useState(false);\n  const [duration, setDuration] = useState(0);\n  const [error, setError] = useState<string | null>(null);\n  const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n  const chunksRef = useRef<Blob[]>([]);\n  const streamRef = useRef<MediaStream | null>(null);\n  const timerRef = useRef<number | null>(null);\n  const startTimeRef = useRef<number | null>(null);\n  const cancelledRef = useRef<boolean>(false);\n\n  const startRecording = useCallback(async () => {\n    try {\n      setError(null);\n      chunksRef.current = [];\n      cancelledRef.current = false;\n      setDuration(0);\n\n      // Check if getUserMedia is available\n      // In Tauri, navigator.mediaDevices might not be available immediately\n      if (typeof navigator === 'undefined') {\n        const errorMsg =\n          'Navigator API is not available. This might be a Tauri configuration issue.';\n        setError(errorMsg);\n        throw new Error(errorMsg);\n      }\n\n      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n        // Try waiting a bit for Tauri webview to initialize\n        await new Promise((resolve) => setTimeout(resolve, 100));\n\n        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n          console.error('MediaDevices check:', {\n            hasNavigator: typeof navigator !== 'undefined',\n            hasMediaDevices: !!navigator?.mediaDevices,\n            hasGetUserMedia: !!navigator?.mediaDevices?.getUserMedia,\n            isTauri: platform.metadata.isTauri,\n          });\n\n          const errorMsg = platform.metadata.isTauri\n            ? 'Microphone access is not available. Please ensure:\\n1. The app has microphone permissions in System Settings (macOS: System Settings > Privacy & Security > Microphone)\\n2. You restart the app after granting permissions\\n3. You are using Tauri v2 with a webview that supports getUserMedia'\n            : 'Microphone access is not available. Please ensure you are using a secure context (HTTPS or localhost) and that your browser has microphone permissions enabled.';\n          setError(errorMsg);\n          throw new Error(errorMsg);\n        }\n      }\n\n      // Request microphone access\n      const stream = await navigator.mediaDevices.getUserMedia({\n        audio: {\n          echoCancellation: true,\n          noiseSuppression: true,\n          autoGainControl: true,\n        },\n      });\n\n      streamRef.current = stream;\n\n      // Create MediaRecorder with preferred MIME type\n      const options: MediaRecorderOptions = {\n        mimeType: 'audio/webm;codecs=opus',\n      };\n\n      // Fallback to default if webm not supported\n      if (!MediaRecorder.isTypeSupported(options.mimeType!)) {\n        delete options.mimeType;\n      }\n\n      const mediaRecorder = new MediaRecorder(stream, options);\n      mediaRecorderRef.current = mediaRecorder;\n\n      mediaRecorder.ondataavailable = (event) => {\n        if (event.data.size > 0) {\n          chunksRef.current.push(event.data);\n        }\n      };\n\n      mediaRecorder.onstop = async () => {\n        // Snapshot the cancellation flag and recorded duration immediately —\n        // cancelRecording() clears chunks and sets cancelledRef synchronously\n        // before this async handler runs, so we must check it first.\n        const wasCancelled = cancelledRef.current;\n        const recordedDuration = startTimeRef.current\n          ? (Date.now() - startTimeRef.current) / 1000\n          : undefined;\n\n        const webmBlob = new Blob(chunksRef.current, { type: 'audio/webm' });\n\n        // Stop all tracks now that we have the data\n        streamRef.current?.getTracks().forEach((track) => {\n          track.stop();\n        });\n        streamRef.current = null;\n\n        // Don't fire completion callback if the recording was cancelled\n        if (wasCancelled) return;\n\n        // Convert to WAV format to avoid needing ffmpeg on backend\n        try {\n          const wavBlob = await convertToWav(webmBlob);\n          onRecordingComplete?.(wavBlob, recordedDuration);\n        } catch (err) {\n          console.error('Error converting audio to WAV:', err);\n          // Fallback to original blob if conversion fails\n          onRecordingComplete?.(webmBlob, recordedDuration);\n        }\n      };\n\n      mediaRecorder.onerror = (event) => {\n        setError('Recording error occurred');\n        console.error('MediaRecorder error:', event);\n      };\n\n      // Start recording\n      mediaRecorder.start(100); // Collect data every 100ms\n      setIsRecording(true);\n      startTimeRef.current = Date.now();\n\n      // Start timer\n      timerRef.current = window.setInterval(() => {\n        if (startTimeRef.current) {\n          const elapsed = (Date.now() - startTimeRef.current) / 1000;\n          setDuration(elapsed);\n\n          // Auto-stop at max duration\n          if (elapsed >= maxDurationSeconds) {\n            if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {\n              mediaRecorderRef.current.stop();\n              setIsRecording(false);\n              if (timerRef.current !== null) {\n                clearInterval(timerRef.current);\n                timerRef.current = null;\n              }\n            }\n          }\n        }\n      }, 100);\n    } catch (err) {\n      const errorMessage =\n        err instanceof Error\n          ? err.message\n          : 'Failed to access microphone. Please check permissions.';\n      setError(errorMessage);\n      setIsRecording(false);\n    }\n  }, [maxDurationSeconds, onRecordingComplete]);\n\n  const stopRecording = useCallback(() => {\n    if (mediaRecorderRef.current && isRecording) {\n      mediaRecorderRef.current.stop();\n      setIsRecording(false);\n\n      if (timerRef.current !== null) {\n        clearInterval(timerRef.current);\n        timerRef.current = null;\n      }\n    }\n  }, [isRecording]);\n\n  const cancelRecording = useCallback(() => {\n    if (mediaRecorderRef.current) {\n      cancelledRef.current = true; // Must be set before stop() triggers onstop\n      chunksRef.current = [];\n      mediaRecorderRef.current.stop();\n      setIsRecording(false);\n      setDuration(0);\n    }\n\n    // Stop all tracks\n    streamRef.current?.getTracks().forEach((track) => {\n      track.stop();\n    });\n    streamRef.current = null;\n\n    if (timerRef.current !== null) {\n      clearInterval(timerRef.current);\n      timerRef.current = null;\n    }\n  }, []);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return () => {\n      if (timerRef.current !== null) {\n        clearInterval(timerRef.current);\n      }\n      streamRef.current?.getTracks().forEach((track) => {\n        track.stop();\n      });\n    };\n  }, []);\n\n  return {\n    isRecording,\n    duration,\n    error,\n    startRecording,\n    stopRecording,\n    cancelRecording,\n  };\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useGeneration.ts",
    "content": "import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { apiClient } from '@/lib/api/client';\nimport type { GenerationRequest } from '@/lib/api/types';\n\nexport function useGeneration() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (data: GenerationRequest) => apiClient.generateSpeech(data),\n    onSuccess: () => {\n      // Invalidate history to show new generation\n      queryClient.invalidateQueries({ queryKey: ['history'] });\n    },\n  });\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useGenerationForm.ts",
    "content": "import { zodResolver } from '@hookform/resolvers/zod';\nimport { useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport * as z from 'zod';\nimport { useToast } from '@/components/ui/use-toast';\nimport { apiClient } from '@/lib/api/client';\nimport type { EffectConfig } from '@/lib/api/types';\nimport { LANGUAGE_CODES, type LanguageCode } from '@/lib/constants/languages';\nimport { useGeneration } from '@/lib/hooks/useGeneration';\nimport { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';\nimport { useGenerationStore } from '@/stores/generationStore';\nimport { useServerStore } from '@/stores/serverStore';\n\nconst generationSchema = z.object({\n  text: z.string().min(1, '').max(50000),\n  language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),\n  seed: z.number().int().optional(),\n  modelSize: z.enum(['1.7B', '0.6B', '1B', '3B']).optional(),\n  instruct: z.string().max(500).optional(),\n  engine: z.enum(['qwen', 'luxtts', 'chatterbox', 'chatterbox_turbo', 'tada', 'kokoro']).optional(),\n});\n\nexport type GenerationFormValues = z.infer<typeof generationSchema>;\n\ninterface UseGenerationFormOptions {\n  onSuccess?: (generationId: string) => void;\n  defaultValues?: Partial<GenerationFormValues>;\n  getEffectsChain?: () => EffectConfig[] | undefined;\n}\n\nexport function useGenerationForm(options: UseGenerationFormOptions = {}) {\n  const { toast } = useToast();\n  const generation = useGeneration();\n  const addPendingGeneration = useGenerationStore((state) => state.addPendingGeneration);\n  const maxChunkChars = useServerStore((state) => state.maxChunkChars);\n  const crossfadeMs = useServerStore((state) => state.crossfadeMs);\n  const normalizeAudio = useServerStore((state) => state.normalizeAudio);\n  const [downloadingModelName, setDownloadingModelName] = useState<string | null>(null);\n  const [downloadingDisplayName, setDownloadingDisplayName] = useState<string | null>(null);\n\n  useModelDownloadToast({\n    modelName: downloadingModelName || '',\n    displayName: downloadingDisplayName || '',\n    enabled: !!downloadingModelName,\n  });\n\n  const form = useForm<GenerationFormValues>({\n    resolver: zodResolver(generationSchema),\n    defaultValues: {\n      text: '',\n      language: 'en',\n      seed: undefined,\n      modelSize: '1.7B',\n      instruct: '',\n      engine: 'qwen',\n      ...options.defaultValues,\n    },\n  });\n\n  async function handleSubmit(\n    data: GenerationFormValues,\n    selectedProfileId: string | null,\n  ): Promise<void> {\n    if (!selectedProfileId) {\n      toast({\n        title: 'No profile selected',\n        description: 'Please select a voice profile from the cards above.',\n        variant: 'destructive',\n      });\n      return;\n    }\n\n    try {\n      const engine = data.engine || 'qwen';\n      const modelName =\n        engine === 'luxtts'\n          ? 'luxtts'\n          : engine === 'chatterbox'\n            ? 'chatterbox-tts'\n            : engine === 'chatterbox_turbo'\n              ? 'chatterbox-turbo'\n              : engine === 'tada'\n                ? data.modelSize === '3B'\n                  ? 'tada-3b-ml'\n                  : 'tada-1b'\n                : engine === 'kokoro'\n                  ? 'kokoro'\n                  : `qwen-tts-${data.modelSize}`;\n      const displayName =\n        engine === 'luxtts'\n          ? 'LuxTTS'\n          : engine === 'chatterbox'\n            ? 'Chatterbox TTS'\n            : engine === 'chatterbox_turbo'\n              ? 'Chatterbox Turbo'\n              : engine === 'tada'\n                ? data.modelSize === '3B'\n                  ? 'TADA 3B Multilingual'\n                  : 'TADA 1B'\n                : engine === 'kokoro'\n                  ? 'Kokoro 82M'\n                  : data.modelSize === '1.7B'\n                    ? 'Qwen TTS 1.7B'\n                    : 'Qwen TTS 0.6B';\n\n      // Check if model needs downloading\n      try {\n        const modelStatus = await apiClient.getModelStatus();\n        const model = modelStatus.models.find((m) => m.model_name === modelName);\n\n        if (model && !model.downloaded) {\n          setDownloadingModelName(modelName);\n          setDownloadingDisplayName(displayName);\n        }\n      } catch (error) {\n        console.error('Failed to check model status:', error);\n      }\n\n      const hasModelSizes = engine === 'qwen' || engine === 'tada';\n      const effectsChain = options.getEffectsChain?.();\n      // This now returns immediately with status=\"generating\"\n      const result = await generation.mutateAsync({\n        profile_id: selectedProfileId,\n        text: data.text,\n        language: data.language,\n        seed: data.seed,\n        model_size: hasModelSizes ? data.modelSize : undefined,\n        engine,\n        instruct: engine === 'qwen' ? data.instruct || undefined : undefined,\n        max_chunk_chars: maxChunkChars,\n        crossfade_ms: crossfadeMs,\n        normalize: normalizeAudio,\n        effects_chain: effectsChain?.length ? effectsChain : undefined,\n      });\n\n      // Track this generation for SSE status updates\n      addPendingGeneration(result.id);\n\n      // Reset form immediately — user can start typing again\n      form.reset({\n        text: '',\n        language: data.language,\n        seed: undefined,\n        modelSize: data.modelSize,\n        instruct: '',\n        engine: data.engine,\n      });\n      options.onSuccess?.(result.id);\n    } catch (error) {\n      toast({\n        title: 'Generation failed',\n        description: error instanceof Error ? error.message : 'Failed to generate audio',\n        variant: 'destructive',\n      });\n    } finally {\n      setDownloadingModelName(null);\n      setDownloadingDisplayName(null);\n    }\n  }\n\n  return {\n    form,\n    handleSubmit,\n    isPending: generation.isPending,\n  };\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useGenerationProgress.ts",
    "content": "import { useQueryClient } from '@tanstack/react-query';\nimport { useEffect, useRef } from 'react';\nimport { useToast } from '@/components/ui/use-toast';\nimport { apiClient } from '@/lib/api/client';\nimport { useGenerationStore } from '@/stores/generationStore';\nimport { usePlayerStore } from '@/stores/playerStore';\nimport { useServerStore } from '@/stores/serverStore';\n\ninterface GenerationStatusEvent {\n  id: string;\n  status: 'loading_model' | 'generating' | 'completed' | 'failed' | 'not_found';\n  duration?: number;\n  error?: string;\n}\n\n/**\n * Subscribes to SSE for all pending generations. When a generation completes,\n * invalidates the history query, removes it from pending, and auto-plays\n * if the player is idle.\n */\nexport function useGenerationProgress() {\n  const queryClient = useQueryClient();\n  const { toast } = useToast();\n  const pendingIds = useGenerationStore((s) => s.pendingGenerationIds);\n  const removePendingGeneration = useGenerationStore((s) => s.removePendingGeneration);\n  const removePendingStoryAdd = useGenerationStore((s) => s.removePendingStoryAdd);\n  const isPlaying = usePlayerStore((s) => s.isPlaying);\n  const setAudioWithAutoPlay = usePlayerStore((s) => s.setAudioWithAutoPlay);\n  const autoplayOnGenerate = useServerStore((s) => s.autoplayOnGenerate);\n\n  // Keep refs to avoid stale closures in EventSource handlers\n  const isPlayingRef = useRef(isPlaying);\n  const autoplayRef = useRef(autoplayOnGenerate);\n  isPlayingRef.current = isPlaying;\n  autoplayRef.current = autoplayOnGenerate;\n\n  // Track active EventSource instances\n  const eventSourcesRef = useRef<Map<string, EventSource>>(new Map());\n\n  // Unmount-only cleanup — close all SSE connections when the hook is torn down\n  useEffect(() => {\n    const sources = eventSourcesRef.current;\n    return () => {\n      for (const source of sources.values()) {\n        source.close();\n      }\n      sources.clear();\n    };\n  }, []);\n\n  useEffect(() => {\n    const currentSources = eventSourcesRef.current;\n\n    // Close SSE connections for IDs no longer pending\n    for (const [id, source] of currentSources.entries()) {\n      if (!pendingIds.has(id)) {\n        source.close();\n        currentSources.delete(id);\n      }\n    }\n\n    // Open SSE connections for new pending IDs\n    for (const id of pendingIds) {\n      if (currentSources.has(id)) continue;\n\n      const url = apiClient.getGenerationStatusUrl(id);\n      const source = new EventSource(url);\n\n      source.onmessage = (event) => {\n        try {\n          const data: GenerationStatusEvent = JSON.parse(event.data);\n\n          if (data.status === 'completed') {\n            source.close();\n            currentSources.delete(id);\n            removePendingGeneration(id);\n\n            // Refetch history to pick up the completed generation\n            queryClient.refetchQueries({ queryKey: ['history'] });\n\n            // If this generation was queued for a story, add it now\n            const storyId = removePendingStoryAdd(id);\n            if (storyId) {\n              apiClient\n                .addStoryItem(storyId, { generation_id: id })\n                .then(() => {\n                  queryClient.invalidateQueries({ queryKey: ['stories'] });\n                  queryClient.invalidateQueries({ queryKey: ['stories', storyId] });\n                  toast({\n                    title: 'Added to story',\n                    description: data.duration\n                      ? `Audio generated (${data.duration.toFixed(2)}s) and added to story`\n                      : 'Audio generated and added to story',\n                  });\n                })\n                .catch(() => {\n                  toast({\n                    title: 'Generation complete',\n                    description: 'Audio generated but failed to add to story',\n                    variant: 'destructive',\n                  });\n                });\n            } else {\n              // toast({\n              //   title: 'Generation complete!',\n              //   description: data.duration\n              //     ? `Audio generated (${data.duration.toFixed(2)}s)`\n              //     : 'Audio generated',\n              // });\n            }\n\n            // Auto-play if enabled and nothing is currently playing\n            if (autoplayRef.current && !isPlayingRef.current) {\n              const genAudioUrl = apiClient.getAudioUrl(id);\n              setAudioWithAutoPlay(genAudioUrl, id, '', '');\n            }\n          } else if (data.status === 'failed' || data.status === 'not_found') {\n            source.close();\n            currentSources.delete(id);\n            removePendingGeneration(id);\n            removePendingStoryAdd(id);\n\n            queryClient.refetchQueries({ queryKey: ['history'] });\n\n            toast({\n              title: data.status === 'not_found' ? 'Generation not found' : 'Generation failed',\n              description: data.error || 'An error occurred during generation',\n              variant: 'destructive',\n            });\n          }\n        } catch {\n          // Ignore parse errors from heartbeats etc\n        }\n      };\n\n      source.onerror = () => {\n        // SSE connection dropped — clean up and refresh history so any\n        // completed/failed generation still appears in the list\n        source.close();\n        currentSources.delete(id);\n        removePendingGeneration(id);\n        queryClient.refetchQueries({ queryKey: ['history'] });\n      };\n\n      currentSources.set(id, source);\n    }\n  }, [\n    pendingIds,\n    removePendingGeneration,\n    removePendingStoryAdd,\n    queryClient,\n    toast,\n    setAudioWithAutoPlay,\n  ]);\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useHistory.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { apiClient } from '@/lib/api/client';\nimport type { HistoryQuery } from '@/lib/api/types';\nimport { usePlatform } from '@/platform/PlatformContext';\n\nexport function useHistory(query?: HistoryQuery) {\n  return useQuery({\n    queryKey: ['history', query],\n    queryFn: () => apiClient.listHistory(query),\n  });\n}\n\nexport function useGenerationDetail(generationId: string) {\n  return useQuery({\n    queryKey: ['history', generationId],\n    queryFn: () => apiClient.getGeneration(generationId),\n    enabled: !!generationId,\n  });\n}\n\nexport function useDeleteGeneration() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (generationId: string) => apiClient.deleteGeneration(generationId),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['history'] });\n    },\n  });\n}\n\nexport function useExportGeneration() {\n  const platform = usePlatform();\n\n  return useMutation({\n    mutationFn: async ({ generationId, text }: { generationId: string; text: string }) => {\n      const blob = await apiClient.exportGeneration(generationId);\n\n      // Create safe filename from text\n      const safeText = text\n        .substring(0, 30)\n        .replace(/[^a-z0-9]/gi, '-')\n        .toLowerCase();\n      const filename = `generation-${safeText}.voicebox.zip`;\n\n      await platform.filesystem.saveFile(filename, blob, [\n        {\n          name: 'Voicebox Generation',\n          extensions: ['zip'],\n        },\n      ]);\n\n      return blob;\n    },\n  });\n}\n\nexport function useExportGenerationAudio() {\n  const platform = usePlatform();\n\n  return useMutation({\n    mutationFn: async ({ generationId, text }: { generationId: string; text: string }) => {\n      const blob = await apiClient.exportGenerationAudio(generationId);\n\n      // Create safe filename from text\n      const safeText = text\n        .substring(0, 30)\n        .replace(/[^a-z0-9]/gi, '-')\n        .toLowerCase();\n      const filename = `${safeText}.wav`;\n\n      await platform.filesystem.saveFile(filename, blob, [\n        {\n          name: 'Audio File',\n          extensions: ['wav'],\n        },\n      ]);\n\n      return blob;\n    },\n  });\n}\n\nexport function useImportGeneration() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (file: File) => apiClient.importGeneration(file),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['history'] });\n    },\n  });\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useModelDownloadToast.tsx",
    "content": "import { CheckCircle2, Loader2, XCircle } from 'lucide-react';\nimport { useCallback, useEffect, useRef } from 'react';\nimport { Progress } from '@/components/ui/progress';\nimport { useToast } from '@/components/ui/use-toast';\nimport type { ModelProgress } from '@/lib/api/types';\nimport { useServerStore } from '@/stores/serverStore';\n\ninterface UseModelDownloadToastOptions {\n  modelName: string;\n  displayName: string;\n  enabled?: boolean;\n  onComplete?: () => void;\n  onError?: (error: string) => void;\n}\n\n/**\n * Hook to show and update a toast notification with model download progress.\n * Subscribes to Server-Sent Events for real-time progress updates.\n */\nexport function useModelDownloadToast({\n  modelName,\n  displayName,\n  enabled = false,\n  onComplete,\n  onError,\n}: UseModelDownloadToastOptions) {\n  const { toast } = useToast();\n  const serverUrl = useServerStore((state) => state.serverUrl);\n  const toastIdRef = useRef<string | null>(null);\n  // biome-ignore lint: Using any for toast update ref to handle complex toast types\n  const toastUpdateRef = useRef<any>(null);\n  const eventSourceRef = useRef<EventSource | null>(null);\n\n  const formatBytes = useCallback((bytes: number): string => {\n    if (bytes === 0) return '0 B';\n    const k = 1024;\n    const sizes = ['B', 'KB', 'MB', 'GB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;\n  }, []);\n\n  useEffect(() => {\n    console.log('[useModelDownloadToast] useEffect triggered', {\n      enabled,\n      serverUrl,\n      modelName,\n      displayName,\n    });\n\n    if (!enabled || !serverUrl || !modelName) {\n      console.log('[useModelDownloadToast] Not enabled, skipping');\n      return;\n    }\n\n    console.log('[useModelDownloadToast] Creating toast and EventSource for:', modelName);\n\n    // Create initial toast\n    const toastResult = toast({\n      title: displayName,\n      description: (\n        <div className=\"flex items-center gap-2\">\n          <Loader2 className=\"h-4 w-4 animate-spin\" />\n          <span>Connecting to download...</span>\n        </div>\n      ),\n      duration: Infinity, // Don't auto-dismiss, we'll handle it manually\n    });\n    toastIdRef.current = toastResult.id;\n    toastUpdateRef.current = toastResult.update;\n\n    // Subscribe to progress updates via Server-Sent Events\n    const eventSourceUrl = `${serverUrl}/models/progress/${modelName}`;\n    console.log('[useModelDownloadToast] Creating EventSource to:', eventSourceUrl);\n    const eventSource = new EventSource(eventSourceUrl);\n\n    eventSource.onopen = () => {\n      console.log('[useModelDownloadToast] EventSource connection opened for:', modelName);\n    };\n\n    eventSource.onmessage = (event) => {\n      console.log('[useModelDownloadToast] Received SSE message:', event.data);\n      try {\n        const progress = JSON.parse(event.data) as ModelProgress;\n\n        // Update toast with progress\n        if (toastIdRef.current && toastUpdateRef.current) {\n          const progressPercent = progress.total > 0 ? progress.progress : 0;\n          const progressText =\n            progress.total > 0\n              ? `${formatBytes(progress.current)} / ${formatBytes(progress.total)} (${progress.progress.toFixed(1)}%)`\n              : '';\n\n          // Determine status icon and text\n          let statusIcon: React.ReactNode = null;\n          let statusText = 'Processing...';\n\n          switch (progress.status) {\n            case 'complete':\n              statusIcon = <CheckCircle2 className=\"h-4 w-4 text-green-500\" />;\n              statusText = 'Download complete';\n              break;\n            case 'error':\n              statusIcon = <XCircle className=\"h-4 w-4 text-destructive\" />;\n              statusText = 'Download failed. See Problems panel for details.';\n              break;\n            case 'downloading':\n              statusIcon = <Loader2 className=\"h-4 w-4 animate-spin\" />;\n              statusText = progress.filename || 'Downloading...';\n              break;\n            case 'extracting':\n              statusIcon = <Loader2 className=\"h-4 w-4 animate-spin\" />;\n              statusText = 'Extracting...';\n              break;\n          }\n\n          toastUpdateRef.current({\n            title: (\n              <div className=\"flex items-center gap-2\">\n                {statusIcon}\n                <span>{displayName}</span>\n              </div>\n            ),\n            description: (\n              <div className=\"space-y-2\">\n                <div className=\"text-sm\">{statusText}</div>\n                {progress.total > 0 && (\n                  <>\n                    <Progress value={progressPercent} className=\"h-2\" />\n                    <div className=\"text-xs text-muted-foreground\">{progressText}</div>\n                  </>\n                )}\n              </div>\n            ),\n            duration: progress.status === 'complete' || progress.status === 'error' ? 5000 : Infinity,\n          });\n\n          // Close connection and dismiss toast on completion or error\n          // Also treat progress >= 100% as complete\n          const isComplete = progress.status === 'complete' || progress.progress >= 100;\n          const isError = progress.status === 'error';\n\n          if (isComplete || isError) {\n            console.log('[useModelDownloadToast] Download finished:', {\n              isComplete,\n              isError,\n              progress: progress.progress,\n            });\n            eventSource.close();\n            eventSourceRef.current = null;\n\n            // Update toast to show completion state before callbacks\n            if (isComplete && toastUpdateRef.current) {\n              toastUpdateRef.current({\n                title: (\n                  <div className=\"flex items-center gap-2\">\n                    <CheckCircle2 className=\"h-4 w-4 text-green-500\" />\n                    <span>{displayName}</span>\n                  </div>\n                ),\n                description: 'Download complete',\n                duration: 3000,\n              });\n            }\n\n            // Call callbacks\n            if (isComplete && onComplete) {\n              console.log('[useModelDownloadToast] Download complete, calling onComplete callback');\n              onComplete();\n            } else if (isError && onError) {\n              console.log('[useModelDownloadToast] Download error, calling onError callback');\n              onError(progress.error || 'Unknown error');\n            }\n          }\n        }\n      } catch (error) {\n        console.error('Error parsing progress event:', error);\n      }\n    };\n\n    eventSource.onerror = (error) => {\n      console.error('[useModelDownloadToast] SSE error for:', modelName, error);\n      console.log('[useModelDownloadToast] EventSource readyState:', eventSource.readyState);\n      eventSource.close();\n      eventSourceRef.current = null;\n\n      // Show error toast\n      if (toastIdRef.current && toastUpdateRef.current) {\n        toastUpdateRef.current({\n          title: displayName,\n          description: 'Failed to track download progress',\n          variant: 'destructive',\n          duration: 5000,\n        });\n        toastIdRef.current = null;\n        toastUpdateRef.current = null;\n      }\n    };\n\n    eventSourceRef.current = eventSource;\n\n    // Cleanup on unmount or when disabled\n    return () => {\n      console.log('[useModelDownloadToast] Cleanup - closing EventSource for:', modelName);\n      if (eventSourceRef.current) {\n        eventSourceRef.current.close();\n        eventSourceRef.current = null;\n      }\n      // Note: We don't dismiss the toast here as it might still be showing completion state\n    };\n  }, [enabled, serverUrl, modelName, displayName, toast, formatBytes, onComplete, onError]);\n\n  return {\n    isTracking: enabled && eventSourceRef.current !== null,\n  };\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useProfiles.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { apiClient } from '@/lib/api/client';\nimport type { VoiceProfileCreate } from '@/lib/api/types';\nimport { usePlatform } from '@/platform/PlatformContext';\n\nexport function useProfiles() {\n  return useQuery({\n    queryKey: ['profiles'],\n    queryFn: () => apiClient.listProfiles(),\n  });\n}\n\nexport function useProfile(profileId: string) {\n  return useQuery({\n    queryKey: ['profiles', profileId],\n    queryFn: () => apiClient.getProfile(profileId),\n    enabled: !!profileId,\n  });\n}\n\nexport function useCreateProfile() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (data: VoiceProfileCreate) => apiClient.createProfile(data),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['profiles'] });\n    },\n  });\n}\n\nexport function useUpdateProfile() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ profileId, data }: { profileId: string; data: VoiceProfileCreate }) =>\n      apiClient.updateProfile(profileId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['profiles'] });\n      queryClient.invalidateQueries({\n        queryKey: ['profiles', variables.profileId],\n      });\n    },\n  });\n}\n\nexport function useDeleteProfile() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (profileId: string) => apiClient.deleteProfile(profileId),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['profiles'] });\n    },\n  });\n}\n\nexport function useProfileSamples(profileId: string) {\n  return useQuery({\n    queryKey: ['profiles', profileId, 'samples'],\n    queryFn: () => apiClient.listProfileSamples(profileId),\n    enabled: !!profileId,\n  });\n}\n\nexport function useAddSample() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({\n      profileId,\n      file,\n      referenceText,\n    }: {\n      profileId: string;\n      file: File;\n      referenceText: string;\n    }) => apiClient.addProfileSample(profileId, file, referenceText),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({\n        queryKey: ['profiles', variables.profileId, 'samples'],\n      });\n      queryClient.invalidateQueries({\n        queryKey: ['profiles', variables.profileId],\n      });\n    },\n  });\n}\n\nexport function useDeleteSample() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (sampleId: string) => apiClient.deleteProfileSample(sampleId),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['profiles'] });\n    },\n  });\n}\n\nexport function useUpdateSample() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ sampleId, referenceText }: { sampleId: string; referenceText: string }) =>\n      apiClient.updateProfileSample(sampleId, referenceText),\n    onSuccess: (data) => {\n      queryClient.invalidateQueries({\n        queryKey: ['profiles', data.profile_id, 'samples'],\n      });\n      queryClient.invalidateQueries({\n        queryKey: ['profiles', data.profile_id],\n      });\n      queryClient.invalidateQueries({ queryKey: ['profiles'] });\n    },\n  });\n}\n\nexport function useExportProfile() {\n  const platform = usePlatform();\n\n  return useMutation({\n    mutationFn: async (profileId: string) => {\n      const blob = await apiClient.exportProfile(profileId);\n\n      // Get profile name for filename\n      const profile = await apiClient.getProfile(profileId);\n      const safeName = profile.name.replace(/[^a-z0-9]/gi, '-').toLowerCase();\n      const filename = `profile-${safeName}.voicebox.zip`;\n\n      await platform.filesystem.saveFile(filename, blob, [\n        {\n          name: 'Voicebox Profile',\n          extensions: ['zip'],\n        },\n      ]);\n\n      return blob;\n    },\n  });\n}\n\nexport function useImportProfile() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (file: File) => apiClient.importProfile(file),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['profiles'] });\n    },\n  });\n}\n\nexport function useUploadAvatar() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ profileId, file }: { profileId: string; file: File }) =>\n      apiClient.uploadAvatar(profileId, file),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['profiles'] });\n      queryClient.invalidateQueries({\n        queryKey: ['profiles', variables.profileId],\n      });\n    },\n  });\n}\n\nexport function useDeleteAvatar() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (profileId: string) => apiClient.deleteAvatar(profileId),\n    onSuccess: (_, profileId) => {\n      queryClient.invalidateQueries({ queryKey: ['profiles'] });\n      queryClient.invalidateQueries({\n        queryKey: ['profiles', profileId],\n      });\n    },\n  });\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useRestoreActiveTasks.tsx",
    "content": "import { useCallback, useEffect, useRef, useState } from 'react';\nimport { apiClient } from '@/lib/api/client';\nimport type { ActiveDownloadTask } from '@/lib/api/types';\nimport { useGenerationStore } from '@/stores/generationStore';\n\n// Polling interval in milliseconds\nconst POLL_INTERVAL = 30000;\n\n/**\n * Hook to monitor active tasks (downloads and generations).\n * Polls the server periodically to catch downloads triggered from anywhere\n * (transcription, generation, explicit download, etc.).\n *\n * Returns the active downloads so components can render download toasts.\n */\nexport function useRestoreActiveTasks() {\n  const [activeDownloads, setActiveDownloads] = useState<ActiveDownloadTask[]>([]);\n  const setActiveGenerationId = useGenerationStore((state) => state.setActiveGenerationId);\n  const addPendingGeneration = useGenerationStore((state) => state.addPendingGeneration);\n\n  // Track which downloads we've seen to detect new ones\n  const seenDownloadsRef = useRef<Set<string>>(new Set());\n\n  const fetchActiveTasks = useCallback(async () => {\n    try {\n      const tasks = await apiClient.getActiveTasks();\n\n      // Restore pending generations (e.g., after page refresh)\n      if (tasks.generations.length > 0) {\n        setActiveGenerationId(tasks.generations[0].task_id);\n        for (const gen of tasks.generations) {\n          addPendingGeneration(gen.task_id);\n        }\n      } else {\n        const currentId = useGenerationStore.getState().activeGenerationId;\n        if (currentId) {\n          setActiveGenerationId(null);\n        }\n      }\n\n      // Update active downloads\n      // Keep track of all active downloads (including new ones)\n      const currentDownloadNames = new Set(tasks.downloads.map((d) => d.model_name));\n\n      // Remove completed downloads from our seen set\n      for (const name of seenDownloadsRef.current) {\n        if (!currentDownloadNames.has(name)) {\n          seenDownloadsRef.current.delete(name);\n        }\n      }\n\n      // Add new downloads to seen set\n      for (const download of tasks.downloads) {\n        seenDownloadsRef.current.add(download.model_name);\n      }\n\n      setActiveDownloads(tasks.downloads);\n    } catch (error) {\n      // Silently fail - server might be temporarily unavailable\n      console.debug('Failed to fetch active tasks:', error);\n    }\n  }, [setActiveGenerationId, addPendingGeneration]);\n\n  useEffect(() => {\n    // Fetch immediately on mount\n    fetchActiveTasks();\n\n    // Poll for active tasks\n    const interval = setInterval(fetchActiveTasks, POLL_INTERVAL);\n\n    return () => clearInterval(interval);\n  }, [fetchActiveTasks]);\n\n  return activeDownloads;\n}\n\n/**\n * Map model names to display names for download toasts.\n */\nexport const MODEL_DISPLAY_NAMES: Record<string, string> = {\n  'qwen-tts-1.7B': 'Qwen TTS 1.7B',\n  'qwen-tts-0.6B': 'Qwen TTS 0.6B',\n  'whisper-base': 'Whisper Base',\n  'whisper-small': 'Whisper Small',\n  'whisper-medium': 'Whisper Medium',\n  'whisper-large': 'Whisper Large',\n};\n"
  },
  {
    "path": "app/src/lib/hooks/useServer.ts",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { apiClient } from '@/lib/api/client';\nimport { useServerStore } from '@/stores/serverStore';\n\nexport function useServerHealth() {\n  const serverUrl = useServerStore((state) => state.serverUrl);\n\n  return useQuery({\n    queryKey: ['server', 'health', serverUrl],\n    queryFn: () => apiClient.getHealth(),\n    refetchInterval: 30000, // Check every 30 seconds\n    retry: 1,\n  });\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useStories.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { apiClient } from '@/lib/api/client';\nimport type {\n  StoryCreate,\n  StoryItemBatchUpdate,\n  StoryItemCreate,\n  StoryItemMove,\n  StoryItemReorder,\n  StoryItemSplit,\n  StoryItemTrim,\n  StoryItemVersionUpdate,\n} from '@/lib/api/types';\nimport { usePlatform } from '@/platform/PlatformContext';\n\nexport function useStories() {\n  return useQuery({\n    queryKey: ['stories'],\n    queryFn: () => apiClient.listStories(),\n  });\n}\n\nexport function useStory(storyId: string | null) {\n  return useQuery({\n    queryKey: ['stories', storyId],\n    queryFn: () => apiClient.getStory(storyId!),\n    enabled: !!storyId,\n  });\n}\n\nexport function useCreateStory() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (data: StoryCreate) => apiClient.createStory(data),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n    },\n  });\n}\n\nexport function useUpdateStory() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ storyId, data }: { storyId: string; data: StoryCreate }) =>\n      apiClient.updateStory(storyId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useDeleteStory() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: (storyId: string) => apiClient.deleteStory(storyId),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n    },\n  });\n}\n\nexport function useAddStoryItem() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ storyId, data }: { storyId: string; data: StoryItemCreate }) =>\n      apiClient.addStoryItem(storyId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useRemoveStoryItem() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ storyId, itemId }: { storyId: string; itemId: string }) =>\n      apiClient.removeStoryItem(storyId, itemId),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useUpdateStoryItemTimes() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ storyId, data }: { storyId: string; data: StoryItemBatchUpdate }) =>\n      apiClient.updateStoryItemTimes(storyId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useReorderStoryItems() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ storyId, data }: { storyId: string; data: StoryItemReorder }) =>\n      apiClient.reorderStoryItems(storyId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useMoveStoryItem() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({\n      storyId,\n      itemId,\n      data,\n    }: {\n      storyId: string;\n      itemId: string;\n      data: StoryItemMove;\n    }) => apiClient.moveStoryItem(storyId, itemId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useTrimStoryItem() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({\n      storyId,\n      itemId,\n      data,\n    }: {\n      storyId: string;\n      itemId: string;\n      data: StoryItemTrim;\n    }) => apiClient.trimStoryItem(storyId, itemId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useSplitStoryItem() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({\n      storyId,\n      itemId,\n      data,\n    }: {\n      storyId: string;\n      itemId: string;\n      data: StoryItemSplit;\n    }) => apiClient.splitStoryItem(storyId, itemId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useDuplicateStoryItem() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({ storyId, itemId }: { storyId: string; itemId: string }) =>\n      apiClient.duplicateStoryItem(storyId, itemId),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useSetStoryItemVersion() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: ({\n      storyId,\n      itemId,\n      data,\n    }: {\n      storyId: string;\n      itemId: string;\n      data: StoryItemVersionUpdate;\n    }) => apiClient.setStoryItemVersion(storyId, itemId, data),\n    onSuccess: (_, variables) => {\n      queryClient.invalidateQueries({ queryKey: ['stories'] });\n      queryClient.invalidateQueries({ queryKey: ['stories', variables.storyId] });\n    },\n  });\n}\n\nexport function useExportStoryAudio() {\n  const platform = usePlatform();\n\n  return useMutation({\n    mutationFn: async ({ storyId, storyName }: { storyId: string; storyName: string }) => {\n      const blob = await apiClient.exportStoryAudio(storyId);\n\n      // Create safe filename\n      const safeName = storyName\n        .substring(0, 50)\n        .replace(/[^a-z0-9]/gi, '-')\n        .toLowerCase();\n      const filename = `${safeName || 'story'}.wav`;\n\n      await platform.filesystem.saveFile(filename, blob, [\n        {\n          name: 'Audio File',\n          extensions: ['wav'],\n        },\n      ]);\n\n      return blob;\n    },\n  });\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useStoryPlayback.ts",
    "content": "import { useCallback, useEffect, useRef } from 'react';\nimport { apiClient } from '@/lib/api/client';\nimport type { StoryItemDetail } from '@/lib/api/types';\nimport { useStoryStore } from '@/stores/storyStore';\n\ninterface ActiveSource {\n  source: AudioBufferSourceNode;\n  itemId: string;\n  generationId: string;\n  startTimeMs: number;\n  endTimeMs: number;\n}\n\n/**\n * Hook for managing timecode-based story playback using Web Audio API.\n * Supports multiple simultaneous audio sources for overlapping clips on different tracks.\n * Uses AudioContext for sample-accurate timing synchronization.\n */\nexport function useStoryPlayback(items: StoryItemDetail[] | undefined) {\n  const isPlaying = useStoryStore((state) => state.isPlaying);\n  const playbackItems = useStoryStore((state) => state.playbackItems);\n  const playbackStartContextTime = useStoryStore((state) => state.playbackStartContextTime);\n  const playbackStartStoryTime = useStoryStore((state) => state.playbackStartStoryTime);\n  const setPlaybackTiming = useStoryStore((state) => state.setPlaybackTiming);\n\n  // AudioContext instance (created once)\n  const audioContextRef = useRef<AudioContext | null>(null);\n  // Master gain for volume control\n  const masterGainRef = useRef<GainNode | null>(null);\n  // Preloaded AudioBuffers by generation_id (audio file is shared between split clips)\n  const audioBuffersRef = useRef<Map<string, AudioBuffer>>(new Map());\n  // Currently playing AudioBufferSourceNodes by item.id (unique per clip)\n  const activeSourcesRef = useRef<Map<string, ActiveSource>>(new Map());\n  // Animation frame for syncing visual playhead\n  const animationFrameRef = useRef<number | null>(null);\n\n  // Get or create AudioContext and audio graph\n  const getAudioContext = useCallback(() => {\n    if (!audioContextRef.current) {\n      audioContextRef.current = new AudioContext();\n      console.log(\n        '[StoryPlayback] Created AudioContext, sample rate:',\n        audioContextRef.current.sampleRate,\n      );\n\n      // Create master gain node for volume control\n      masterGainRef.current = audioContextRef.current.createGain();\n      masterGainRef.current.gain.value = 1;\n      masterGainRef.current.connect(audioContextRef.current.destination);\n    }\n    // Resume context if suspended (browser autoplay policy)\n    if (audioContextRef.current.state === 'suspended') {\n      audioContextRef.current.resume().catch(() => {\n        // Ignore resume errors\n      });\n    }\n    return audioContextRef.current;\n  }, []);\n\n  // Stop a source by item id\n  const stopSource = useCallback((itemId: string) => {\n    const activeSource = activeSourcesRef.current.get(itemId);\n    if (activeSource) {\n      try {\n        activeSource.source.stop();\n      } catch {\n        // Source may have already stopped\n      }\n      activeSourcesRef.current.delete(itemId);\n    }\n  }, []);\n\n  // Resolve the audio buffer key and URL for an item.\n  // When a version_id is pinned, use that version's audio; otherwise use the generation default.\n  const getAudioKey = (item: StoryItemDetail) =>\n    item.version_id ? `v:${item.version_id}` : item.generation_id;\n\n  const getAudioUrlForItem = (item: StoryItemDetail) =>\n    item.version_id\n      ? apiClient.getVersionAudioUrl(item.version_id)\n      : apiClient.getAudioUrl(item.generation_id);\n\n  // Preload audio files as AudioBuffers\n  useEffect(() => {\n    if (!items || items.length === 0) {\n      // Clear preloaded buffers when no items\n      audioBuffersRef.current.clear();\n      return;\n    }\n\n    const currentKeys = new Set(items.map(getAudioKey));\n    const audioContext = getAudioContext();\n\n    // Remove buffers for items that no longer exist\n    for (const [id] of audioBuffersRef.current) {\n      if (!currentKeys.has(id)) {\n        audioBuffersRef.current.delete(id);\n      }\n    }\n\n    // Preload audio for new items\n    const preloadPromises: Promise<void>[] = [];\n    for (const item of items) {\n      const key = getAudioKey(item);\n      if (!audioBuffersRef.current.has(key)) {\n        const audioUrl = getAudioUrlForItem(item);\n        console.log('[StoryPlayback] Preloading audio buffer:', key);\n\n        const preloadPromise = fetch(audioUrl)\n          .then((response) => response.arrayBuffer())\n          .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))\n          .then((audioBuffer) => {\n            audioBuffersRef.current.set(key, audioBuffer);\n            console.log(\n              '[StoryPlayback] Preloaded buffer:',\n              key,\n              'duration:',\n              audioBuffer.duration,\n            );\n          })\n          .catch((err) => {\n            console.error('[StoryPlayback] Failed to preload audio:', key, err);\n          });\n\n        preloadPromises.push(preloadPromise);\n      }\n    }\n\n    Promise.all(preloadPromises).then(() => {\n      console.log('[StoryPlayback] Preloaded', audioBuffersRef.current.size, 'audio buffers');\n    });\n  }, [items, getAudioContext]);\n\n  // Cleanup AudioContext on unmount\n  useEffect(() => {\n    return () => {\n      // Stop all sources\n      for (const [itemId] of activeSourcesRef.current) {\n        stopSource(itemId);\n      }\n      activeSourcesRef.current.clear();\n\n      // Clean up audio graph\n      if (masterGainRef.current) {\n        masterGainRef.current.disconnect();\n        masterGainRef.current = null;\n      }\n      if (audioContextRef.current && audioContextRef.current.state !== 'closed') {\n        audioContextRef.current.close().catch(() => {\n          // Ignore errors when closing\n        });\n        audioContextRef.current = null;\n      }\n\n      if (animationFrameRef.current !== null) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, [stopSource]);\n\n  // Find ALL items that should be playing at a given story time\n  const findActiveItems = useCallback(\n    (storyTimeMs: number, itemList: StoryItemDetail[]): StoryItemDetail[] => {\n      return itemList.filter((item) => {\n        const itemStart = item.start_time_ms;\n        // Use effective duration (accounting for trims)\n        const trimStartMs = item.trim_start_ms || 0;\n        const trimEndMs = item.trim_end_ms || 0;\n        const effectiveDurationMs = item.duration * 1000 - trimStartMs - trimEndMs;\n        const itemEnd = item.start_time_ms + effectiveDurationMs;\n        return storyTimeMs >= itemStart && storyTimeMs < itemEnd;\n      });\n    },\n    [],\n  );\n\n  // Convert AudioContext time to story time (ms)\n  const contextTimeToStoryTime = useCallback(\n    (contextTime: number): number => {\n      if (playbackStartContextTime === null || playbackStartStoryTime === null) {\n        return 0;\n      }\n      const elapsedContextTime = contextTime - playbackStartContextTime;\n      return playbackStartStoryTime + elapsedContextTime * 1000;\n    },\n    [playbackStartContextTime, playbackStartStoryTime],\n  );\n\n  // Convert story time (ms) to AudioContext time\n  const storyTimeToContextTime = useCallback(\n    (storyTimeMs: number): number => {\n      if (playbackStartContextTime === null || playbackStartStoryTime === null) {\n        return 0;\n      }\n      const elapsedStoryTime = (storyTimeMs - playbackStartStoryTime) / 1000;\n      return playbackStartContextTime + elapsedStoryTime;\n    },\n    [playbackStartContextTime, playbackStartStoryTime],\n  );\n\n  // Stop all sources\n  const stopAllSources = useCallback(() => {\n    console.log('[StoryPlayback] Stopping all sources');\n    for (const [itemId] of activeSourcesRef.current) {\n      stopSource(itemId);\n    }\n    activeSourcesRef.current.clear();\n  }, [stopSource]);\n\n  // Schedule playback for all items that should be playing\n  const schedulePlayback = useCallback(\n    (storyTimeMs: number, itemList: StoryItemDetail[]) => {\n      const audioContext = getAudioContext();\n      const currentContextTime = audioContext.currentTime;\n\n      // Find all items that should be playing\n      const shouldBePlaying = findActiveItems(storyTimeMs, itemList);\n      const shouldBePlayingIds = new Set(shouldBePlaying.map((item) => item.id));\n\n      // Stop sources that shouldn't be playing anymore\n      for (const [itemId] of activeSourcesRef.current) {\n        if (!shouldBePlayingIds.has(itemId)) {\n          stopSource(itemId);\n        }\n      }\n\n      // Schedule new sources for items that should be playing\n      for (const item of shouldBePlaying) {\n        if (!activeSourcesRef.current.has(item.id)) {\n          const bufferKey = getAudioKey(item);\n          const buffer = audioBuffersRef.current.get(bufferKey);\n          if (!buffer) {\n            console.warn('[StoryPlayback] Buffer not loaded for:', bufferKey);\n            continue;\n          }\n\n          // Calculate when this item should start in AudioContext time\n          const itemStartContextTime = storyTimeToContextTime(item.start_time_ms);\n\n          // Calculate effective duration and trim offsets\n          const trimStartSec = (item.trim_start_ms || 0) / 1000;\n          const trimEndSec = (item.trim_end_ms || 0) / 1000;\n          const effectiveDuration = item.duration - trimStartSec - trimEndSec;\n          const itemEndStoryTime = item.start_time_ms + effectiveDuration * 1000;\n\n          // Calculate offset into the buffer (if seeking mid-way)\n          // Offset is relative to the trimmed start of the clip\n          const offsetIntoEffectiveClip = Math.max(0, (storyTimeMs - item.start_time_ms) / 1000);\n          const offsetIntoBuffer = trimStartSec + offsetIntoEffectiveClip;\n          const duration = effectiveDuration - offsetIntoEffectiveClip;\n\n          // If the item should have already started, schedule it to start immediately\n          const startAtContextTime = Math.max(currentContextTime, itemStartContextTime);\n\n          console.log('[StoryPlayback] Scheduling source:', {\n            itemId: item.id,\n            generationId: item.generation_id,\n            storyTimeMs,\n            itemStart: item.start_time_ms,\n            offsetIntoBuffer,\n            startAtContextTime,\n            duration,\n          });\n\n          const source = audioContext.createBufferSource();\n          source.buffer = buffer;\n          source.connect(masterGainRef.current || audioContext.destination);\n\n          const activeSource: ActiveSource = {\n            source,\n            itemId: item.id,\n            generationId: item.generation_id,\n            startTimeMs: item.start_time_ms,\n            endTimeMs: itemEndStoryTime,\n          };\n\n          activeSourcesRef.current.set(item.id, activeSource);\n\n          // Schedule playback\n          source.start(startAtContextTime, offsetIntoBuffer, duration);\n\n          // Clean up when source ends\n          source.onended = () => {\n            console.log('[StoryPlayback] Source ended:', item.id);\n            activeSourcesRef.current.delete(item.id);\n          };\n        }\n      }\n    },\n    [getAudioContext, findActiveItems, storyTimeToContextTime, stopSource],\n  );\n\n  // Sync visual playhead from AudioContext time\n  useEffect(() => {\n    if (!isPlaying || playbackStartContextTime === null || playbackStartStoryTime === null) {\n      if (animationFrameRef.current !== null) {\n        cancelAnimationFrame(animationFrameRef.current);\n        animationFrameRef.current = null;\n      }\n      return;\n    }\n\n    const audioContext = getAudioContext();\n    const itemList = playbackItems || [];\n\n    const syncPlayhead = () => {\n      if (!useStoryStore.getState().isPlaying) {\n        return;\n      }\n\n      const currentContextTime = audioContext.currentTime;\n      const currentStoryTime = contextTimeToStoryTime(currentContextTime);\n      const totalDuration = useStoryStore.getState().totalDurationMs;\n\n      // Update store with current story time\n      useStoryStore.setState({ currentTimeMs: Math.min(currentStoryTime, totalDuration) });\n\n      // Schedule any items that should be playing\n      schedulePlayback(currentStoryTime, itemList);\n\n      // Check if we've reached the end\n      if (currentStoryTime >= totalDuration) {\n        // Check if all sources have ended\n        if (activeSourcesRef.current.size === 0) {\n          console.log('[StoryPlayback] Reached end');\n          useStoryStore.getState().stop();\n          return;\n        }\n      }\n\n      // Continue sync loop\n      animationFrameRef.current = requestAnimationFrame(syncPlayhead);\n    };\n\n    // Initial sync\n    const currentContextTime = audioContext.currentTime;\n    const currentStoryTime = contextTimeToStoryTime(currentContextTime);\n    schedulePlayback(currentStoryTime, itemList);\n\n    // Start sync loop\n    animationFrameRef.current = requestAnimationFrame(syncPlayhead);\n\n    return () => {\n      if (animationFrameRef.current !== null) {\n        cancelAnimationFrame(animationFrameRef.current);\n        animationFrameRef.current = null;\n      }\n    };\n  }, [\n    isPlaying,\n    playbackItems,\n    playbackStartContextTime,\n    playbackStartStoryTime,\n    getAudioContext,\n    contextTimeToStoryTime,\n    schedulePlayback,\n  ]);\n\n  // Handle play/pause changes - stop sources when paused\n  useEffect(() => {\n    if (!isPlaying) {\n      console.log('[StoryPlayback] Stopping playback');\n      stopAllSources();\n    }\n  }, [isPlaying, stopAllSources]);\n\n  // Handle seek - reset timing anchors when they become null (triggered by seek)\n  useEffect(() => {\n    if (!isPlaying || !playbackItems || playbackItems.length === 0) {\n      return;\n    }\n\n    // Only run when timing anchors are null (after a seek)\n    if (playbackStartContextTime !== null && playbackStartStoryTime !== null) {\n      return;\n    }\n\n    const audioContext = getAudioContext();\n    const currentContextTime = audioContext.currentTime;\n    const currentStoryTime = useStoryStore.getState().currentTimeMs;\n\n    console.log('[StoryPlayback] Setting timing anchors after seek:', {\n      contextTime: currentContextTime,\n      storyTime: currentStoryTime,\n    });\n    setPlaybackTiming(currentContextTime, currentStoryTime);\n\n    // Stop all existing sources and reschedule from new position\n    stopAllSources();\n    schedulePlayback(currentStoryTime, playbackItems);\n  }, [\n    isPlaying,\n    playbackItems,\n    playbackStartContextTime,\n    playbackStartStoryTime,\n    getAudioContext,\n    stopAllSources,\n    schedulePlayback,\n    setPlaybackTiming,\n  ]);\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useSystemAudioCapture.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from 'react';\nimport { usePlatform } from '@/platform/PlatformContext';\n\ninterface UseSystemAudioCaptureOptions {\n  maxDurationSeconds?: number;\n  onRecordingComplete?: (blob: Blob, duration?: number) => void;\n}\n\n/**\n * Hook for native system audio capture using Tauri commands.\n * Uses ScreenCaptureKit on macOS and WASAPI loopback on Windows.\n */\nexport function useSystemAudioCapture({\n  maxDurationSeconds = 29,\n  onRecordingComplete,\n}: UseSystemAudioCaptureOptions = {}) {\n  const platform = usePlatform();\n  const [isRecording, setIsRecording] = useState(false);\n  const [duration, setDuration] = useState(0);\n  const [error, setError] = useState<string | null>(null);\n  const [isSupported, setIsSupported] = useState(false);\n  const timerRef = useRef<number | null>(null);\n  const startTimeRef = useRef<number | null>(null);\n  const stopRecordingRef = useRef<(() => Promise<void>) | null>(null);\n  const isRecordingRef = useRef(false);\n\n  // Check if system audio capture is supported\n  useEffect(() => {\n    const supported = platform.audio.isSystemAudioSupported();\n    setIsSupported(supported);\n  }, [platform]);\n\n  const startRecording = useCallback(async () => {\n    if (!platform.metadata.isTauri) {\n      const errorMsg = 'System audio capture is only available in the desktop app.';\n      setError(errorMsg);\n      return;\n    }\n\n    if (!isSupported) {\n      const errorMsg = 'System audio capture is not supported on this platform.';\n      setError(errorMsg);\n      return;\n    }\n\n    try {\n      setError(null);\n      setDuration(0);\n\n      // Start native capture\n      await platform.audio.startSystemAudioCapture(maxDurationSeconds);\n\n      setIsRecording(true);\n      isRecordingRef.current = true;\n      startTimeRef.current = Date.now();\n\n      // Start timer\n      timerRef.current = window.setInterval(() => {\n        if (startTimeRef.current) {\n          const elapsed = (Date.now() - startTimeRef.current) / 1000;\n          setDuration(elapsed);\n\n          // Auto-stop at max duration\n          if (elapsed >= maxDurationSeconds && stopRecordingRef.current) {\n            void stopRecordingRef.current();\n          }\n        }\n      }, 100);\n    } catch (err) {\n      const errorMessage =\n        err instanceof Error\n          ? err.message\n          : 'Failed to start system audio capture. Please check permissions.';\n      setError(errorMessage);\n      setIsRecording(false);\n    }\n  }, [maxDurationSeconds, isSupported, platform]);\n\n  const stopRecording = useCallback(async () => {\n    if (!isRecording || !platform.metadata.isTauri) {\n      return;\n    }\n\n    try {\n      setIsRecording(false);\n      isRecordingRef.current = false;\n\n      if (timerRef.current !== null) {\n        clearInterval(timerRef.current);\n        timerRef.current = null;\n      }\n\n      // Stop capture and get Blob\n      const blob = await platform.audio.stopSystemAudioCapture();\n\n      // Pass the actual recorded duration\n      const recordedDuration = startTimeRef.current\n        ? (Date.now() - startTimeRef.current) / 1000\n        : undefined;\n      onRecordingComplete?.(blob, recordedDuration);\n    } catch (err) {\n      const errorMessage =\n        err instanceof Error ? err.message : 'Failed to stop system audio capture.';\n      setError(errorMessage);\n    }\n  }, [isRecording, onRecordingComplete, platform]);\n\n  // Store stopRecording in ref for use in timer\n  useEffect(() => {\n    stopRecordingRef.current = stopRecording;\n  }, [stopRecording]);\n\n  const cancelRecording = useCallback(async () => {\n    if (isRecordingRef.current) {\n      await stopRecording();\n    }\n\n    setIsRecording(false);\n    isRecordingRef.current = false;\n    setDuration(0);\n\n    if (timerRef.current !== null) {\n      clearInterval(timerRef.current);\n      timerRef.current = null;\n    }\n  }, [stopRecording]);\n\n  // Cleanup on unmount only\n  useEffect(() => {\n    return () => {\n      if (timerRef.current !== null) {\n        clearInterval(timerRef.current);\n        timerRef.current = null;\n      }\n      // Cancel recording on unmount if still recording\n      if (isRecordingRef.current && platform.metadata.isTauri) {\n        // Call stop directly without the callback to avoid stale closure\n        platform.audio.stopSystemAudioCapture().catch((err) => {\n          console.error('Error stopping audio capture on unmount:', err);\n        });\n      }\n    };\n    // biome-ignore lint/correctness/useExhaustiveDependencies: Only run on unmount\n  }, [platform]);\n\n  return {\n    isRecording,\n    duration,\n    error,\n    isSupported,\n    startRecording,\n    stopRecording,\n    cancelRecording,\n  };\n}\n"
  },
  {
    "path": "app/src/lib/hooks/useTranscription.ts",
    "content": "import { useMutation } from '@tanstack/react-query';\nimport { apiClient } from '@/lib/api/client';\nimport type { WhisperModelSize } from '@/lib/api/types';\nimport type { LanguageCode } from '@/lib/constants/languages';\n\nexport function useTranscription() {\n  return useMutation({\n    mutationFn: ({\n      file,\n      language,\n      model,\n    }: {\n      file: File;\n      language?: LanguageCode;\n      model?: WhisperModelSize;\n    }) => apiClient.transcribeAudio(file, language, model),\n  });\n}\n"
  },
  {
    "path": "app/src/lib/utils/.gitkeep",
    "content": "# Utility functions will be placed here\n"
  },
  {
    "path": "app/src/lib/utils/audio.ts",
    "content": "export function createAudioUrl(audioId: string, serverUrl: string): string {\n  return `${serverUrl}/audio/${audioId}`;\n}\n\nexport function downloadAudio(url: string, filename: string): void {\n  const link = document.createElement('a');\n  link.href = url;\n  link.download = filename;\n  document.body.appendChild(link);\n  link.click();\n  document.body.removeChild(link);\n}\n\nexport function formatAudioDuration(seconds: number): string {\n  const mins = Math.floor(seconds / 60);\n  const secs = Math.floor(seconds % 60);\n  return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\n/**\n * Get audio duration from a File.\n * If the file has a recordedDuration property (from recording hooks),\n * use that instead of trying to read metadata. This fixes issues on Windows\n * where WebM files from MediaRecorder don't have proper duration metadata.\n *\n * For uploaded files we use AudioContext.decodeAudioData which fully decodes\n * the audio and returns the exact duration. This is more reliable than\n * HTMLMediaElement.duration which can return incorrect large values for VBR\n * MP3 files that lack a proper XING/VBRI header.\n */\nexport async function getAudioDuration(\n  file: File & { recordedDuration?: number },\n): Promise<number> {\n  if (file.recordedDuration !== undefined && Number.isFinite(file.recordedDuration)) {\n    return file.recordedDuration;\n  }\n\n  // Use Web Audio API for accurate duration — avoids VBR MP3 metadata issues.\n  try {\n    const audioContext = new AudioContext();\n    try {\n      const arrayBuffer = await file.arrayBuffer();\n      const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n      return audioBuffer.duration;\n    } finally {\n      await audioContext.close();\n    }\n  } catch {\n    // Fallback: read duration from the media element (less accurate but works for WAV).\n    return new Promise((resolve, reject) => {\n      const audio = new Audio();\n      const url = URL.createObjectURL(file);\n\n      audio.addEventListener('loadedmetadata', () => {\n        URL.revokeObjectURL(url);\n        if (Number.isFinite(audio.duration) && audio.duration > 0) {\n          resolve(audio.duration);\n        } else {\n          reject(new Error('Audio file has invalid duration metadata'));\n        }\n      });\n\n      audio.addEventListener('error', () => {\n        URL.revokeObjectURL(url);\n        reject(new Error('Failed to load audio file'));\n      });\n\n      audio.src = url;\n    });\n  }\n}\n\n/**\n * Convert any audio blob to WAV format using Web Audio API.\n * This ensures compatibility without requiring ffmpeg on the backend.\n */\nexport async function convertToWav(audioBlob: Blob): Promise<Blob> {\n  // Create audio context\n  const audioContext = new AudioContext();\n\n  // Read blob as array buffer\n  const arrayBuffer = await audioBlob.arrayBuffer();\n\n  // Decode audio data\n  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n\n  // Convert to WAV\n  const wavBlob = audioBufferToWav(audioBuffer);\n\n  // Close audio context to free resources\n  await audioContext.close();\n\n  return wavBlob;\n}\n\n/**\n * Convert AudioBuffer to WAV blob.\n */\nfunction audioBufferToWav(buffer: AudioBuffer): Blob {\n  const numberOfChannels = buffer.numberOfChannels;\n  const sampleRate = buffer.sampleRate;\n  const format = 1; // PCM\n  const bitDepth = 16;\n\n  const bytesPerSample = bitDepth / 8;\n  const blockAlign = numberOfChannels * bytesPerSample;\n\n  // Interleave channels\n  const interleaved = interleaveChannels(buffer);\n\n  // Create WAV file\n  const dataLength = interleaved.length * bytesPerSample;\n  const buffer2 = new ArrayBuffer(44 + dataLength);\n  const view = new DataView(buffer2);\n\n  // Write WAV header\n  writeString(view, 0, 'RIFF');\n  view.setUint32(4, 36 + dataLength, true);\n  writeString(view, 8, 'WAVE');\n  writeString(view, 12, 'fmt ');\n  view.setUint32(16, 16, true); // fmt chunk size\n  view.setUint16(20, format, true); // audio format (PCM)\n  view.setUint16(22, numberOfChannels, true);\n  view.setUint32(24, sampleRate, true);\n  view.setUint32(28, sampleRate * blockAlign, true); // byte rate\n  view.setUint16(32, blockAlign, true);\n  view.setUint16(34, bitDepth, true);\n  writeString(view, 36, 'data');\n  view.setUint32(40, dataLength, true);\n\n  // Write audio data\n  floatTo16BitPCM(view, 44, interleaved);\n\n  return new Blob([buffer2], { type: 'audio/wav' });\n}\n\n/**\n * Interleave multiple channels into a single array.\n */\nfunction interleaveChannels(buffer: AudioBuffer): Float32Array {\n  const numberOfChannels = buffer.numberOfChannels;\n  const length = buffer.length;\n  const interleaved = new Float32Array(length * numberOfChannels);\n\n  for (let channel = 0; channel < numberOfChannels; channel++) {\n    const channelData = buffer.getChannelData(channel);\n    for (let i = 0; i < length; i++) {\n      interleaved[i * numberOfChannels + channel] = channelData[i];\n    }\n  }\n\n  return interleaved;\n}\n\n/**\n * Write string to DataView.\n */\nfunction writeString(view: DataView, offset: number, string: string): void {\n  for (let i = 0; i < string.length; i++) {\n    view.setUint8(offset + i, string.charCodeAt(i));\n  }\n}\n\n/**\n * Convert float32 audio data to 16-bit PCM.\n */\nfunction floatTo16BitPCM(view: DataView, offset: number, input: Float32Array): void {\n  for (let i = 0; i < input.length; i++, offset += 2) {\n    const s = Math.max(-1, Math.min(1, input[i]));\n    view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);\n  }\n}\n"
  },
  {
    "path": "app/src/lib/utils/cn.ts",
    "content": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "app/src/lib/utils/debug.ts",
    "content": "const DEBUG = import.meta.env.DEV;\n\nexport const debug = {\n  log: (...args: unknown[]) => {\n    if (DEBUG) {\n      console.log(...args);\n    }\n  },\n  error: (...args: unknown[]) => {\n    if (DEBUG) {\n      console.error(...args);\n    }\n  },\n  warn: (...args: unknown[]) => {\n    if (DEBUG) {\n      console.warn(...args);\n    }\n  },\n};\n"
  },
  {
    "path": "app/src/lib/utils/format.ts",
    "content": "import { formatDistance } from 'date-fns';\n\nexport function formatDuration(seconds: number): string {\n  const mins = Math.floor(seconds / 60);\n  const secs = Math.floor(seconds % 60);\n  return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\nexport function formatDate(date: string | Date): string {\n  // Parse the date string - if it doesn't have timezone info, treat it as UTC\n  let dateObj: Date;\n  if (typeof date === 'string') {\n    // If the string doesn't end with Z or have timezone offset, assume it's UTC\n    const dateStr = date.trim();\n    if (!dateStr.includes('Z') && !dateStr.match(/[+-]\\d{2}:\\d{2}$/)) {\n      // No timezone info, treat as UTC\n      dateObj = new Date(dateStr + 'Z');\n    } else {\n      dateObj = new Date(dateStr);\n    }\n  } else {\n    dateObj = date;\n  }\n\n  return formatDistance(dateObj, new Date(), { addSuffix: true }).replace(/^about /i, '');\n}\n\nconst ENGINE_DISPLAY_NAMES: Record<string, string> = {\n  qwen: 'Qwen',\n  luxtts: 'LuxTTS',\n  chatterbox: 'Chatterbox',\n  chatterbox_turbo: 'Chatterbox Turbo',\n};\n\nexport function formatEngineName(engine?: string, modelSize?: string): string {\n  const name = ENGINE_DISPLAY_NAMES[engine ?? 'qwen'] ?? engine ?? 'Qwen';\n  if (engine === 'qwen' && modelSize) {\n    return `${name} ${modelSize}`;\n  }\n  return name;\n}\n\nexport function formatFileSize(bytes: number): string {\n  if (bytes === 0) return '0 Bytes';\n  const k = 1024;\n  const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;\n}\n"
  },
  {
    "path": "app/src/lib/utils/parseChangelog.ts",
    "content": "export interface ChangelogEntry {\n  version: string;\n  date: string | null;\n  body: string;\n}\n\n/**\n * Parses a Keep-a-Changelog style markdown string into structured entries.\n *\n * Splits on `## [version]` headings and extracts the version + date from each.\n * The body is the raw markdown between headings (trimmed), with the leading\n * `# Changelog` title and trailing link references stripped.\n */\nexport function parseChangelog(raw: string): ChangelogEntry[] {\n  const entries: ChangelogEntry[] = [];\n\n  // Strip trailing link reference definitions (e.g. [0.1.0]: https://...)\n  const cleaned = raw.replace(/^\\[[\\w.]+\\]:.*$/gm, '').trimEnd();\n\n  // Match `## [version]` or `## [version] - date`\n  const headingRe = /^## \\[(.+?)\\](?:\\s*-\\s*(.+))?$/gm;\n  const matches = [...cleaned.matchAll(headingRe)];\n\n  for (let i = 0; i < matches.length; i++) {\n    const match = matches[i];\n    const version = match[1];\n    const date = match[2]?.trim() || null;\n\n    const start = match.index! + match[0].length;\n    const end = i + 1 < matches.length ? matches[i + 1].index! : cleaned.length;\n    const body = cleaned.slice(start, end).trim();\n\n    entries.push({ version, date, body });\n  }\n\n  return entries;\n}\n"
  },
  {
    "path": "app/src/main.tsx",
    "content": "import { QueryClient, QueryClientProvider } from '@tanstack/react-query';\n// import { ReactQueryDevtools } from '@tanstack/react-query-devtools';\nimport React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\nimport './index.css';\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 1000 * 60 * 5, // 5 minutes\n      gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime)\n      retry: 1,\n      refetchOnWindowFocus: false,\n    },\n  },\n});\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <React.StrictMode>\n    <QueryClientProvider client={queryClient}>\n      <App />\n      {/* <ReactQueryDevtools initialIsOpen={false} /> */}\n    </QueryClientProvider>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "app/src/platform/PlatformContext.tsx",
    "content": "import { createContext, useContext, type ReactNode } from 'react';\nimport type { Platform } from './types';\n\nconst PlatformContext = createContext<Platform | null>(null);\n\nexport interface PlatformProviderProps {\n  platform: Platform;\n  children: ReactNode;\n}\n\nexport function PlatformProvider({ platform, children }: PlatformProviderProps) {\n  return (\n    <PlatformContext.Provider value={platform}>\n      {children}\n    </PlatformContext.Provider>\n  );\n}\n\nexport function usePlatform(): Platform {\n  const platform = useContext(PlatformContext);\n  if (!platform) {\n    throw new Error('usePlatform must be used within PlatformProvider');\n  }\n  return platform;\n}\n"
  },
  {
    "path": "app/src/platform/types.ts",
    "content": "/**\n * Platform abstraction types\n * These interfaces define the contract that platform implementations must fulfill\n */\n\nexport interface FileFilter {\n  name: string;\n  extensions: string[];\n}\n\nexport interface PlatformFilesystem {\n  saveFile(filename: string, blob: Blob, filters?: FileFilter[]): Promise<void>;\n  openPath(path: string): Promise<void>;\n  pickDirectory(title: string): Promise<string | null>;\n}\n\nexport interface UpdateStatus {\n  checking: boolean;\n  available: boolean;\n  version?: string;\n  downloading: boolean;\n  installing: boolean;\n  readyToInstall: boolean;\n  error?: string;\n  downloadProgress?: number; // 0-100 percentage\n  downloadedBytes?: number;\n  totalBytes?: number;\n}\n\nexport interface PlatformUpdater {\n  checkForUpdates(): Promise<void>;\n  downloadAndInstall(): Promise<void>;\n  restartAndInstall(): Promise<void>;\n  getStatus(): UpdateStatus;\n  subscribe(callback: (status: UpdateStatus) => void): () => void;\n}\n\nexport interface AudioDevice {\n  id: string;\n  name: string;\n  is_default: boolean;\n}\n\nexport interface PlatformAudio {\n  isSystemAudioSupported(): boolean;\n  startSystemAudioCapture(maxDurationSecs: number): Promise<void>;\n  stopSystemAudioCapture(): Promise<Blob>;\n  listOutputDevices(): Promise<AudioDevice[]>;\n  playToDevices(audioData: Uint8Array, deviceIds: string[]): Promise<void>;\n  stopPlayback(): void;\n}\n\nexport interface ServerLogEntry {\n  stream: 'stdout' | 'stderr';\n  line: string;\n}\n\nexport interface PlatformLifecycle {\n  startServer(remote?: boolean, modelsDir?: string | null): Promise<string>;\n  stopServer(): Promise<void>;\n  restartServer(modelsDir?: string | null): Promise<string>;\n  setKeepServerRunning(keep: boolean): Promise<void>;\n  setupWindowCloseHandler(): Promise<void>;\n  subscribeToServerLogs(callback: (entry: ServerLogEntry) => void): () => void;\n  onServerReady?: () => void;\n}\n\nexport interface PlatformMetadata {\n  getVersion(): Promise<string>;\n  isTauri: boolean;\n}\n\nexport interface Platform {\n  filesystem: PlatformFilesystem;\n  updater: PlatformUpdater;\n  audio: PlatformAudio;\n  lifecycle: PlatformLifecycle;\n  metadata: PlatformMetadata;\n}\n"
  },
  {
    "path": "app/src/router.tsx",
    "content": "import {\n  createRootRoute,\n  createRoute,\n  createRouter,\n  Outlet,\n  redirect,\n} from '@tanstack/react-router';\nimport { AppFrame } from '@/components/AppFrame/AppFrame';\nimport { AudioTab } from '@/components/AudioTab/AudioTab';\nimport { EffectsTab } from '@/components/EffectsTab/EffectsTab';\nimport { MainEditor } from '@/components/MainEditor/MainEditor';\nimport { ModelsTab } from '@/components/ModelsTab/ModelsTab';\nimport { AboutPage } from '@/components/ServerTab/AboutPage';\nimport { ChangelogPage } from '@/components/ServerTab/ChangelogPage';\nimport { GeneralPage } from '@/components/ServerTab/GeneralPage';\nimport { GenerationPage } from '@/components/ServerTab/GenerationPage';\nimport { GpuPage } from '@/components/ServerTab/GpuPage';\nimport { LogsPage } from '@/components/ServerTab/LogsPage';\nimport { SettingsLayout } from '@/components/ServerTab/ServerTab';\nimport { Sidebar } from '@/components/Sidebar';\nimport { StoriesTab } from '@/components/StoriesTab/StoriesTab';\nimport { Toaster } from '@/components/ui/toaster';\nimport { VoicesTab } from '@/components/VoicesTab/VoicesTab';\nimport { useGenerationProgress } from '@/lib/hooks/useGenerationProgress';\nimport { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';\nimport { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';\n\n// Simple platform check that works in both web and Tauri\nconst isMacOS = () => navigator.platform.toLowerCase().includes('mac');\n\n// Root layout component\nfunction RootLayout() {\n  // Monitor active downloads/generations and show toasts for them\n  const activeDownloads = useRestoreActiveTasks();\n\n  // Subscribe to SSE for pending generations — handles completion, auto-play, and history refresh\n  useGenerationProgress();\n\n  return (\n    <AppFrame>\n      <div className=\"flex flex-1 min-h-0 overflow-hidden\">\n        <Sidebar isMacOS={isMacOS()} />\n\n        <main className=\"flex-1 ml-20 overflow-hidden flex flex-col\">\n          <div className=\"container mx-auto px-8 max-w-[1800px] h-full overflow-hidden flex flex-col\">\n            <Outlet />\n          </div>\n        </main>\n      </div>\n\n      {/* Show download toasts for any active downloads (from anywhere) */}\n      {activeDownloads.map((download) => {\n        const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name;\n        return (\n          <DownloadToastRestorer\n            key={download.model_name}\n            modelName={download.model_name}\n            displayName={displayName}\n          />\n        );\n      })}\n\n      <Toaster />\n    </AppFrame>\n  );\n}\n\n/**\n * Component that restores a download toast for a specific model.\n */\nfunction DownloadToastRestorer({\n  modelName,\n  displayName,\n}: {\n  modelName: string;\n  displayName: string;\n}) {\n  // Use the download toast hook to restore the toast\n  useModelDownloadToast({\n    modelName,\n    displayName,\n    enabled: true,\n  });\n\n  return null;\n}\n\n// Root route with layout\nconst rootRoute = createRootRoute({\n  component: RootLayout,\n});\n\n// Index route (main/generate)\nconst indexRoute = createRoute({\n  getParentRoute: () => rootRoute,\n  path: '/',\n  component: MainEditor,\n});\n\n// Stories route\nconst storiesRoute = createRoute({\n  getParentRoute: () => rootRoute,\n  path: '/stories',\n  component: StoriesTab,\n});\n\n// Voices route\nconst voicesRoute = createRoute({\n  getParentRoute: () => rootRoute,\n  path: '/voices',\n  component: VoicesTab,\n});\n\n// Audio route\nconst audioRoute = createRoute({\n  getParentRoute: () => rootRoute,\n  path: '/audio',\n  component: AudioTab,\n});\n\n// Effects route\nconst effectsRoute = createRoute({\n  getParentRoute: () => rootRoute,\n  path: '/effects',\n  component: EffectsTab,\n});\n\n// Models route\nconst modelsRoute = createRoute({\n  getParentRoute: () => rootRoute,\n  path: '/models',\n  component: ModelsTab,\n});\n\n// Settings layout route (parent for sub-tabs)\nconst settingsRoute = createRoute({\n  getParentRoute: () => rootRoute,\n  path: '/settings',\n  component: SettingsLayout,\n});\n\n// Settings sub-routes\nconst settingsGeneralRoute = createRoute({\n  getParentRoute: () => settingsRoute,\n  path: '/',\n  component: GeneralPage,\n});\n\nconst settingsGenerationRoute = createRoute({\n  getParentRoute: () => settingsRoute,\n  path: '/generation',\n  component: GenerationPage,\n});\n\nconst settingsGpuRoute = createRoute({\n  getParentRoute: () => settingsRoute,\n  path: '/gpu',\n  component: GpuPage,\n});\n\nconst settingsChangelogRoute = createRoute({\n  getParentRoute: () => settingsRoute,\n  path: '/changelog',\n  component: ChangelogPage,\n});\n\nconst settingsLogsRoute = createRoute({\n  getParentRoute: () => settingsRoute,\n  path: '/logs',\n  component: LogsPage,\n});\n\nconst settingsAboutRoute = createRoute({\n  getParentRoute: () => settingsRoute,\n  path: '/about',\n  component: AboutPage,\n});\n\n// Redirect old /server path to /settings\nconst serverRedirectRoute = createRoute({\n  getParentRoute: () => rootRoute,\n  path: '/server',\n  beforeLoad: () => {\n    throw redirect({ to: '/settings' });\n  },\n});\n\n// Route tree\nconst routeTree = rootRoute.addChildren([\n  indexRoute,\n  storiesRoute,\n  voicesRoute,\n  audioRoute,\n  effectsRoute,\n  modelsRoute,\n  settingsRoute.addChildren([\n    settingsGeneralRoute,\n    settingsGenerationRoute,\n    settingsGpuRoute,\n    settingsLogsRoute,\n    settingsChangelogRoute,\n    settingsAboutRoute,\n  ]),\n  serverRedirectRoute,\n]);\n\n// Create router\nexport const router = createRouter({ routeTree });\n\n// Register router for type safety\ndeclare module '@tanstack/react-router' {\n  interface Register {\n    router: typeof router;\n  }\n}\n"
  },
  {
    "path": "app/src/stores/audioChannelStore.ts",
    "content": "import { create } from 'zustand';\nimport { persist } from 'zustand/middleware';\n\nexport interface AudioChannel {\n  id: string;\n  name: string;\n  is_default: boolean;\n  device_ids: string[];\n  created_at: string;\n}\n\ninterface AudioChannelStore {\n  channels: AudioChannel[];\n  setChannels: (channels: AudioChannel[]) => void;\n  addChannel: (channel: AudioChannel) => void;\n  updateChannel: (id: string, channel: Partial<AudioChannel>) => void;\n  removeChannel: (id: string) => void;\n}\n\nexport const useAudioChannelStore = create<AudioChannelStore>()(\n  persist(\n    (set) => ({\n      channels: [],\n      setChannels: (channels) => set({ channels }),\n      addChannel: (channel) =>\n        set((state) => ({\n          channels: [...state.channels, channel],\n        })),\n      updateChannel: (id, updates) =>\n        set((state) => ({\n          channels: state.channels.map((ch) => (ch.id === id ? { ...ch, ...updates } : ch)),\n        })),\n      removeChannel: (id) =>\n        set((state) => ({\n          channels: state.channels.filter((ch) => ch.id !== id),\n        })),\n    }),\n    {\n      name: 'voicebox-audio-channels',\n    },\n  ),\n);\n"
  },
  {
    "path": "app/src/stores/effectsStore.ts",
    "content": "import { create } from 'zustand';\nimport type { EffectConfig } from '@/lib/api/types';\n\ninterface EffectsStore {\n  selectedPresetId: string | null;\n  setSelectedPresetId: (id: string | null) => void;\n\n  // Working chain for the detail panel (editing a preset or building a new one)\n  workingChain: EffectConfig[];\n  setWorkingChain: (chain: EffectConfig[]) => void;\n\n  // Track if editing an existing preset vs creating new\n  isCreatingNew: boolean;\n  setIsCreatingNew: (v: boolean) => void;\n}\n\nexport const useEffectsStore = create<EffectsStore>((set) => ({\n  selectedPresetId: null,\n  setSelectedPresetId: (id) => set({ selectedPresetId: id, isCreatingNew: false }),\n\n  workingChain: [],\n  setWorkingChain: (chain) => set({ workingChain: chain }),\n\n  isCreatingNew: false,\n  setIsCreatingNew: (v) => set({ isCreatingNew: v, ...(v && { selectedPresetId: null }) }),\n}));\n"
  },
  {
    "path": "app/src/stores/generationStore.ts",
    "content": "import { create } from 'zustand';\n\ninterface GenerationState {\n  /** IDs of generations currently in progress */\n  pendingGenerationIds: Set<string>;\n  /** Whether any generation is in progress (derived from pendingGenerationIds) */\n  isGenerating: boolean;\n  /** Map of generationId → storyId for deferred story additions */\n  pendingStoryAdds: Map<string, string>;\n  addPendingGeneration: (id: string) => void;\n  removePendingGeneration: (id: string) => void;\n  addPendingStoryAdd: (generationId: string, storyId: string) => void;\n  removePendingStoryAdd: (generationId: string) => string | undefined;\n  setActiveGenerationId: (id: string | null) => void;\n  activeGenerationId: string | null;\n}\n\nexport const useGenerationStore = create<GenerationState>((set, get) => ({\n  pendingGenerationIds: new Set(),\n  isGenerating: false,\n  activeGenerationId: null,\n  pendingStoryAdds: new Map(),\n\n  addPendingGeneration: (id) =>\n    set((state) => {\n      const next = new Set(state.pendingGenerationIds);\n      next.add(id);\n      return { pendingGenerationIds: next, isGenerating: true };\n    }),\n\n  removePendingGeneration: (id) =>\n    set((state) => {\n      const next = new Set(state.pendingGenerationIds);\n      next.delete(id);\n      return { pendingGenerationIds: next, isGenerating: next.size > 0 };\n    }),\n\n  addPendingStoryAdd: (generationId, storyId) =>\n    set((state) => {\n      const next = new Map(state.pendingStoryAdds);\n      next.set(generationId, storyId);\n      return { pendingStoryAdds: next };\n    }),\n\n  removePendingStoryAdd: (generationId) => {\n    const storyId = get().pendingStoryAdds.get(generationId);\n    if (storyId) {\n      set((state) => {\n        const next = new Map(state.pendingStoryAdds);\n        next.delete(generationId);\n        return { pendingStoryAdds: next };\n      });\n    }\n    return storyId;\n  },\n\n  setActiveGenerationId: (id) => set({ activeGenerationId: id }),\n}));\n"
  },
  {
    "path": "app/src/stores/logStore.ts",
    "content": "import { create } from 'zustand';\nimport type { ServerLogEntry } from '@/platform/types';\n\nconst MAX_LOG_ENTRIES = 2000;\n\nlet nextLogEntryId = 0;\n\nexport interface LogEntry extends ServerLogEntry {\n  id: number;\n  timestamp: number;\n}\n\ninterface LogStore {\n  entries: LogEntry[];\n  addEntry: (entry: ServerLogEntry) => void;\n  clear: () => void;\n}\n\nexport const useLogStore = create<LogStore>((set) => ({\n  entries: [],\n  addEntry: (entry) =>\n    set((state) => {\n      const newEntry: LogEntry = { ...entry, id: nextLogEntryId++, timestamp: Date.now() };\n      const entries = [...state.entries, newEntry];\n      if (entries.length > MAX_LOG_ENTRIES) {\n        return { entries: entries.slice(entries.length - MAX_LOG_ENTRIES) };\n      }\n      return { entries };\n    }),\n  clear: () => set({ entries: [] }),\n}));\n"
  },
  {
    "path": "app/src/stores/playerStore.ts",
    "content": "import { create } from 'zustand';\n\ninterface PlayerState {\n  audioUrl: string | null;\n  audioId: string | null;\n  profileId: string | null;\n  title: string | null;\n  isPlaying: boolean;\n  currentTime: number;\n  duration: number;\n  volume: number;\n  isLooping: boolean;\n  shouldRestart: boolean;\n  shouldAutoPlay: boolean;\n  onFinish: (() => void) | null;\n\n  setAudio: (url: string, id: string, profileId: string | null, title?: string) => void;\n  setAudioWithAutoPlay: (url: string, id: string, profileId: string | null, title?: string) => void;\n  setIsPlaying: (playing: boolean) => void;\n  setCurrentTime: (time: number) => void;\n  setDuration: (duration: number) => void;\n  setVolume: (volume: number) => void;\n  toggleLoop: () => void;\n  restartCurrentAudio: () => void;\n  clearRestartFlag: () => void;\n  clearAutoPlayFlag: () => void;\n  setOnFinish: (callback: (() => void) | null) => void;\n  reset: () => void;\n}\n\nexport const usePlayerStore = create<PlayerState>((set) => ({\n  audioUrl: null,\n  audioId: null,\n  profileId: null,\n  title: null,\n  isPlaying: false,\n  currentTime: 0,\n  duration: 0,\n  volume: 1,\n  isLooping: false,\n  shouldRestart: false,\n  shouldAutoPlay: false,\n  onFinish: null,\n\n  setAudio: (url, id, profileId, title) =>\n    set({\n      audioUrl: url,\n      audioId: id,\n      profileId: profileId || null,\n      title: title || null,\n      currentTime: 0,\n      isPlaying: false,\n      shouldRestart: false,\n      shouldAutoPlay: false,\n    }),\n  setAudioWithAutoPlay: (url, id, profileId, title) =>\n    set({\n      audioUrl: url,\n      audioId: id,\n      profileId: profileId || null,\n      title: title || null,\n      currentTime: 0,\n      isPlaying: false,\n      shouldRestart: false,\n      shouldAutoPlay: true,\n    }),\n  setIsPlaying: (playing) => set({ isPlaying: playing }),\n  setCurrentTime: (time) => set({ currentTime: time }),\n  setDuration: (duration) => set({ duration }),\n  setVolume: (volume) => set({ volume }),\n  toggleLoop: () => set((state) => ({ isLooping: !state.isLooping })),\n  restartCurrentAudio: () => set({ shouldRestart: true }),\n  clearRestartFlag: () => set({ shouldRestart: false }),\n  clearAutoPlayFlag: () => set({ shouldAutoPlay: false }),\n  setOnFinish: (callback) => set({ onFinish: callback }),\n  reset: () =>\n    set({\n      audioUrl: null,\n      audioId: null,\n      profileId: null,\n      title: null,\n      isPlaying: false,\n      currentTime: 0,\n      duration: 0,\n      isLooping: false,\n      shouldRestart: false,\n      shouldAutoPlay: false,\n      onFinish: null,\n    }),\n}));\n"
  },
  {
    "path": "app/src/stores/serverStore.ts",
    "content": "import { create } from 'zustand';\nimport { persist } from 'zustand/middleware';\n\ninterface ServerStore {\n  serverUrl: string;\n  setServerUrl: (url: string) => void;\n\n  isConnected: boolean;\n  setIsConnected: (connected: boolean) => void;\n\n  mode: 'local' | 'remote';\n  setMode: (mode: 'local' | 'remote') => void;\n\n  keepServerRunningOnClose: boolean;\n  setKeepServerRunningOnClose: (keepRunning: boolean) => void;\n\n  maxChunkChars: number;\n  setMaxChunkChars: (value: number) => void;\n\n  crossfadeMs: number;\n  setCrossfadeMs: (value: number) => void;\n\n  normalizeAudio: boolean;\n  setNormalizeAudio: (value: boolean) => void;\n\n  autoplayOnGenerate: boolean;\n  setAutoplayOnGenerate: (value: boolean) => void;\n\n  customModelsDir: string | null;\n  setCustomModelsDir: (dir: string | null) => void;\n}\n\nexport const useServerStore = create<ServerStore>()(\n  persist(\n    (set) => ({\n      serverUrl: 'http://127.0.0.1:17493',\n      setServerUrl: (url) => set({ serverUrl: url }),\n\n      isConnected: false,\n      setIsConnected: (connected) => set({ isConnected: connected }),\n\n      mode: 'local',\n      setMode: (mode) => set({ mode }),\n\n      keepServerRunningOnClose: false,\n      setKeepServerRunningOnClose: (keepRunning) => set({ keepServerRunningOnClose: keepRunning }),\n\n      maxChunkChars: 800,\n      setMaxChunkChars: (value) => set({ maxChunkChars: value }),\n\n      crossfadeMs: 50,\n      setCrossfadeMs: (value) => set({ crossfadeMs: value }),\n\n      normalizeAudio: true,\n      setNormalizeAudio: (value) => set({ normalizeAudio: value }),\n\n      autoplayOnGenerate: true,\n      setAutoplayOnGenerate: (value) => set({ autoplayOnGenerate: value }),\n\n      customModelsDir: null,\n      setCustomModelsDir: (dir) => set({ customModelsDir: dir }),\n    }),\n    {\n      name: 'voicebox-server',\n    },\n  ),\n);\n"
  },
  {
    "path": "app/src/stores/storyStore.ts",
    "content": "import { create } from 'zustand';\nimport type { StoryItemDetail } from '@/lib/api/types';\n\ninterface StoryPlaybackState {\n  // Selection\n  selectedStoryId: string | null;\n  setSelectedStoryId: (id: string | null) => void;\n  selectedClipId: string | null;\n  setSelectedClipId: (id: string | null) => void;\n\n  // Track editor UI state\n  trackEditorHeight: number;\n  setTrackEditorHeight: (height: number) => void;\n\n  // Playback state\n  isPlaying: boolean;\n  currentTimeMs: number;\n  totalDurationMs: number;\n  playbackStoryId: string | null;\n  playbackItems: StoryItemDetail[] | null;\n  // Web Audio API timing (null when not playing)\n  playbackStartContextTime: number | null; // AudioContext.currentTime when playback started\n  playbackStartStoryTime: number | null; // Story time (ms) when playback started\n\n  // Actions\n  play: (storyId: string, items: StoryItemDetail[]) => void;\n  pause: () => void;\n  stop: () => void;\n  seek: (timeMs: number) => void;\n  setPlaybackTiming: (contextTime: number, storyTime: number) => void; // Set timing anchors for Web Audio API\n  setActiveStory: (storyId: string, items: StoryItemDetail[], totalDurationMs: number) => void; // Activate story for seeking without playing\n}\n\nconst DEFAULT_TRACK_EDITOR_HEIGHT = 250;\n\nexport const useStoryStore = create<StoryPlaybackState>((set, get) => ({\n  // Selection\n  selectedStoryId: null,\n  setSelectedStoryId: (id) => set({ selectedStoryId: id }),\n  selectedClipId: null,\n  setSelectedClipId: (id) => set({ selectedClipId: id }),\n\n  // Track editor UI state\n  trackEditorHeight: DEFAULT_TRACK_EDITOR_HEIGHT,\n  setTrackEditorHeight: (height) => set({ trackEditorHeight: height }),\n\n  // Playback state\n  isPlaying: false,\n  currentTimeMs: 0,\n  totalDurationMs: 0,\n  playbackStoryId: null,\n  playbackItems: null,\n  playbackStartContextTime: null,\n  playbackStartStoryTime: null,\n\n  // Actions\n  play: (storyId, items) => {\n    // Calculate total duration from items\n    const maxEndTimeMs = Math.max(\n      ...items.map((item) => item.start_time_ms + item.duration * 1000),\n      0,\n    );\n\n    // Find the minimum start time (first item)\n    const minStartTimeMs = Math.min(...items.map((item) => item.start_time_ms), 0);\n\n    // If resuming the same story, keep position; otherwise start at first item\n    const currentState = get();\n    const shouldResume = currentState.playbackStoryId === storyId && currentState.currentTimeMs > 0;\n    const startTimeMs = shouldResume ? currentState.currentTimeMs : minStartTimeMs;\n\n    console.log('[StoryStore] Play called:', {\n      storyId,\n      itemCount: items.length,\n      items: items.map((i) => ({\n        id: i.generation_id,\n        start: i.start_time_ms,\n        duration: i.duration,\n      })),\n      maxEndTimeMs,\n      minStartTimeMs,\n      startTimeMs,\n      shouldResume,\n    });\n\n    set({\n      isPlaying: true,\n      playbackStoryId: storyId,\n      playbackItems: items,\n      totalDurationMs: maxEndTimeMs,\n      currentTimeMs: startTimeMs,\n      // Reset timing anchors - will be set fresh by the playback hook\n      playbackStartContextTime: null,\n      playbackStartStoryTime: null,\n    });\n  },\n\n  pause: () => {\n    set({\n      isPlaying: false,\n      // Keep timing anchors so we can resume from same position\n    });\n  },\n\n  stop: () => {\n    set({\n      isPlaying: false,\n      currentTimeMs: 0,\n      playbackStoryId: null,\n      playbackItems: null,\n      totalDurationMs: 0,\n      playbackStartContextTime: null,\n      playbackStartStoryTime: null,\n    });\n  },\n\n  seek: (timeMs) => {\n    const state = get();\n    const clampedTime = Math.max(0, Math.min(timeMs, state.totalDurationMs));\n    set({\n      currentTimeMs: clampedTime,\n      // Reset timing anchors - will be set by hook when playback resumes\n      playbackStartContextTime: null,\n      playbackStartStoryTime: null,\n    });\n  },\n\n  setPlaybackTiming: (contextTime, storyTime) => {\n    set({\n      playbackStartContextTime: contextTime,\n      playbackStartStoryTime: storyTime,\n    });\n  },\n\n  setActiveStory: (storyId, items, totalDurationMs) => {\n    const currentState = get();\n    // Only update if switching to a different story\n    if (currentState.playbackStoryId !== storyId) {\n      set({\n        playbackStoryId: storyId,\n        playbackItems: items,\n        totalDurationMs,\n        currentTimeMs: 0,\n        isPlaying: false,\n      });\n    }\n  },\n}));\n"
  },
  {
    "path": "app/src/stores/uiStore.ts",
    "content": "import { create } from 'zustand';\n\n// Draft state for the create voice profile form\nexport interface ProfileFormDraft {\n  name: string;\n  description: string;\n  language: string;\n  referenceText: string;\n  sampleMode: 'upload' | 'record' | 'system';\n  // Note: File objects can't be persisted, so we store metadata\n  sampleFileName?: string;\n  sampleFileType?: string;\n  sampleFileData?: string; // Base64 encoded\n}\n\ninterface UIStore {\n  // Sidebar\n  sidebarOpen: boolean;\n  setSidebarOpen: (open: boolean) => void;\n\n  // Modals\n  profileDialogOpen: boolean;\n  setProfileDialogOpen: (open: boolean) => void;\n  editingProfileId: string | null;\n  setEditingProfileId: (id: string | null) => void;\n\n  generationDialogOpen: boolean;\n  setGenerationDialogOpen: (open: boolean) => void;\n\n  // Selected profile for generation\n  selectedProfileId: string | null;\n  setSelectedProfileId: (id: string | null) => void;\n\n  // Currently selected engine (synced from generation form)\n  selectedEngine: string;\n  setSelectedEngine: (engine: string) => void;\n\n  // Selected voice in Voices tab inspector\n  selectedVoiceId: string | null;\n  setSelectedVoiceId: (id: string | null) => void;\n\n  // Profile form draft (for persisting create voice modal state)\n  profileFormDraft: ProfileFormDraft | null;\n  setProfileFormDraft: (draft: ProfileFormDraft | null) => void;\n\n  // Theme\n  theme: 'light' | 'dark';\n  setTheme: (theme: 'light' | 'dark') => void;\n}\n\nexport const useUIStore = create<UIStore>((set) => ({\n  sidebarOpen: true,\n  setSidebarOpen: (open) => set({ sidebarOpen: open }),\n\n  profileDialogOpen: false,\n  setProfileDialogOpen: (open) => set({ profileDialogOpen: open }),\n  editingProfileId: null,\n  setEditingProfileId: (id) => set({ editingProfileId: id }),\n\n  generationDialogOpen: false,\n  setGenerationDialogOpen: (open) => set({ generationDialogOpen: open }),\n\n  selectedProfileId: null,\n  setSelectedProfileId: (id) => set({ selectedProfileId: id }),\n\n  selectedEngine: 'qwen',\n  setSelectedEngine: (engine) => set({ selectedEngine: engine }),\n\n  selectedVoiceId: null,\n  setSelectedVoiceId: (id) => set({ selectedVoiceId: id }),\n\n  profileFormDraft: null,\n  setProfileFormDraft: (draft) => set({ profileFormDraft: draft }),\n\n  theme: 'light',\n  setTheme: (theme) => {\n    set({ theme });\n    document.documentElement.classList.toggle('dark', theme === 'dark');\n  },\n}));\n"
  },
  {
    "path": "app/src/types/index.ts",
    "content": "// Shared TypeScript types for the voicebox application\n\nexport interface VoiceProfile {\n  id: string;\n  name: string;\n  description?: string;\n  language: string;\n  createdAt: string;\n  updatedAt: string;\n}\n\nexport interface Generation {\n  id: string;\n  profileId: string;\n  text: string;\n  language: string;\n  audioPath: string;\n  duration: number;\n  seed?: number;\n  createdAt: string;\n}\n\nexport interface ServerConfig {\n  url: string;\n  isRemote: boolean;\n  isRunning: boolean;\n}\n"
  },
  {
    "path": "app/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    /* Path aliases */\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"types\": [\"vite/client\"]\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "app/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\", \"plugins/**/*.ts\"]\n}\n"
  },
  {
    "path": "app/vite.config.ts",
    "content": "import path from 'node:path';\nimport tailwindcss from '@tailwindcss/vite';\nimport react from '@vitejs/plugin-react';\nimport { defineConfig } from 'vite';\nimport { changelogPlugin } from './plugins/changelog';\n\nexport default defineConfig({\n  plugins: [tailwindcss(), react(), changelogPlugin(path.resolve(__dirname, '..'))],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, './src'),\n    },\n  },\n});\n"
  },
  {
    "path": "backend/README.md",
    "content": "# Voicebox Backend\n\nFastAPI server powering voice cloning, speech generation, and audio processing. Runs locally as a Tauri sidecar or standalone via `python -m backend.main`.\n\n## Running\n\n```bash\n# Via justfile (recommended)\njust dev:server\n\n# Standalone\npython -m backend.main --host 127.0.0.1 --port 17493\n\n# With custom data directory\npython -m backend.main --data-dir /path/to/data\n```\n\nThe server auto-initializes the SQLite database on first startup. Models are downloaded from HuggingFace on first use.\n\n## Architecture\n\n```\nbackend/\n  app.py                  # FastAPI app factory, CORS, lifecycle events\n  main.py                 # Entry point (imports app, runs uvicorn)\n  config.py               # Data directory paths and configuration\n  models.py               # Pydantic request/response schemas\n  server.py               # Tauri sidecar launcher, parent-pid watchdog\n\n  routes/                 # Thin HTTP handlers — validation, delegation, response formatting\n  services/               # Business logic, CRUD, orchestration\n  backends/               # TTS/STT engine implementations (MLX, PyTorch, etc.)\n  database/               # ORM models, session management, migrations, seed data\n  utils/                  # Shared utilities (audio, effects, caching, progress tracking)\n```\n\n### Request flow\n\n```\nHTTP request\n  -> routes/        (validate input, parse params)\n  -> services/      (business logic, database queries, orchestration)\n  -> backends/      (TTS/STT inference)\n  -> utils/         (audio processing, effects, caching)\n```\n\nRoute handlers are intentionally thin. They validate input, delegate to a service function, and format the response. All business logic lives in `services/`.\n\n### Key modules\n\n**services/generation.py** -- Single `run_generation()` function that handles all three generation modes (generate, retry, regenerate). Manages model loading, voice prompt creation, chunked inference, normalization, effects, and version persistence.\n\n**services/task_queue.py** -- Serial generation queue. Ensures only one GPU inference runs at a time. Background tasks are tracked to prevent garbage collection.\n\n**backends/__init__.py** -- Protocol definitions (`TTSBackend`, `STTBackend`), model config registry, and factory functions. Adding a new engine means implementing the protocol and registering a config entry.\n\n**backends/base.py** -- Shared utilities used across all engine implementations: HuggingFace cache checks, device detection, voice prompt combination, progress tracking.\n\n**database/** -- SQLAlchemy ORM models with a re-exporting `__init__.py` for backward compatibility. Migrations run automatically on startup.\n\n### Backend selection\n\nThe server detects the best inference backend at startup:\n\n| Platform | Backend | Acceleration |\n|----------|---------|-------------|\n| macOS (Apple Silicon) | MLX | Metal / Neural Engine |\n| Windows / Linux (NVIDIA) | PyTorch | CUDA |\n| Linux (AMD) | PyTorch | ROCm |\n| Intel Arc | PyTorch | IPEX / XPU |\n| Windows (any GPU) | PyTorch | DirectML |\n| Any | PyTorch | CPU fallback |\n\nDetection is handled by `utils/platform_detect.py`. Both backends implement the same `TTSBackend` protocol, so the API layer is engine-agnostic.\n\n## API\n\n90 endpoints organized by domain. Full interactive documentation available at `http://localhost:17493/docs` when the server is running.\n\n| Domain | Prefix | Description |\n|--------|--------|-------------|\n| Health | `/`, `/health` | Server status, GPU info, filesystem checks |\n| Profiles | `/profiles` | Voice profile CRUD, samples, avatars, import/export |\n| Channels | `/channels` | Audio channel management and voice assignment |\n| Generation | `/generate` | TTS generation, retry, regenerate, status SSE |\n| History | `/history` | Generation history, search, favorites, export |\n| Transcription | `/transcribe` | Whisper-based audio-to-text |\n| Stories | `/stories` | Multi-track timeline editor, audio export |\n| Effects | `/effects` | Effect presets, preview, version management |\n| Audio | `/audio`, `/samples` | Audio file serving |\n| Models | `/models` | Load, unload, download, migrate, status |\n| Tasks | `/tasks`, `/cache` | Active task tracking, cache management |\n| CUDA | `/backend/cuda-*` | CUDA binary download and management |\n\n### Quick examples\n\n```bash\n# Generate speech\ncurl -X POST http://localhost:17493/generate \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"text\": \"Hello world\", \"profile_id\": \"...\", \"language\": \"en\"}'\n\n# List profiles\ncurl http://localhost:17493/profiles\n\n# Stream generation status (SSE)\ncurl http://localhost:17493/generate/{id}/status\n```\n\n## Data directory\n\n```\n{data_dir}/\n  voicebox.db             # SQLite database\n  profiles/{id}/          # Voice samples per profile\n  generations/            # Generated audio files\n  cache/                  # Voice prompt cache (memory + disk)\n  backends/               # Downloaded CUDA binary (if applicable)\n```\n\nDefault location is the OS-specific app data directory. Override with `--data-dir` or the `VOICEBOX_DATA_DIR` environment variable.\n\n## Code quality\n\nLinting and formatting are enforced by [ruff](https://docs.astral.sh/ruff/), configured in `pyproject.toml`. See `STYLE_GUIDE.md` for conventions.\n\n```bash\njust check-python       # lint + format check\njust fix-python         # auto-fix lint issues + reformat\njust test               # run pytest\n```\n\n## Dependencies\n\nRuntime dependencies are in `requirements.txt`. macOS-only MLX dependencies are in `requirements-mlx.txt`. Dev tools (ruff, pytest) are installed automatically by `just setup-python`.\n"
  },
  {
    "path": "backend/STYLE_GUIDE.md",
    "content": "# Python Style Guide\n\nTarget: **Python 3.12+** | Formatter/Linter: **Ruff** | Config: `backend/pyproject.toml`\n\nThis guide codifies the conventions used across the backend, and prescribes the target style for code written during the refactor (Phases 3-6). Existing code should be migrated incrementally -- don't reformat entire files in unrelated PRs.\n\n---\n\n## Formatting\n\nEnforced by `ruff format` (Black-compatible).\n\n- **Line length**: 120 characters.\n- **Indent**: 4 spaces. No tabs.\n- **Trailing commas**: Required on multi-line function signatures, arguments, collections.\n- **Quotes**: Double quotes (`\"`) for strings. Single quotes are acceptable in f-string expressions and dict keys inside f-strings where avoiding escapes improves readability.\n\nRun: `ruff format backend/`\n\n---\n\n## Imports\n\nEnforced by ruff's `isort` rules (rule set `I`).\n\n**Grouping** -- three blocks separated by a blank line:\n\n```python\nimport asyncio                          # 1. stdlib\nfrom pathlib import Path\n\nimport numpy as np                      # 2. third-party\nfrom fastapi import APIRouter, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom backend.config import get_data_dir  # 3. local (absolute)\nfrom .database import get_db            #    or relative\n```\n\n**Rules:**\n- Within the `backend` package, use **relative imports** for sibling/child modules: `from .database import get_db`, `from ..utils.audio import load_audio`.\n- Absolute imports are fine for top-level references from entry points (`main.py`, `server.py`).\n- Never use wildcard imports (`from module import *`).\n- One import per line for `from X import Y` when there are 4+ names; below that, comma-separated is fine.\n- **Lazy imports** are acceptable for heavy dependencies (torch, transformers, mlx) inside functions to reduce startup time. Add a comment: `# lazy: heavy import`.\n\n---\n\n## Type Annotations\n\nPython 3.12 means we use **built-in generics and union syntax natively**. No `from __future__ import annotations`, no `typing.List`/`typing.Dict`.\n\n```python\n# Yes\ndef process(items: list[str], config: dict[str, int] | None = None) -> tuple[int, str]: ...\n\n# No\nfrom typing import List, Dict, Optional, Tuple\ndef process(items: List[str], config: Optional[Dict[str, int]] = None) -> Tuple[int, str]: ...\n```\n\n**What to annotate:**\n- All public function signatures (parameters + return type).\n- Private functions: parameters at minimum; return type encouraged.\n- Module-level variables: only when the type isn't obvious from the assignment.\n- Route handlers: parameters are annotated via FastAPI's dependency injection. Add explicit `-> SomeResponse` return types when the route doesn't use `response_model`.\n\n**Imports from `typing` that are still needed** (no built-in equivalent):\n`Literal`, `TypeAlias`, `Protocol`, `runtime_checkable`, `Callable`, `Any`, `ClassVar`, `TypeVar`, `overload`, `TYPE_CHECKING`.\n\nUse `collections.abc` for abstract types: `Sequence`, `Mapping`, `Iterable`, `Iterator`, `Generator`.\n\n---\n\n## Naming\n\n| Thing | Convention | Example |\n|-------|-----------|---------|\n| Module | `snake_case` | `task_queue.py` |\n| Class | `PascalCase` | `ProgressManager` |\n| Function / method | `snake_case` | `create_profile` |\n| Variable | `snake_case` | `sample_rate` |\n| Constant | `UPPER_SNAKE_CASE` | `DEFAULT_SAMPLE_RATE` |\n| Private | `_leading_underscore` | `_generation_queue` |\n| Type alias | `PascalCase` | `EffectChain = list[dict[str, Any]]` |\n\n**Specific conventions:**\n- Database ORM models imported with `DB` prefix alias: `from .database import VoiceProfile as DBVoiceProfile`.\n- Pydantic models use descriptive suffixes: `VoiceProfileCreate`, `VoiceProfileResponse`, `GenerationRequest`.\n- Backend classes use engine-name prefix: `MLXTTSBackend`, `PyTorchSTTBackend`.\n\n---\n\n## Docstrings\n\n**Google style**. Required on all public functions, classes, and modules.\n\n```python\ndef combine_voice_prompts(\n    profile_dir: Path,\n    *,\n    target_sr: int = 24000,\n) -> tuple[np.ndarray, int]:\n    \"\"\"Load and concatenate all voice prompt files for a profile.\n\n    Reads .wav/.mp3/.flac files from the profile directory, resamples to\n    the target sample rate, normalizes, and concatenates into a single array.\n\n    Args:\n        profile_dir: Path to the voice profile directory containing audio files.\n        target_sr: Target sample rate for the output. Defaults to 24000.\n\n    Returns:\n        Tuple of (concatenated audio array, sample rate).\n\n    Raises:\n        FileNotFoundError: If profile_dir does not exist.\n        ValueError: If no valid audio files are found.\n    \"\"\"\n```\n\n**Short form** is fine for simple functions:\n\n```python\ndef get_db_path() -> Path:\n    \"\"\"Get the path to the SQLite database file.\"\"\"\n```\n\n**When to skip**: Private helpers under ~5 lines where the name and signature make intent obvious.\n\n**Module docstrings**: A single sentence at the top of every file describing its purpose.\n\n```python\n\"\"\"Voice profile CRUD operations.\"\"\"\n```\n\n---\n\n## Comments\n\nComments explain **why**, not **what**. If the code needs a comment to explain what it does, the code should be rewritten to be clearer. The exceptions are non-obvious performance choices, external constraints, and concurrency/race-condition reasoning -- those always deserve a comment.\n\n### No section dividers\n\nDo not use ASCII dividers to create visual sections in files:\n\n```python\n# No -- any of these:\n# ============================================\n# GENERATION ENDPOINTS\n# ============================================\n\n# ---------------------------------------------------------------------------\n# Device detection\n# ---------------------------------------------------------------------------\n\n# --- Load model --------------------------------------------------\n```\n\nIf a file needs section dividers to be navigable, the file is too long. Split it into modules. Within a function, if you need labeled sections to follow the logic, extract those sections into named functions.\n\n### Inline comments\n\nInline comments (end-of-line) are fine when they add information the code can't express:\n\n```python\n# Yes -- explains a non-obvious constraint or gives context:\naudio, sr = load_audio(path, sr=24000)  # Qwen expects 24kHz mono\n_generation_queue: asyncio.Queue = None  # type: ignore  # initialized at startup\n\"tauri://localhost\",         # Tauri webview (macOS)\n\n# No -- restates the code:\n# Check if profile name already exists\nexisting = db.query(DBVoiceProfile).filter_by(name=data.name).first()\n\n# Delete from database\ndb.delete(sample)\n\n# Update fields\nprofile.name = data.name\n```\n\nDelete comments that narrate what the next line of code obviously does. If the function name, variable name, or method call already communicates intent, the comment is noise.\n\n### Block comments\n\nUse block comments for **why** explanations -- constraints, workarounds, non-obvious decisions:\n\n```python\n# PyInstaller + multiprocessing: child processes re-execute the frozen binary\n# with internal arguments. freeze_support() handles this and exits early.\nmultiprocessing.freeze_support()\n\n# Mark any stale \"generating\" records as failed -- these are leftovers\n# from a previous process that was killed mid-generation.\ndb.query(Generation).filter_by(status=\"generating\").update({\"status\": \"failed\"})\n```\n\nKeep block comments tight. Two to three lines is normal. If you need a paragraph, it probably belongs in the docstring or a design doc.\n\n### Linter/type-checker suppression\n\nAlways add a reason after `noqa` and `type: ignore`:\n\n```python\nimport intel_extension_for_pytorch  # noqa: F401 -- side-effect import enables XPU\n_queue: asyncio.Queue = None  # type: ignore[assignment]  # initialized at startup\n```\n\nBare `# noqa` or `# type: ignore` with no explanation are not allowed.\n\n### TODO / FIXME\n\nUse sparingly. Every `TODO` must include a brief description of what needs doing. Don't use them as a substitute for tracking work properly:\n\n```python\n# TODO: replace with async SQLAlchemy once CRUD modules are migrated (Phase 5)\nresult = await asyncio.to_thread(profiles.get_profile, profile_id, db)\n```\n\nNever commit `HACK`, `XXX`, or `FIXME` -- fix the problem or file an issue.\n\n### Commented-out code\n\nDelete it. That's what git is for. If you need to document that something was intentionally removed, a short tombstone comment is acceptable:\n\n```python\n# Removed config.json-only check -- too lenient, doesn't confirm weights exist.\n```\n\n---\n\n## Error Handling\n\nThe refactor is standardizing on a **two-layer pattern**:\n\n### 1. Domain layer -- raise plain exceptions\n\nCRUD modules and services raise `ValueError`, `FileNotFoundError`, or (post-refactor) custom exceptions defined in `backend/errors.py`:\n\n```python\n# backend/errors.py  (to be created in Phase 4)\nclass NotFoundError(Exception):\n    \"\"\"Raised when a requested resource does not exist.\"\"\"\n\nclass ConflictError(Exception):\n    \"\"\"Raised on uniqueness constraint violations.\"\"\"\n```\n\n```python\n# In a service or CRUD module:\nraise NotFoundError(f\"Profile {profile_id} not found\")\n```\n\n### 2. Route layer -- translate to HTTPException\n\nRoute handlers catch domain exceptions and convert:\n\n```python\n@router.post(\"/profiles\")\nasync def create_profile(data: VoiceProfileCreate, db: Session = Depends(get_db)):\n    try:\n        return await profiles.create_profile(data, db)\n    except ConflictError as e:\n        raise HTTPException(status_code=409, detail=str(e))\n```\n\n**Background tasks** catch `Exception` broadly, log with `logger.exception()`, and update the task status to `\"failed\"`.\n\n**Never**: silently swallow exceptions, use bare `except:`, or catch `BaseException`.\n\n---\n\n## Async\n\n### Rules for the refactor\n\n1. **Don't declare `async def` unless the function awaits something.** Several service modules still declare `async def` without awaiting -- these should be migrated to sync functions with `asyncio.to_thread()` at the route layer, or to real async SQLAlchemy.\n2. **CPU-bound work** (audio processing, numpy operations) goes through `asyncio.to_thread()`:\n   ```python\n   audio, sr = await asyncio.to_thread(load_audio, source_path)\n   ```\n3. **GPU-bound TTS inference** is serialized through the generation queue (`services/task_queue.py`). Never call a backend's `generate()` directly from a route handler.\n4. **Fire-and-forget tasks**: use `asyncio.create_task()` and track the task reference to prevent garbage collection:\n   ```python\n   task = asyncio.create_task(some_coro())\n   _background_tasks.add(task)\n   task.add_done_callback(_background_tasks.discard)\n   ```\n\n---\n\n## Logging\n\nUse the `logging` module. Not `print()`.\n\n```python\nimport logging\n\nlogger = logging.getLogger(__name__)\n\nlogger.info(\"Loading model %s on %s\", model_name, device)\nlogger.warning(\"Cache miss for %s, downloading\", repo_id)\nlogger.exception(\"Generation %s failed\")  # logs traceback automatically\n```\n\n**Rules:**\n- Use `%s`-style placeholders in log calls (not f-strings). This avoids formatting the string if the log level is filtered out.\n- Use `logger.exception()` inside `except` blocks -- it captures the traceback.\n- Logger name should be `__name__` (yields `backend.utils.audio`, etc.).\n- Existing `print()` calls should be migrated to logging as files are touched during the refactor.\n\n---\n\n## Constants\n\n- Define at **module level** in the file where they're primarily used.\n- Use `UPPER_SNAKE_CASE`.\n- Shared/cross-cutting constants (sample rates, file size limits, CORS origins) go in `backend/config.py` after Phase 6 consolidation.\n- Magic numbers in function bodies should be extracted to named constants:\n  ```python\n  # No\n  if len(audio) > 24000 * 60 * 10:\n\n  # Yes\n  MAX_AUDIO_DURATION_SAMPLES = SAMPLE_RATE * 60 * 10\n  if len(audio) > MAX_AUDIO_DURATION_SAMPLES:\n  ```\n\n---\n\n## Function Signatures\n\n- **Keyword-only arguments** (after `*`) for functions with 3+ parameters, especially when several share the same type:\n  ```python\n  def is_model_cached(\n      hf_repo: str,\n      *,\n      weight_extensions: tuple[str, ...] = (\".safetensors\", \".bin\"),\n      required_files: list[str] | None = None,\n  ) -> bool:\n  ```\n- Parameters on **separate lines** when the signature exceeds ~100 characters or has 3+ params.\n- **Trailing comma** after the last parameter in multi-line signatures.\n- Default values inline with the parameter.\n\n---\n\n## String Formatting\n\n- **f-strings** for runtime string construction.\n- **`%s`-style** for `logging` calls (lazy evaluation).\n- **`.format()`**: avoid; f-strings are preferred.\n\n---\n\n## Testing\n\nFramework: **pytest** with `pytest-asyncio`.\n\n- Test files: `test_<module>.py` in `backend/tests/`.\n- Use `conftest.py` for shared fixtures (db sessions, test client, mock backends).\n- Group related tests in classes: `class TestProfileCRUD:`.\n- Use `@pytest.mark.asyncio` for async tests.\n- Use `@pytest.mark.parametrize` to reduce repetition.\n- Manual integration scripts stay in `tests/` but are clearly marked (filename prefix `manual_` or documented in `tests/README.md`).\n\n---\n\n## Project Layout\n\n```\nbackend/\n  app.py              # FastAPI app factory, CORS, lifecycle events\n  main.py             # Entry point (imports app, runs uvicorn)\n  config.py           # Data directory paths\n  models.py           # Pydantic request/response schemas\n  server.py           # Tauri sidecar launcher, parent-pid watchdog\n  routes/             # Thin HTTP handlers (validation, delegation, response formatting)\n  services/           # Business logic, CRUD, orchestration\n  backends/           # TTS/STT engine implementations\n  database/           # ORM models, session management, migrations, seeds\n  utils/              # Shared utilities (audio, effects, caching, progress)\n  tests/              # pytest suite\n```\n\n---\n\n## Ruff Adoption\n\n`pyproject.toml` configures ruff for linting and formatting. Run:\n\n```bash\n# Lint (check)\nruff check backend/\n\n# Lint (auto-fix)\nruff check backend/ --fix\n\n# Format\nruff format backend/\n```\n\nIntroduce ruff fixes file-by-file as you touch them. Don't run `--fix` across the entire codebase in one shot -- that creates unreviewable diffs.\n"
  },
  {
    "path": "backend/__init__.py",
    "content": "# Backend package\n\n__version__ = \"0.3.1\"\n"
  },
  {
    "path": "backend/app.py",
    "content": "\"\"\"FastAPI application factory, middleware, and lifecycle events.\"\"\"\n\nimport asyncio\nimport logging\nimport os\nimport sys\nfrom pathlib import Path\n\n\nclass ColoredFormatter(logging.Formatter):\n    \"\"\"Custom formatter to add colors matching uvicorn's style.\"\"\"\n\n    COLORS = {\n        \"DEBUG\": \"\\033[36m\",  # Cyan\n        \"INFO\": \"\\033[32m\",  # Green\n        \"WARNING\": \"\\033[33m\",  # Yellow\n        \"ERROR\": \"\\033[31m\",  # Red\n        \"CRITICAL\": \"\\033[35m\",  # Magenta\n    }\n    RESET = \"\\033[0m\"\n\n    def format(self, record):\n        log_color = self.COLORS.get(record.levelname, self.RESET)\n        record.levelname = f\"{log_color}{record.levelname}{self.RESET}\"\n        return super().format(record)\n\n\n# Configure logging to match uvicorn's format with colors\nhandler = logging.StreamHandler(sys.stderr)\nhandler.setFormatter(ColoredFormatter(\"%(levelname)s:     %(message)s\"))\nlogging.basicConfig(\n    level=logging.INFO,\n    handlers=[handler],\n)\n\nlogger = logging.getLogger(__name__)\n\n# AMD GPU environment variables must be set before torch import\nif not os.environ.get(\"HSA_OVERRIDE_GFX_VERSION\"):\n    os.environ[\"HSA_OVERRIDE_GFX_VERSION\"] = \"10.3.0\"\nif not os.environ.get(\"MIOPEN_LOG_LEVEL\"):\n    os.environ[\"MIOPEN_LOG_LEVEL\"] = \"4\"\n\nimport torch\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom urllib.parse import quote\n\nfrom . import __version__, config, database\nfrom .services import tts, transcribe\nfrom .database import get_db\nfrom .utils.platform_detect import get_backend_type\nfrom .utils.progress import get_progress_manager\nfrom .services.task_queue import create_background_task, init_queue\nfrom .routes import register_routers\n\n\ndef safe_content_disposition(disposition_type: str, filename: str) -> str:\n    \"\"\"Build a Content-Disposition header safe for non-ASCII filenames.\n\n    Uses RFC 5987 ``filename*`` parameter so browsers can decode UTF-8\n    filenames while the ``filename`` fallback stays ASCII-only.\n    \"\"\"\n    ascii_name = \"\".join(c for c in filename if c.isascii() and (c.isalnum() or c in \" -_.\")).strip() or \"download\"\n    utf8_name = quote(filename, safe=\"\")\n    return f\"{disposition_type}; filename=\\\"{ascii_name}\\\"; filename*=UTF-8''{utf8_name}\"\n\n\ndef create_app() -> FastAPI:\n    \"\"\"Create and configure the FastAPI application.\"\"\"\n    application = FastAPI(\n        title=\"voicebox API\",\n        description=\"Production-quality Qwen3-TTS voice cloning API\",\n        version=__version__,\n    )\n\n    _configure_cors(application)\n    register_routers(application)\n    _register_lifecycle(application)\n    _mount_frontend(application)\n\n    return application\n\n\ndef _configure_cors(application: FastAPI) -> None:\n    \"\"\"Set up CORS middleware with local-first defaults.\"\"\"\n    default_origins = [\n        \"http://localhost:5173\",  # Vite dev server\n        \"http://127.0.0.1:5173\",\n        \"http://localhost:17493\",\n        \"http://127.0.0.1:17493\",\n        \"tauri://localhost\",  # Tauri webview (macOS)\n        \"https://tauri.localhost\",  # Tauri webview (Windows/Linux)\n        \"http://tauri.localhost\",  # Tauri webview (Windows, some builds)\n    ]\n    env_origins = os.environ.get(\"VOICEBOX_CORS_ORIGINS\", \"\")\n    all_origins = default_origins + [o.strip() for o in env_origins.split(\",\") if o.strip()]\n\n    application.add_middleware(\n        CORSMiddleware,\n        allow_origins=all_origins,\n        allow_credentials=True,\n        allow_methods=[\"*\"],\n        allow_headers=[\"*\"],\n    )\n\n\ndef _mount_frontend(application: FastAPI) -> None:\n    \"\"\"Serve the built web frontend when present (Docker / web deployment).\n\n    The Dockerfile copies the Vite build output to ``/app/frontend/``.  When\n    that directory exists we mount static assets and add a catch-all route so\n    the React SPA handles client-side routing.  In dev or API-only mode the\n    directory is absent and this function is a no-op.\n    \"\"\"\n    frontend_dir = Path(__file__).resolve().parent.parent / \"frontend\"\n    if not frontend_dir.is_dir():\n        return\n\n    from fastapi.staticfiles import StaticFiles\n    from fastapi.responses import FileResponse\n\n    # Mount hashed assets (JS, CSS, images) that Vite places under /assets\n    assets_dir = frontend_dir / \"assets\"\n    if assets_dir.is_dir():\n        application.mount(\n            \"/assets\",\n            StaticFiles(directory=str(assets_dir)),\n            name=\"frontend-assets\",\n        )\n\n    # SPA catch-all: serve files if they exist, otherwise index.html for\n    # client-side routes like /voices, /stories, /models, etc.\n    @application.get(\"/{full_path:path}\")\n    async def serve_spa(full_path: str):\n        file_path = (frontend_dir / full_path).resolve()\n        # Guard against path traversal — only serve files inside frontend_dir\n        if full_path and file_path.is_file() and str(file_path).startswith(str(frontend_dir)):\n            return FileResponse(file_path)\n        return FileResponse(frontend_dir / \"index.html\", media_type=\"text/html\")\n\n    logger.info(\"Frontend: serving SPA from %s\", frontend_dir)\n\n\ndef _get_gpu_status() -> str:\n    \"\"\"Return a human-readable string describing GPU availability.\"\"\"\n    backend_type = get_backend_type()\n    if torch.cuda.is_available():\n        device_name = torch.cuda.get_device_name(0)\n        is_rocm = hasattr(torch.version, \"hip\") and torch.version.hip is not None\n        if is_rocm:\n            return f\"ROCm ({device_name})\"\n        return f\"CUDA ({device_name})\"\n    elif hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available():\n        return \"MPS (Apple Silicon)\"\n    elif backend_type == \"mlx\":\n        return \"Metal (Apple Silicon via MLX)\"\n    return \"None (CPU only)\"\n\n\ndef _register_lifecycle(application: FastAPI) -> None:\n    \"\"\"Attach startup and shutdown event handlers.\"\"\"\n\n    @application.on_event(\"startup\")\n    async def startup_event():\n        import platform\n        import sys\n\n        logger.info(\"Voicebox v%s starting up\", __version__)\n        logger.info(\n            \"Python %s on %s %s (%s)\",\n            sys.version.split()[0],\n            platform.system(),\n            platform.release(),\n            platform.machine(),\n        )\n\n        database.init_db()\n\n        from .database.session import _db_path\n\n        logger.info(\"Database: %s\", _db_path)\n        logger.info(\"Data directory: %s\", config.get_data_dir())\n\n        init_queue()\n\n        # Mark stale \"generating\" records as failed -- leftovers from a killed process\n        from sqlalchemy import text as sa_text\n\n        db = next(get_db())\n        try:\n            result = db.execute(\n                sa_text(\n                    \"UPDATE generations SET status = 'failed', \"\n                    \"error = 'Server was shut down during generation' \"\n                    \"WHERE status IN ('generating', 'loading_model')\"\n                )\n            )\n            if result.rowcount > 0:\n                logger.info(\"Marked %d stale generation(s) as failed\", result.rowcount)\n\n            from .database import VoiceProfile as DBVoiceProfile, Generation as DBGeneration\n\n            profile_count = db.query(DBVoiceProfile).count()\n            generation_count = db.query(DBGeneration).count()\n            logger.info(\"Profiles: %d, Generations: %d\", profile_count, generation_count)\n\n            db.commit()\n        except Exception as e:\n            db.rollback()\n            logger.warning(\"Could not clean up stale generations: %s\", e)\n        finally:\n            db.close()\n\n        backend_type = get_backend_type()\n        logger.info(\"Backend: %s\", backend_type.upper())\n        logger.info(\"GPU: %s\", _get_gpu_status())\n\n        from .services.cuda import check_and_update_cuda_binary\n\n        create_background_task(check_and_update_cuda_binary())\n\n        try:\n            progress_manager = get_progress_manager()\n            progress_manager._set_main_loop(asyncio.get_running_loop())\n        except Exception as e:\n            logger.warning(\"Could not initialize progress manager event loop: %s\", e)\n\n        try:\n            from huggingface_hub import constants as hf_constants\n\n            cache_dir = Path(hf_constants.HF_HUB_CACHE)\n            cache_dir.mkdir(parents=True, exist_ok=True)\n            logger.info(\"Model cache: %s\", cache_dir)\n        except Exception as e:\n            logger.warning(\"Could not create HuggingFace cache directory: %s\", e)\n\n        logger.info(\"Ready\")\n\n    @application.on_event(\"shutdown\")\n    async def shutdown_event():\n        logger.info(\"Voicebox server shutting down...\")\n        try:\n            tts.unload_tts_model()\n        except Exception:\n            logger.exception(\"Failed to unload TTS model\")\n        try:\n            transcribe.unload_whisper_model()\n        except Exception:\n            logger.exception(\"Failed to unload Whisper model\")\n\n\napp = create_app()\n"
  },
  {
    "path": "backend/backends/__init__.py",
    "content": "\"\"\"\nBackend abstraction layer for TTS and STT.\n\nProvides a unified interface for MLX and PyTorch backends,\nand a model config registry that eliminates per-engine dispatch maps.\n\"\"\"\n\nimport threading\nfrom dataclasses import dataclass, field\nfrom typing import Protocol, Optional, Tuple, List\nfrom typing_extensions import runtime_checkable\nimport numpy as np\n\nfrom ..utils.platform_detect import get_backend_type\n\nLANGUAGE_CODE_TO_NAME = {\n    \"zh\": \"chinese\",\n    \"en\": \"english\",\n    \"ja\": \"japanese\",\n    \"ko\": \"korean\",\n    \"de\": \"german\",\n    \"fr\": \"french\",\n    \"ru\": \"russian\",\n    \"pt\": \"portuguese\",\n    \"es\": \"spanish\",\n    \"it\": \"italian\",\n}\n\nWHISPER_HF_REPOS = {\n    \"base\": \"openai/whisper-base\",\n    \"small\": \"openai/whisper-small\",\n    \"medium\": \"openai/whisper-medium\",\n    \"large\": \"openai/whisper-large-v3\",\n    \"turbo\": \"openai/whisper-large-v3-turbo\",\n}\n\n\n@dataclass\nclass ModelConfig:\n    \"\"\"Declarative config for a downloadable model variant.\"\"\"\n\n    model_name: str  # e.g. \"luxtts\", \"chatterbox-tts\"\n    display_name: str  # e.g. \"LuxTTS (Fast, CPU-friendly)\"\n    engine: str  # e.g. \"luxtts\", \"chatterbox\"\n    hf_repo_id: str  # e.g. \"YatharthS/LuxTTS\"\n    model_size: str = \"default\"\n    size_mb: int = 0\n    needs_trim: bool = False\n    supports_instruct: bool = False\n    languages: list[str] = field(default_factory=lambda: [\"en\"])\n\n\n@runtime_checkable\nclass TTSBackend(Protocol):\n    \"\"\"Protocol for TTS backend implementations.\"\"\"\n\n    # Each backend class should define MODEL_CONFIGS as a class variable:\n    # MODEL_CONFIGS: list[ModelConfig]\n\n    async def load_model(self, model_size: str) -> None:\n        \"\"\"Load TTS model.\"\"\"\n        ...\n\n    async def create_voice_prompt(\n        self,\n        audio_path: str,\n        reference_text: str,\n        use_cache: bool = True,\n    ) -> Tuple[dict, bool]:\n        \"\"\"\n        Create voice prompt from reference audio.\n\n        Returns:\n            Tuple of (voice_prompt_dict, was_cached)\n        \"\"\"\n        ...\n\n    async def combine_voice_prompts(\n        self,\n        audio_paths: List[str],\n        reference_texts: List[str],\n    ) -> Tuple[np.ndarray, str]:\n        \"\"\"\n        Combine multiple voice prompts.\n\n        Returns:\n            Tuple of (combined_audio_array, combined_text)\n        \"\"\"\n        ...\n\n    async def generate(\n        self,\n        text: str,\n        voice_prompt: dict,\n        language: str = \"en\",\n        seed: Optional[int] = None,\n        instruct: Optional[str] = None,\n    ) -> Tuple[np.ndarray, int]:\n        \"\"\"\n        Generate audio from text.\n\n        Returns:\n            Tuple of (audio_array, sample_rate)\n        \"\"\"\n        ...\n\n    def unload_model(self) -> None:\n        \"\"\"Unload model to free memory.\"\"\"\n        ...\n\n    def is_loaded(self) -> bool:\n        \"\"\"Check if model is loaded.\"\"\"\n        ...\n\n    def _get_model_path(self, model_size: str) -> str:\n        \"\"\"\n        Get model path for a given size.\n\n        Returns:\n            Model path or HuggingFace Hub ID\n        \"\"\"\n        ...\n\n\n@runtime_checkable\nclass STTBackend(Protocol):\n    \"\"\"Protocol for STT (Speech-to-Text) backend implementations.\"\"\"\n\n    async def load_model(self, model_size: str) -> None:\n        \"\"\"Load STT model.\"\"\"\n        ...\n\n    async def transcribe(\n        self,\n        audio_path: str,\n        language: Optional[str] = None,\n        model_size: Optional[str] = None,\n    ) -> str:\n        \"\"\"\n        Transcribe audio to text.\n\n        Returns:\n            Transcribed text\n        \"\"\"\n        ...\n\n    def unload_model(self) -> None:\n        \"\"\"Unload model to free memory.\"\"\"\n        ...\n\n    def is_loaded(self) -> bool:\n        \"\"\"Check if model is loaded.\"\"\"\n        ...\n\n\n# Global backend instances\n_tts_backend: Optional[TTSBackend] = None\n_tts_backends: dict[str, TTSBackend] = {}\n_tts_backends_lock = threading.Lock()\n_stt_backend: Optional[STTBackend] = None\n\n# Supported TTS engines — keyed by engine name, value is the backend class import path.\n# The factory function uses this for the if/elif chain; the model configs live on the backend classes.\nTTS_ENGINES = {\n    \"qwen\": \"Qwen TTS\",\n    \"luxtts\": \"LuxTTS\",\n    \"chatterbox\": \"Chatterbox TTS\",\n    \"chatterbox_turbo\": \"Chatterbox Turbo\",\n    \"tada\": \"TADA\",\n    \"kokoro\": \"Kokoro\",\n}\n\n\ndef _get_qwen_model_configs() -> list[ModelConfig]:\n    \"\"\"Return Qwen model configs with backend-aware HF repo IDs.\"\"\"\n    backend_type = get_backend_type()\n    if backend_type == \"mlx\":\n        repo_1_7b = \"mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16\"\n        repo_0_6b = \"mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16\"  # 0.6B not available in MLX, falls back\n    else:\n        repo_1_7b = \"Qwen/Qwen3-TTS-12Hz-1.7B-Base\"\n        repo_0_6b = \"Qwen/Qwen3-TTS-12Hz-0.6B-Base\"\n\n    return [\n        ModelConfig(\n            model_name=\"qwen-tts-1.7B\",\n            display_name=\"Qwen TTS 1.7B\",\n            engine=\"qwen\",\n            hf_repo_id=repo_1_7b,\n            model_size=\"1.7B\",\n            size_mb=3500,\n            supports_instruct=False,  # Base model drops instruct silently\n            languages=[\"zh\", \"en\", \"ja\", \"ko\", \"de\", \"fr\", \"ru\", \"pt\", \"es\", \"it\"],\n        ),\n        ModelConfig(\n            model_name=\"qwen-tts-0.6B\",\n            display_name=\"Qwen TTS 0.6B\",\n            engine=\"qwen\",\n            hf_repo_id=repo_0_6b,\n            model_size=\"0.6B\",\n            size_mb=1200,\n            supports_instruct=False,\n            languages=[\"zh\", \"en\", \"ja\", \"ko\", \"de\", \"fr\", \"ru\", \"pt\", \"es\", \"it\"],\n        ),\n    ]\n\n\ndef _get_non_qwen_tts_configs() -> list[ModelConfig]:\n    \"\"\"Return model configs for non-Qwen TTS engines.\n\n    These are static — no backend-type branching needed.\n    \"\"\"\n    return [\n        ModelConfig(\n            model_name=\"luxtts\",\n            display_name=\"LuxTTS (Fast, CPU-friendly)\",\n            engine=\"luxtts\",\n            hf_repo_id=\"YatharthS/LuxTTS\",\n            size_mb=300,\n            languages=[\"en\"],\n        ),\n        ModelConfig(\n            model_name=\"chatterbox-tts\",\n            display_name=\"Chatterbox TTS (Multilingual)\",\n            engine=\"chatterbox\",\n            hf_repo_id=\"ResembleAI/chatterbox\",\n            size_mb=3200,\n            needs_trim=True,\n            languages=[\n                \"zh\",\n                \"en\",\n                \"ja\",\n                \"ko\",\n                \"de\",\n                \"fr\",\n                \"ru\",\n                \"pt\",\n                \"es\",\n                \"it\",\n                \"he\",\n                \"ar\",\n                \"da\",\n                \"el\",\n                \"fi\",\n                \"hi\",\n                \"ms\",\n                \"nl\",\n                \"no\",\n                \"pl\",\n                \"sv\",\n                \"sw\",\n                \"tr\",\n            ],\n        ),\n        ModelConfig(\n            model_name=\"chatterbox-turbo\",\n            display_name=\"Chatterbox Turbo (English, Tags)\",\n            engine=\"chatterbox_turbo\",\n            hf_repo_id=\"ResembleAI/chatterbox-turbo\",\n            size_mb=1500,\n            needs_trim=True,\n            languages=[\"en\"],\n        ),\n        ModelConfig(\n            model_name=\"tada-1b\",\n            display_name=\"TADA 1B (English)\",\n            engine=\"tada\",\n            hf_repo_id=\"HumeAI/tada-1b\",\n            model_size=\"1B\",\n            size_mb=4000,\n            languages=[\"en\"],\n        ),\n        ModelConfig(\n            model_name=\"tada-3b-ml\",\n            display_name=\"TADA 3B Multilingual\",\n            engine=\"tada\",\n            hf_repo_id=\"HumeAI/tada-3b-ml\",\n            model_size=\"3B\",\n            size_mb=8000,\n            languages=[\"en\", \"ar\", \"zh\", \"de\", \"es\", \"fr\", \"it\", \"ja\", \"pl\", \"pt\"],\n        ),\n        ModelConfig(\n            model_name=\"kokoro\",\n            display_name=\"Kokoro 82M\",\n            engine=\"kokoro\",\n            hf_repo_id=\"hexgrad/Kokoro-82M\",\n            size_mb=350,\n            languages=[\"en\", \"es\", \"fr\", \"hi\", \"it\", \"pt\", \"ja\", \"zh\"],\n        ),\n    ]\n\n\ndef _get_whisper_configs() -> list[ModelConfig]:\n    \"\"\"Return Whisper STT model configs.\"\"\"\n    return [\n        ModelConfig(\n            model_name=\"whisper-base\",\n            display_name=\"Whisper Base\",\n            engine=\"whisper\",\n            hf_repo_id=\"openai/whisper-base\",\n            model_size=\"base\",\n        ),\n        ModelConfig(\n            model_name=\"whisper-small\",\n            display_name=\"Whisper Small\",\n            engine=\"whisper\",\n            hf_repo_id=\"openai/whisper-small\",\n            model_size=\"small\",\n        ),\n        ModelConfig(\n            model_name=\"whisper-medium\",\n            display_name=\"Whisper Medium\",\n            engine=\"whisper\",\n            hf_repo_id=\"openai/whisper-medium\",\n            model_size=\"medium\",\n        ),\n        ModelConfig(\n            model_name=\"whisper-large\",\n            display_name=\"Whisper Large\",\n            engine=\"whisper\",\n            hf_repo_id=\"openai/whisper-large-v3\",\n            model_size=\"large\",\n        ),\n        ModelConfig(\n            model_name=\"whisper-turbo\",\n            display_name=\"Whisper Turbo\",\n            engine=\"whisper\",\n            hf_repo_id=\"openai/whisper-large-v3-turbo\",\n            model_size=\"turbo\",\n        ),\n    ]\n\n\ndef get_all_model_configs() -> list[ModelConfig]:\n    \"\"\"Return the full list of model configs (TTS + STT).\"\"\"\n    return _get_qwen_model_configs() + _get_non_qwen_tts_configs() + _get_whisper_configs()\n\n\ndef get_tts_model_configs() -> list[ModelConfig]:\n    \"\"\"Return only TTS model configs.\"\"\"\n    return _get_qwen_model_configs() + _get_non_qwen_tts_configs()\n\n\n# Lookup helpers — these replace the if/elif chains in main.py\n\n\ndef get_model_config(model_name: str) -> Optional[ModelConfig]:\n    \"\"\"Look up a model config by model_name.\"\"\"\n    for cfg in get_all_model_configs():\n        if cfg.model_name == model_name:\n            return cfg\n    return None\n\n\ndef engine_needs_trim(engine: str) -> bool:\n    \"\"\"Whether this engine's output should be run through trim_tts_output.\"\"\"\n    for cfg in get_tts_model_configs():\n        if cfg.engine == engine:\n            return cfg.needs_trim\n    return False\n\n\ndef engine_has_model_sizes(engine: str) -> bool:\n    \"\"\"Whether this engine supports multiple model sizes (only Qwen currently).\"\"\"\n    configs = [c for c in get_tts_model_configs() if c.engine == engine]\n    return len(configs) > 1\n\n\nasync def load_engine_model(engine: str, model_size: str = \"default\") -> None:\n    \"\"\"Load a model for the given engine, handling engines with multiple model sizes.\"\"\"\n    backend = get_tts_backend_for_engine(engine)\n    if engine == \"qwen\":\n        await backend.load_model_async(model_size)\n    elif engine == \"tada\":\n        await backend.load_model(model_size)\n    else:\n        await backend.load_model()\n\n\nasync def ensure_model_cached_or_raise(engine: str, model_size: str = \"default\") -> None:\n    \"\"\"Check if a model is cached, raise HTTPException if not. Used by streaming endpoint.\"\"\"\n    from fastapi import HTTPException\n\n    backend = get_tts_backend_for_engine(engine)\n    cfg = None\n    for c in get_tts_model_configs():\n        if c.engine == engine and c.model_size == model_size:\n            cfg = c\n            break\n\n    if engine in (\"qwen\", \"tada\"):\n        if not backend._is_model_cached(model_size):\n            raise HTTPException(\n                status_code=400,\n                detail=f\"Model {model_size} is not downloaded yet. Use /generate to trigger a download.\",\n            )\n    else:\n        if not backend._is_model_cached():\n            display = cfg.display_name if cfg else engine\n            raise HTTPException(\n                status_code=400,\n                detail=f\"{display} model is not downloaded yet. Use /generate to trigger a download.\",\n            )\n\n\ndef unload_model_by_config(config: ModelConfig) -> bool:\n    \"\"\"Unload a model given its config. Returns True if it was loaded, False otherwise.\"\"\"\n    from . import get_tts_backend_for_engine\n    from ..services import tts, transcribe\n\n    if config.engine == \"whisper\":\n        whisper_model = transcribe.get_whisper_model()\n        if whisper_model.is_loaded() and whisper_model.model_size == config.model_size:\n            transcribe.unload_whisper_model()\n            return True\n        return False\n\n    if config.engine == \"qwen\":\n        tts_model = tts.get_tts_model()\n        loaded_size = getattr(tts_model, \"_current_model_size\", None) or getattr(tts_model, \"model_size\", None)\n        if tts_model.is_loaded() and loaded_size == config.model_size:\n            tts.unload_tts_model()\n            return True\n        return False\n\n    # All other TTS engines\n    backend = get_tts_backend_for_engine(config.engine)\n    if backend.is_loaded():\n        backend.unload_model()\n        return True\n    return False\n\n\ndef check_model_loaded(config: ModelConfig) -> bool:\n    \"\"\"Check if a model is currently loaded.\"\"\"\n    from . import get_tts_backend_for_engine\n    from ..services import tts, transcribe\n\n    try:\n        if config.engine == \"whisper\":\n            whisper_model = transcribe.get_whisper_model()\n            return whisper_model.is_loaded() and getattr(whisper_model, \"model_size\", None) == config.model_size\n\n        if config.engine == \"qwen\":\n            tts_model = tts.get_tts_model()\n            loaded_size = getattr(tts_model, \"_current_model_size\", None) or getattr(tts_model, \"model_size\", None)\n            return tts_model.is_loaded() and loaded_size == config.model_size\n\n        backend = get_tts_backend_for_engine(config.engine)\n        return backend.is_loaded()\n    except Exception:\n        return False\n\n\ndef get_model_load_func(config: ModelConfig):\n    \"\"\"Return a callable that loads/downloads the model.\"\"\"\n    from . import get_tts_backend_for_engine\n    from ..services import tts, transcribe\n\n    if config.engine == \"whisper\":\n        return lambda: transcribe.get_whisper_model().load_model(config.model_size)\n\n    if config.engine == \"qwen\":\n        return lambda: tts.get_tts_model().load_model(config.model_size)\n\n    return lambda: get_tts_backend_for_engine(config.engine).load_model()\n\n\ndef get_tts_backend() -> TTSBackend:\n    \"\"\"\n    Get or create the default (Qwen) TTS backend instance based on platform.\n\n    Returns:\n        TTS backend instance (MLX or PyTorch)\n    \"\"\"\n    return get_tts_backend_for_engine(\"qwen\")\n\n\ndef get_tts_backend_for_engine(engine: str) -> TTSBackend:\n    \"\"\"\n    Get or create a TTS backend for the given engine.\n\n    Args:\n        engine: Engine name (e.g. \"qwen\", \"luxtts\", \"chatterbox\", \"chatterbox_turbo\")\n\n    Returns:\n        TTS backend instance\n    \"\"\"\n    global _tts_backends\n\n    # Fast path: check without lock\n    if engine in _tts_backends:\n        return _tts_backends[engine]\n\n    # Slow path: create with lock to avoid duplicate instantiation\n    with _tts_backends_lock:\n        # Double-check after acquiring lock\n        if engine in _tts_backends:\n            return _tts_backends[engine]\n\n        if engine == \"qwen\":\n            backend_type = get_backend_type()\n            if backend_type == \"mlx\":\n                from .mlx_backend import MLXTTSBackend\n\n                backend = MLXTTSBackend()\n            else:\n                from .pytorch_backend import PyTorchTTSBackend\n\n                backend = PyTorchTTSBackend()\n        elif engine == \"luxtts\":\n            from .luxtts_backend import LuxTTSBackend\n\n            backend = LuxTTSBackend()\n        elif engine == \"chatterbox\":\n            from .chatterbox_backend import ChatterboxTTSBackend\n\n            backend = ChatterboxTTSBackend()\n        elif engine == \"chatterbox_turbo\":\n            from .chatterbox_turbo_backend import ChatterboxTurboTTSBackend\n\n            backend = ChatterboxTurboTTSBackend()\n        elif engine == \"tada\":\n            from .hume_backend import HumeTadaBackend\n\n            backend = HumeTadaBackend()\n        elif engine == \"kokoro\":\n            from .kokoro_backend import KokoroTTSBackend\n\n            backend = KokoroTTSBackend()\n        else:\n            raise ValueError(f\"Unknown TTS engine: {engine}. Supported: {list(TTS_ENGINES.keys())}\")\n\n        _tts_backends[engine] = backend\n        return backend\n\n\ndef get_stt_backend() -> STTBackend:\n    \"\"\"\n    Get or create STT backend instance based on platform.\n\n    Returns:\n        STT backend instance (MLX or PyTorch)\n    \"\"\"\n    global _stt_backend\n\n    if _stt_backend is None:\n        backend_type = get_backend_type()\n\n        if backend_type == \"mlx\":\n            from .mlx_backend import MLXSTTBackend\n\n            _stt_backend = MLXSTTBackend()\n        else:\n            from .pytorch_backend import PyTorchSTTBackend\n\n            _stt_backend = PyTorchSTTBackend()\n\n    return _stt_backend\n\n\ndef reset_backends():\n    \"\"\"Reset backend instances (useful for testing).\"\"\"\n    global _tts_backend, _tts_backends, _stt_backend\n    _tts_backend = None\n    _tts_backends.clear()\n    _stt_backend = None\n"
  },
  {
    "path": "backend/backends/base.py",
    "content": "\"\"\"\nShared utilities for TTS/STT backend implementations.\n\nEliminates duplication of cache checking, device detection,\nvoice prompt combination, and model loading progress tracking.\n\"\"\"\n\nimport logging\nimport platform\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import Callable, List, Optional, Tuple\n\nimport numpy as np\n\nfrom ..utils.audio import normalize_audio, load_audio\nfrom ..utils.progress import get_progress_manager\nfrom ..utils.hf_progress import HFProgressTracker, create_hf_progress_callback\nfrom ..utils.tasks import get_task_manager\n\nlogger = logging.getLogger(__name__)\n\n\ndef is_model_cached(\n    hf_repo: str,\n    *,\n    weight_extensions: tuple[str, ...] = (\".safetensors\", \".bin\"),\n    required_files: Optional[list[str]] = None,\n) -> bool:\n    \"\"\"\n    Check if a HuggingFace model is fully cached locally.\n\n    Args:\n        hf_repo: HuggingFace repo ID (e.g. \"Qwen/Qwen3-TTS-12Hz-1.7B-Base\")\n        weight_extensions: File extensions that count as model weights.\n        required_files: If set, check that these specific filenames exist\n                        in snapshots instead of checking by extension.\n\n    Returns:\n        True if model is fully cached, False if missing or incomplete.\n    \"\"\"\n    try:\n        from huggingface_hub import constants as hf_constants\n\n        repo_cache = Path(hf_constants.HF_HUB_CACHE) / (\"models--\" + hf_repo.replace(\"/\", \"--\"))\n\n        if not repo_cache.exists():\n            return False\n\n        # Incomplete blobs mean a download is still in progress\n        blobs_dir = repo_cache / \"blobs\"\n        if blobs_dir.exists() and any(blobs_dir.glob(\"*.incomplete\")):\n            logger.debug(f\"Found .incomplete files for {hf_repo}\")\n            return False\n\n        snapshots_dir = repo_cache / \"snapshots\"\n        if not snapshots_dir.exists():\n            return False\n\n        if required_files:\n            # Check that every required filename exists somewhere in snapshots\n            for fname in required_files:\n                if not any(snapshots_dir.rglob(fname)):\n                    return False\n            return True\n\n        # Check that at least one weight file exists\n        for ext in weight_extensions:\n            if any(snapshots_dir.rglob(f\"*{ext}\")):\n                return True\n\n        logger.debug(f\"No model weights found for {hf_repo}\")\n        return False\n\n    except Exception as e:\n        logger.warning(f\"Error checking cache for {hf_repo}: {e}\")\n        return False\n\n\ndef get_torch_device(\n    *,\n    allow_xpu: bool = False,\n    allow_directml: bool = False,\n    allow_mps: bool = False,\n    force_cpu_on_mac: bool = False,\n) -> str:\n    \"\"\"\n    Detect the best available torch device.\n\n    Args:\n        allow_xpu: Check for Intel XPU (IPEX) support.\n        allow_directml: Check for DirectML (Windows) support.\n        allow_mps: Allow MPS (Apple Silicon). If False, MPS falls back to CPU.\n        force_cpu_on_mac: Force CPU on macOS regardless of GPU availability.\n    \"\"\"\n    if force_cpu_on_mac and platform.system() == \"Darwin\":\n        return \"cpu\"\n\n    import torch\n\n    if torch.cuda.is_available():\n        return \"cuda\"\n\n    if allow_xpu:\n        try:\n            import intel_extension_for_pytorch  # noqa: F401\n\n            if hasattr(torch, \"xpu\") and torch.xpu.is_available():\n                return \"xpu\"\n        except ImportError:\n            pass\n\n    if allow_directml:\n        try:\n            import torch_directml\n\n            if torch_directml.device_count() > 0:\n                return torch_directml.device(0)\n        except ImportError:\n            pass\n\n    if allow_mps:\n        if hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available():\n            return \"mps\"\n\n    return \"cpu\"\n\n\nasync def combine_voice_prompts(\n    audio_paths: List[str],\n    reference_texts: List[str],\n    *,\n    sample_rate: Optional[int] = None,\n) -> Tuple[np.ndarray, str]:\n    \"\"\"\n    Combine multiple reference audio samples into one.\n\n    Loads each audio file, normalizes, concatenates, and joins texts.\n\n    Args:\n        audio_paths: Paths to reference audio files.\n        reference_texts: Corresponding transcripts.\n        sample_rate: If set, resample audio to this rate during loading.\n    \"\"\"\n    combined_audio = []\n\n    for path in audio_paths:\n        kwargs = {\"sample_rate\": sample_rate} if sample_rate else {}\n        audio, _sr = load_audio(path, **kwargs)\n        audio = normalize_audio(audio)\n        combined_audio.append(audio)\n\n    mixed = np.concatenate(combined_audio)\n    mixed = normalize_audio(mixed)\n    combined_text = \" \".join(reference_texts)\n\n    return mixed, combined_text\n\n\n@contextmanager\ndef model_load_progress(\n    model_name: str,\n    is_cached: bool,\n    filter_non_downloads: Optional[bool] = None,\n):\n    \"\"\"\n    Context manager for model loading with HF download progress tracking.\n\n    Handles the tqdm patching, progress_manager/task_manager lifecycle,\n    and error reporting that every backend duplicates.\n\n    Args:\n        model_name: Progress tracking key (e.g. \"qwen-tts-1.7B\", \"whisper-base\").\n        is_cached: Whether the model is already downloaded.\n        filter_non_downloads: Whether to filter non-download tqdm bars.\n                              Defaults to `is_cached`.\n\n    Yields:\n        The tracker context (already entered). The caller loads the model\n        inside the `with` block. The tqdm patch is torn down on exit.\n\n    Usage:\n        with model_load_progress(\"qwen-tts-1.7B\", is_cached) as ctx:\n            self.model = SomeModel.from_pretrained(...)\n    \"\"\"\n    if filter_non_downloads is None:\n        filter_non_downloads = is_cached\n\n    progress_manager = get_progress_manager()\n    task_manager = get_task_manager()\n\n    progress_callback = create_hf_progress_callback(model_name, progress_manager)\n    tracker = HFProgressTracker(progress_callback, filter_non_downloads=filter_non_downloads)\n\n    tracker_context = tracker.patch_download()\n    tracker_context.__enter__()\n\n    if not is_cached:\n        task_manager.start_download(model_name)\n        progress_manager.update_progress(\n            model_name=model_name,\n            current=0,\n            total=0,\n            filename=\"Connecting to HuggingFace...\",\n            status=\"downloading\",\n        )\n\n    try:\n        yield tracker_context\n    except Exception as e:\n        # Report error to both managers\n        progress_manager.mark_error(model_name, str(e))\n        task_manager.error_download(model_name, str(e))\n        raise\n    else:\n        # Only mark complete if we were tracking a download\n        if not is_cached:\n            progress_manager.mark_complete(model_name)\n            task_manager.complete_download(model_name)\n    finally:\n        tracker_context.__exit__(None, None, None)\n\n\ndef patch_chatterbox_f32(model) -> None:\n    \"\"\"\n    Patch float64 -> float32 dtype mismatches in upstream chatterbox.\n\n    librosa.load returns float64 numpy arrays. Multiple upstream code paths\n    convert these to torch tensors via torch.from_numpy() without casting,\n    then matmul against float32 model weights. This patches the two known\n    entry points:\n\n    1. S3Tokenizer.log_mel_spectrogram — audio tensor hits _mel_filters (f32)\n    2. VoiceEncoder.forward — float64 mel spectrograms hit LSTM weights (f32)\n    \"\"\"\n    import types\n\n    # Patch S3Tokenizer\n    _tokzr = model.s3gen.tokenizer\n    _orig_log_mel = _tokzr.log_mel_spectrogram.__func__\n\n    def _f32_log_mel(self_tokzr, audio, padding=0):\n        import torch as _torch\n\n        if _torch.is_tensor(audio):\n            audio = audio.float()\n        return _orig_log_mel(self_tokzr, audio, padding)\n\n    _tokzr.log_mel_spectrogram = types.MethodType(_f32_log_mel, _tokzr)\n\n    # Patch VoiceEncoder\n    _ve = model.ve\n    _orig_ve_forward = _ve.forward.__func__\n\n    def _f32_ve_forward(self_ve, mels):\n        return _orig_ve_forward(self_ve, mels.float())\n\n    _ve.forward = types.MethodType(_f32_ve_forward, _ve)\n"
  },
  {
    "path": "backend/backends/chatterbox_backend.py",
    "content": "\"\"\"\nChatterbox TTS backend implementation.\n\nWraps ChatterboxMultilingualTTS from chatterbox-tts for zero-shot\nvoice cloning. Supports 23 languages including Hebrew. Forces CPU\non macOS due to known MPS tensor issues.\n\"\"\"\n\nimport asyncio\nimport logging\nimport threading\nfrom pathlib import Path\nfrom typing import ClassVar, List, Optional, Tuple\n\nimport numpy as np\n\nfrom . import TTSBackend\nfrom .base import (\n    is_model_cached,\n    get_torch_device,\n    combine_voice_prompts as _combine_voice_prompts,\n    model_load_progress,\n    patch_chatterbox_f32,\n)\n\nlogger = logging.getLogger(__name__)\n\nCHATTERBOX_HF_REPO = \"ResembleAI/chatterbox\"\n\n# Files that must be present for the multilingual model\n_MTL_WEIGHT_FILES = [\n    \"t3_mtl23ls_v2.safetensors\",\n    \"s3gen.pt\",\n    \"ve.pt\",\n]\n\n\nclass ChatterboxTTSBackend:\n    \"\"\"Chatterbox Multilingual TTS backend for voice cloning.\"\"\"\n\n    # Class-level lock for torch.load monkey-patching\n    _load_lock: ClassVar[threading.Lock] = threading.Lock()\n\n    def __init__(self):\n        self.model = None\n        self.model_size = \"default\"\n        self._device = None\n        self._model_load_lock = asyncio.Lock()\n\n    def _get_device(self) -> str:\n        return get_torch_device(force_cpu_on_mac=True)\n\n    def is_loaded(self) -> bool:\n        return self.model is not None\n\n    def _get_model_path(self, model_size: str = \"default\") -> str:\n        return CHATTERBOX_HF_REPO\n\n    def _is_model_cached(self, model_size: str = \"default\") -> bool:\n        return is_model_cached(CHATTERBOX_HF_REPO, required_files=_MTL_WEIGHT_FILES)\n\n    async def load_model(self, model_size: str = \"default\") -> None:\n        \"\"\"Load the Chatterbox multilingual model.\"\"\"\n        if self.model is not None:\n            return\n        async with self._model_load_lock:\n            if self.model is not None:\n                return\n            await asyncio.to_thread(self._load_model_sync)\n\n    def _load_model_sync(self):\n        \"\"\"Synchronous model loading.\"\"\"\n        model_name = \"chatterbox-tts\"\n        is_cached = self._is_model_cached()\n\n        with model_load_progress(model_name, is_cached):\n            device = self._get_device()\n            self._device = device\n            logger.info(f\"Loading Chatterbox Multilingual TTS on {device}...\")\n\n            import torch\n            from chatterbox.mtl_tts import ChatterboxMultilingualTTS\n\n            if device == \"cpu\":\n                _orig_torch_load = torch.load\n\n                def _patched_load(*args, **kwargs):\n                    kwargs.setdefault(\"map_location\", \"cpu\")\n                    return _orig_torch_load(*args, **kwargs)\n\n                with ChatterboxTTSBackend._load_lock:\n                    torch.load = _patched_load\n                    try:\n                        model = ChatterboxMultilingualTTS.from_pretrained(device=device)\n                    finally:\n                        torch.load = _orig_torch_load\n            else:\n                model = ChatterboxMultilingualTTS.from_pretrained(device=device)\n\n            # Fix sdpa attention for output_attentions support\n            t3_tfmr = model.t3.tfmr\n            if hasattr(t3_tfmr, \"config\") and hasattr(t3_tfmr.config, \"_attn_implementation\"):\n                t3_tfmr.config._attn_implementation = \"eager\"\n                for layer in getattr(t3_tfmr, \"layers\", []):\n                    if hasattr(layer, \"self_attn\"):\n                        layer.self_attn._attn_implementation = \"eager\"\n\n            patch_chatterbox_f32(model)\n            self.model = model\n\n        logger.info(\"Chatterbox Multilingual TTS loaded successfully\")\n\n    def unload_model(self) -> None:\n        \"\"\"Unload model to free memory.\"\"\"\n        if self.model is not None:\n            device = self._device\n            del self.model\n            self.model = None\n            self._device = None\n            if device == \"cuda\":\n                import torch\n\n                torch.cuda.empty_cache()\n            logger.info(\"Chatterbox unloaded\")\n\n    async def create_voice_prompt(\n        self,\n        audio_path: str,\n        reference_text: str,\n        use_cache: bool = True,\n    ) -> Tuple[dict, bool]:\n        \"\"\"\n        Create voice prompt from reference audio.\n\n        Chatterbox processes reference audio at generation time, so the\n        prompt just stores the file path. The actual audio is loaded by\n        model.generate() via audio_prompt_path.\n        \"\"\"\n        voice_prompt = {\n            \"ref_audio\": str(audio_path),\n            \"ref_text\": reference_text,\n        }\n        return voice_prompt, False\n\n    async def combine_voice_prompts(\n        self,\n        audio_paths: List[str],\n        reference_texts: List[str],\n    ) -> Tuple[np.ndarray, str]:\n        return await _combine_voice_prompts(audio_paths, reference_texts)\n\n    # Per-language generation defaults. Lower temp + higher cfg = clearer speech.\n    _LANG_DEFAULTS: ClassVar[dict] = {\n        \"he\": {\n            \"exaggeration\": 0.4,\n            \"cfg_weight\": 0.7,\n            \"temperature\": 0.65,\n            \"repetition_penalty\": 2.5,\n        },\n    }\n    _GLOBAL_DEFAULTS: ClassVar[dict] = {\n        \"exaggeration\": 0.5,\n        \"cfg_weight\": 0.5,\n        \"temperature\": 0.8,\n        \"repetition_penalty\": 2.0,\n    }\n\n    async def generate(\n        self,\n        text: str,\n        voice_prompt: dict,\n        language: str = \"en\",\n        seed: Optional[int] = None,\n        instruct: Optional[str] = None,\n    ) -> Tuple[np.ndarray, int]:\n        \"\"\"\n        Generate audio using Chatterbox Multilingual TTS.\n\n        Args:\n            text: Text to synthesize\n            voice_prompt: Dict with ref_audio path\n            language: BCP-47 language code\n            seed: Random seed for reproducibility\n            instruct: Unused (protocol compatibility)\n\n        Returns:\n            Tuple of (audio_array, sample_rate)\n        \"\"\"\n        await self.load_model()\n\n        ref_audio = voice_prompt.get(\"ref_audio\")\n        if ref_audio and not Path(ref_audio).exists():\n            logger.warning(f\"Reference audio not found: {ref_audio}\")\n            ref_audio = None\n\n        # Merge language-specific defaults with global defaults\n        lang_defaults = self._LANG_DEFAULTS.get(language, self._GLOBAL_DEFAULTS)\n\n        def _generate_sync():\n            import torch\n\n            if seed is not None:\n                torch.manual_seed(seed)\n\n            logger.info(f\"[Chatterbox] Generating: lang={language}\")\n\n            wav = self.model.generate(\n                text,\n                language_id=language,\n                audio_prompt_path=ref_audio,\n                exaggeration=lang_defaults[\"exaggeration\"],\n                cfg_weight=lang_defaults[\"cfg_weight\"],\n                temperature=lang_defaults[\"temperature\"],\n                repetition_penalty=lang_defaults[\"repetition_penalty\"],\n            )\n\n            # Convert tensor -> numpy\n            if isinstance(wav, torch.Tensor):\n                audio = wav.squeeze().cpu().numpy().astype(np.float32)\n            else:\n                audio = np.asarray(wav, dtype=np.float32)\n\n            sample_rate = (\n                getattr(self.model, \"sr\", None)\n                or getattr(self.model, \"sample_rate\", 24000)\n            )\n\n            return audio, sample_rate\n\n        return await asyncio.to_thread(_generate_sync)\n"
  },
  {
    "path": "backend/backends/chatterbox_turbo_backend.py",
    "content": "\"\"\"\nChatterbox Turbo TTS backend implementation.\n\nWraps ChatterboxTurboTTS from chatterbox-tts for fast, English-only\nvoice cloning with paralinguistic tag support ([laugh], [cough], etc.).\nForces CPU on macOS due to known MPS tensor issues.\n\"\"\"\n\nimport asyncio\nimport logging\nimport threading\nfrom pathlib import Path\nfrom typing import ClassVar, List, Optional, Tuple\n\nimport numpy as np\n\nfrom . import TTSBackend\nfrom .base import (\n    is_model_cached,\n    get_torch_device,\n    combine_voice_prompts as _combine_voice_prompts,\n    model_load_progress,\n    patch_chatterbox_f32,\n)\n\nlogger = logging.getLogger(__name__)\n\nCHATTERBOX_TURBO_HF_REPO = \"ResembleAI/chatterbox-turbo\"\n\n# Files that must be present for the turbo model\n_TURBO_WEIGHT_FILES = [\n    \"t3_turbo_v1.safetensors\",\n    \"s3gen_meanflow.safetensors\",\n    \"ve.safetensors\",\n]\n\n\nclass ChatterboxTurboTTSBackend:\n    \"\"\"Chatterbox Turbo TTS backend — fast, English-only, with paralinguistic tags.\"\"\"\n\n    # Class-level lock for torch.load monkey-patching\n    _load_lock: ClassVar[threading.Lock] = threading.Lock()\n\n    def __init__(self):\n        self.model = None\n        self.model_size = \"default\"\n        self._device = None\n        self._model_load_lock = asyncio.Lock()\n\n    def _get_device(self) -> str:\n        return get_torch_device(force_cpu_on_mac=True)\n\n    def is_loaded(self) -> bool:\n        return self.model is not None\n\n    def _get_model_path(self, model_size: str = \"default\") -> str:\n        return CHATTERBOX_TURBO_HF_REPO\n\n    def _is_model_cached(self, model_size: str = \"default\") -> bool:\n        return is_model_cached(CHATTERBOX_TURBO_HF_REPO, required_files=_TURBO_WEIGHT_FILES)\n\n    async def load_model(self, model_size: str = \"default\") -> None:\n        \"\"\"Load the Chatterbox Turbo model.\"\"\"\n        if self.model is not None:\n            return\n        async with self._model_load_lock:\n            if self.model is not None:\n                return\n            await asyncio.to_thread(self._load_model_sync)\n\n    def _load_model_sync(self):\n        \"\"\"Synchronous model loading.\"\"\"\n        model_name = \"chatterbox-turbo\"\n        is_cached = self._is_model_cached()\n\n        with model_load_progress(model_name, is_cached):\n            device = self._get_device()\n            self._device = device\n            logger.info(f\"Loading Chatterbox Turbo TTS on {device}...\")\n\n            import torch\n            from huggingface_hub import snapshot_download\n            from chatterbox.tts_turbo import ChatterboxTurboTTS\n\n            local_path = snapshot_download(\n                repo_id=CHATTERBOX_TURBO_HF_REPO,\n                token=None,\n                allow_patterns=[\"*.safetensors\", \"*.json\", \"*.txt\", \"*.pt\", \"*.model\"],\n            )\n\n            if device == \"cpu\":\n                _orig_torch_load = torch.load\n\n                def _patched_load(*args, **kwargs):\n                    kwargs.setdefault(\"map_location\", \"cpu\")\n                    return _orig_torch_load(*args, **kwargs)\n\n                with ChatterboxTurboTTSBackend._load_lock:\n                    torch.load = _patched_load\n                    try:\n                        model = ChatterboxTurboTTS.from_local(local_path, device)\n                    finally:\n                        torch.load = _orig_torch_load\n            else:\n                model = ChatterboxTurboTTS.from_local(local_path, device)\n\n            patch_chatterbox_f32(model)\n            self.model = model\n\n        logger.info(\"Chatterbox Turbo TTS loaded successfully\")\n\n    def unload_model(self) -> None:\n        \"\"\"Unload model to free memory.\"\"\"\n        if self.model is not None:\n            device = self._device\n            del self.model\n            self.model = None\n            self._device = None\n            if device == \"cuda\":\n                import torch\n\n                torch.cuda.empty_cache()\n            logger.info(\"Chatterbox Turbo unloaded\")\n\n    async def create_voice_prompt(\n        self,\n        audio_path: str,\n        reference_text: str,\n        use_cache: bool = True,\n    ) -> Tuple[dict, bool]:\n        \"\"\"\n        Create voice prompt from reference audio.\n\n        Chatterbox Turbo processes reference audio at generation time, so the\n        prompt just stores the file path.\n        \"\"\"\n        voice_prompt = {\n            \"ref_audio\": str(audio_path),\n            \"ref_text\": reference_text,\n        }\n        return voice_prompt, False\n\n    async def combine_voice_prompts(\n        self,\n        audio_paths: List[str],\n        reference_texts: List[str],\n    ) -> Tuple[np.ndarray, str]:\n        return await _combine_voice_prompts(audio_paths, reference_texts)\n\n    async def generate(\n        self,\n        text: str,\n        voice_prompt: dict,\n        language: str = \"en\",\n        seed: Optional[int] = None,\n        instruct: Optional[str] = None,\n    ) -> Tuple[np.ndarray, int]:\n        \"\"\"\n        Generate audio using Chatterbox Turbo TTS.\n\n        Supports paralinguistic tags in text: [laugh], [cough], [chuckle], etc.\n\n        Args:\n            text: Text to synthesize (may include paralinguistic tags)\n            voice_prompt: Dict with ref_audio path\n            language: Ignored (Turbo is English-only)\n            seed: Random seed for reproducibility\n            instruct: Unused (protocol compatibility)\n\n        Returns:\n            Tuple of (audio_array, sample_rate)\n        \"\"\"\n        await self.load_model()\n\n        ref_audio = voice_prompt.get(\"ref_audio\")\n        if ref_audio and not Path(ref_audio).exists():\n            logger.warning(f\"Reference audio not found: {ref_audio}\")\n            ref_audio = None\n\n        def _generate_sync():\n            import torch\n\n            if seed is not None:\n                torch.manual_seed(seed)\n\n            logger.info(\"[Chatterbox Turbo] Generating (English)\")\n\n            wav = self.model.generate(\n                text,\n                audio_prompt_path=ref_audio,\n                temperature=0.8,\n                top_k=1000,\n                top_p=0.95,\n                repetition_penalty=1.2,\n            )\n\n            # Convert tensor -> numpy\n            if isinstance(wav, torch.Tensor):\n                audio = wav.squeeze().cpu().numpy().astype(np.float32)\n            else:\n                audio = np.asarray(wav, dtype=np.float32)\n\n            sample_rate = (\n                getattr(self.model, \"sr\", None)\n                or getattr(self.model, \"sample_rate\", 24000)\n            )\n\n            return audio, sample_rate\n\n        return await asyncio.to_thread(_generate_sync)\n"
  },
  {
    "path": "backend/backends/hume_backend.py",
    "content": "\"\"\"\nHumeAI TADA TTS backend implementation.\n\nWraps HumeAI's TADA (Text-Acoustic Dual Alignment) model for\nhigh-quality voice cloning. Two model variants:\n  - tada-1b: English-only, ~2B params (Llama 3.2 1B base)\n  - tada-3b-ml: Multilingual, ~4B params (Llama 3.2 3B base)\n\nBoth use a shared encoder/codec (HumeAI/tada-codec). The encoder\nproduces 1:1 aligned token embeddings from reference audio, and the\ncausal LM generates speech via flow-matching diffusion.\n\n24kHz output, bf16 inference on CUDA, fp32 on CPU.\n\"\"\"\n\nimport asyncio\nimport logging\nimport threading\nfrom typing import ClassVar, List, Optional, Tuple\n\nimport numpy as np\n\nfrom . import TTSBackend\nfrom .base import (\n    is_model_cached,\n    get_torch_device,\n    combine_voice_prompts as _combine_voice_prompts,\n    model_load_progress,\n)\nfrom ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt\n\nlogger = logging.getLogger(__name__)\n\n# HuggingFace repos\nTADA_CODEC_REPO = \"HumeAI/tada-codec\"\nTADA_1B_REPO = \"HumeAI/tada-1b\"\nTADA_3B_ML_REPO = \"HumeAI/tada-3b-ml\"\n\nTADA_MODEL_REPOS = {\n    \"1B\": TADA_1B_REPO,\n    \"3B\": TADA_3B_ML_REPO,\n}\n\n# Key weight files for cache detection\n_TADA_MODEL_WEIGHT_FILES = [\n    \"model.safetensors\",\n]\n\n_TADA_CODEC_WEIGHT_FILES = [\n    \"encoder/model.safetensors\",\n]\n\n\nclass HumeTadaBackend:\n    \"\"\"HumeAI TADA TTS backend for high-quality voice cloning.\"\"\"\n\n    _load_lock: ClassVar[threading.Lock] = threading.Lock()\n\n    def __init__(self):\n        self.model = None\n        self.encoder = None\n        self.model_size = \"1B\"  # default to 1B\n        self._device = None\n        self._model_load_lock = asyncio.Lock()\n\n    def _get_device(self) -> str:\n        # Force CPU on macOS — MPS has issues with flow matching\n        # and large vocab lm_head (>65536 output channels)\n        return get_torch_device(force_cpu_on_mac=True)\n\n    def is_loaded(self) -> bool:\n        return self.model is not None\n\n    def _get_model_path(self, model_size: str = \"1B\") -> str:\n        return TADA_MODEL_REPOS.get(model_size, TADA_1B_REPO)\n\n    def _is_model_cached(self, model_size: str = \"1B\") -> bool:\n        repo = TADA_MODEL_REPOS.get(model_size, TADA_1B_REPO)\n        model_cached = is_model_cached(repo, required_files=_TADA_MODEL_WEIGHT_FILES)\n        codec_cached = is_model_cached(TADA_CODEC_REPO, required_files=_TADA_CODEC_WEIGHT_FILES)\n        return model_cached and codec_cached\n\n    async def load_model(self, model_size: str = \"1B\") -> None:\n        \"\"\"Load the TADA model and encoder.\"\"\"\n        if self.model is not None and self.model_size == model_size:\n            return\n        async with self._model_load_lock:\n            if self.model is not None and self.model_size == model_size:\n                return\n            # Unload existing model if switching sizes\n            if self.model is not None:\n                self.unload_model()\n            self.model_size = model_size\n            await asyncio.to_thread(self._load_model_sync, model_size)\n\n    def _load_model_sync(self, model_size: str = \"1B\"):\n        \"\"\"Synchronous model loading with progress tracking.\"\"\"\n        model_name = f\"tada-{model_size.lower()}\"\n        is_cached = self._is_model_cached(model_size)\n        repo = TADA_MODEL_REPOS.get(model_size, TADA_1B_REPO)\n\n        with model_load_progress(model_name, is_cached):\n            # Install DAC shim before importing tada — tada's encoder/decoder\n            # import dac.nn.layers.Snake1d which requires the descript-audio-codec\n            # package.  The real package pulls in onnx/tensorboard/matplotlib via\n            # descript-audiotools, so we use a lightweight shim instead.\n            from ..utils.dac_shim import install_dac_shim\n            install_dac_shim()\n\n            import torch\n            from huggingface_hub import snapshot_download\n\n            device = self._get_device()\n            self._device = device\n            logger.info(f\"Loading HumeAI TADA {model_size} on {device}...\")\n\n            # Download codec (encoder + decoder) if not cached\n            logger.info(\"Downloading TADA codec...\")\n            snapshot_download(\n                repo_id=TADA_CODEC_REPO,\n                token=None,\n                allow_patterns=[\"*.safetensors\", \"*.json\", \"*.txt\", \"*.bin\"],\n            )\n\n            # Download model weights if not cached\n            logger.info(f\"Downloading TADA {model_size} model...\")\n            snapshot_download(\n                repo_id=repo,\n                token=None,\n                allow_patterns=[\"*.safetensors\", \"*.json\", \"*.txt\", \"*.bin\", \"*.model\"],\n            )\n\n            # TADA hardcodes \"meta-llama/Llama-3.2-1B\" as the tokenizer\n            # source in its Aligner and TadaForCausalLM.from_pretrained().\n            # That repo is gated (requires Meta license acceptance).\n            # Download the tokenizer from an ungated mirror and get its\n            # local cache path so we can point TADA at it directly.\n            logger.info(\"Downloading Llama tokenizer (ungated mirror)...\")\n            tokenizer_path = snapshot_download(\n                repo_id=\"unsloth/Llama-3.2-1B\",\n                token=None,\n                allow_patterns=[\"tokenizer*\", \"special_tokens*\"],\n            )\n\n            # Determine dtype — use bf16 on CUDA for ~50% memory savings\n            if device == \"cuda\" and torch.cuda.is_bf16_supported():\n                model_dtype = torch.bfloat16\n            else:\n                model_dtype = torch.float32\n\n            # Patch the Aligner config class to use the local tokenizer\n            # path instead of the gated \"meta-llama/Llama-3.2-1B\" default.\n            # This avoids monkey-patching AutoTokenizer.from_pretrained\n            # which corrupts the classmethod descriptor for other engines.\n            from tada.modules.aligner import AlignerConfig\n            AlignerConfig.tokenizer_name = tokenizer_path\n\n            # Load encoder (only needed for voice prompt encoding)\n            from tada.modules.encoder import Encoder\n            logger.info(\"Loading TADA encoder...\")\n            self.encoder = Encoder.from_pretrained(\n                TADA_CODEC_REPO, subfolder=\"encoder\"\n            ).to(device)\n            self.encoder.eval()\n\n            # Load the causal LM (includes decoder for wav generation).\n            # TadaForCausalLM.from_pretrained() calls\n            #   getattr(config, \"tokenizer_name\", \"meta-llama/Llama-3.2-1B\")\n            # which hits the gated repo. Pre-load the config from HF,\n            # inject the local tokenizer path, then pass it in.\n            from tada.modules.tada import TadaForCausalLM, TadaConfig\n            logger.info(f\"Loading TADA {model_size} model...\")\n            config = TadaConfig.from_pretrained(repo)\n            config.tokenizer_name = tokenizer_path\n            self.model = TadaForCausalLM.from_pretrained(\n                repo, config=config, torch_dtype=model_dtype\n            ).to(device)\n            self.model.eval()\n\n        logger.info(f\"HumeAI TADA {model_size} loaded successfully on {device}\")\n\n    def unload_model(self) -> None:\n        \"\"\"Unload model and encoder to free memory.\"\"\"\n        if self.model is not None:\n            del self.model\n            self.model = None\n        if self.encoder is not None:\n            del self.encoder\n            self.encoder = None\n\n        self._device = None\n\n        import torch\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n\n        logger.info(\"HumeAI TADA unloaded\")\n\n    async def create_voice_prompt(\n        self,\n        audio_path: str,\n        reference_text: str,\n        use_cache: bool = True,\n    ) -> Tuple[dict, bool]:\n        \"\"\"\n        Create voice prompt from reference audio using TADA's encoder.\n\n        TADA's encoder performs forced alignment between audio and text tokens,\n        producing an EncoderOutput with 1:1 token-audio alignment. If no\n        reference_text is provided, the encoder uses built-in ASR (English only).\n\n        We serialize the EncoderOutput to a dict for caching.\n        \"\"\"\n        await self.load_model(self.model_size)\n\n        cache_key = (\n            \"tada_\" + get_cache_key(audio_path, reference_text)\n        ) if use_cache else None\n\n        if cache_key:\n            cached = get_cached_voice_prompt(cache_key)\n            if cached is not None and isinstance(cached, dict):\n                return cached, True\n\n        def _encode_sync():\n            import torch\n            import soundfile as sf\n\n            device = self._device\n\n            # Load audio with soundfile (torchaudio 2.10+ requires torchcodec)\n            audio_np, sr = sf.read(str(audio_path), dtype=\"float32\")\n            audio = torch.from_numpy(audio_np).float()\n            if audio.ndim == 1:\n                audio = audio.unsqueeze(0)  # (samples,) -> (1, samples)\n            else:\n                audio = audio.T  # (samples, channels) -> (channels, samples)\n            audio = audio.to(device)\n\n            # Encode with forced alignment\n            text_arg = [reference_text] if reference_text else None\n            prompt = self.encoder(\n                audio, text=text_arg, sample_rate=sr\n            )\n\n            # Serialize EncoderOutput to a dict of CPU tensors for caching\n            prompt_dict = {}\n            for field_name in prompt.__dataclass_fields__:\n                val = getattr(prompt, field_name)\n                if isinstance(val, torch.Tensor):\n                    prompt_dict[field_name] = val.detach().cpu()\n                elif isinstance(val, list):\n                    prompt_dict[field_name] = val\n                elif isinstance(val, (int, float)):\n                    prompt_dict[field_name] = val\n                else:\n                    prompt_dict[field_name] = val\n            return prompt_dict\n\n        encoded = await asyncio.to_thread(_encode_sync)\n\n        if cache_key:\n            cache_voice_prompt(cache_key, encoded)\n\n        return encoded, False\n\n    async def combine_voice_prompts(\n        self,\n        audio_paths: List[str],\n        reference_texts: List[str],\n    ) -> Tuple[np.ndarray, str]:\n        return await _combine_voice_prompts(audio_paths, reference_texts, sample_rate=24000)\n\n    async def generate(\n        self,\n        text: str,\n        voice_prompt: dict,\n        language: str = \"en\",\n        seed: Optional[int] = None,\n        instruct: Optional[str] = None,\n    ) -> Tuple[np.ndarray, int]:\n        \"\"\"\n        Generate audio from text using HumeAI TADA.\n\n        Args:\n            text: Text to synthesize\n            voice_prompt: Serialized EncoderOutput dict from create_voice_prompt()\n            language: Language code (en, ar, de, es, fr, it, ja, pl, pt, zh)\n            seed: Random seed for reproducibility\n            instruct: Not supported by TADA (ignored)\n\n        Returns:\n            Tuple of (audio_array, sample_rate=24000)\n        \"\"\"\n        await self.load_model(self.model_size)\n\n        def _generate_sync():\n            import torch\n            from tada.modules.encoder import EncoderOutput\n\n            if seed is not None:\n                torch.manual_seed(seed)\n                if torch.cuda.is_available():\n                    torch.cuda.manual_seed(seed)\n\n            device = self._device\n\n            # Reconstruct EncoderOutput from the cached dict\n            restored = {}\n            for k, v in voice_prompt.items():\n                if isinstance(v, torch.Tensor):\n                    # Move to device and match model dtype for float tensors\n                    if v.is_floating_point():\n                        model_dtype = next(self.model.parameters()).dtype\n                        restored[k] = v.to(device=device, dtype=model_dtype)\n                    else:\n                        restored[k] = v.to(device=device)\n                else:\n                    restored[k] = v\n\n            prompt = EncoderOutput(**restored)\n\n            # For non-English with the 3B-ML model, we could reload the\n            # encoder with the language-specific aligner. However, the\n            # generation itself is language-agnostic — only the encoder's\n            # aligner changes. Since we encode at create_voice_prompt time,\n            # the language is already baked in. For simplicity, we don't\n            # reload the encoder here.\n\n            logger.info(f\"[TADA] Generating ({language}), text length: {len(text)}\")\n\n            output = self.model.generate(\n                prompt=prompt,\n                text=text,\n            )\n\n            # output.audio is a list of tensors (one per batch item)\n            if output.audio and output.audio[0] is not None:\n                audio_tensor = output.audio[0]\n                audio = audio_tensor.detach().cpu().numpy().squeeze().astype(np.float32)\n            else:\n                logger.warning(\"[TADA] Generation produced no audio\")\n                audio = np.zeros(24000, dtype=np.float32)\n\n            return audio, 24000\n\n        return await asyncio.to_thread(_generate_sync)\n"
  },
  {
    "path": "backend/backends/kokoro_backend.py",
    "content": "\"\"\"\nKokoro TTS backend implementation.\n\nWraps the Kokoro-82M model for fast, lightweight text-to-speech.\n82M parameters, CPU realtime, 24kHz output, Apache 2.0 license.\n\nKokoro uses pre-built voice style vectors (not traditional zero-shot cloning\nfrom arbitrary audio). Voice prompts are stored as deferred references to\nHF-hosted voice .pt files.\n\nLanguages supported (via misaki G2P):\n  - American English (a), British English (b)\n  - Spanish (e), French (f), Hindi (h), Italian (i), Portuguese (p)\n  - Japanese (j) — requires misaki[ja]\n  - Chinese (z) — requires misaki[zh]\n\"\"\"\n\nimport asyncio\nimport logging\nimport os\nfrom typing import Optional\n\nimport numpy as np\n\nfrom . import TTSBackend\nfrom .base import (\n    get_torch_device,\n    combine_voice_prompts as _combine_voice_prompts,\n    model_load_progress,\n)\n\nlogger = logging.getLogger(__name__)\n\n# HuggingFace repo for model + voice detection\nKOKORO_HF_REPO = \"hexgrad/Kokoro-82M\"\nKOKORO_SAMPLE_RATE = 24000\n\n# Default voice if none specified\nKOKORO_DEFAULT_VOICE = \"af_heart\"\n\n# All available Kokoro voices: (voice_id, display_name, gender, lang_code)\nKOKORO_VOICES = [\n    # American English female\n    (\"af_alloy\", \"Alloy\", \"female\", \"en\"),\n    (\"af_aoede\", \"Aoede\", \"female\", \"en\"),\n    (\"af_bella\", \"Bella\", \"female\", \"en\"),\n    (\"af_heart\", \"Heart\", \"female\", \"en\"),\n    (\"af_jessica\", \"Jessica\", \"female\", \"en\"),\n    (\"af_kore\", \"Kore\", \"female\", \"en\"),\n    (\"af_nicole\", \"Nicole\", \"female\", \"en\"),\n    (\"af_nova\", \"Nova\", \"female\", \"en\"),\n    (\"af_river\", \"River\", \"female\", \"en\"),\n    (\"af_sarah\", \"Sarah\", \"female\", \"en\"),\n    (\"af_sky\", \"Sky\", \"female\", \"en\"),\n    # American English male\n    (\"am_adam\", \"Adam\", \"male\", \"en\"),\n    (\"am_echo\", \"Echo\", \"male\", \"en\"),\n    (\"am_eric\", \"Eric\", \"male\", \"en\"),\n    (\"am_fenrir\", \"Fenrir\", \"male\", \"en\"),\n    (\"am_liam\", \"Liam\", \"male\", \"en\"),\n    (\"am_michael\", \"Michael\", \"male\", \"en\"),\n    (\"am_onyx\", \"Onyx\", \"male\", \"en\"),\n    (\"am_puck\", \"Puck\", \"male\", \"en\"),\n    (\"am_santa\", \"Santa\", \"male\", \"en\"),\n    # British English female\n    (\"bf_alice\", \"Alice\", \"female\", \"en\"),\n    (\"bf_emma\", \"Emma\", \"female\", \"en\"),\n    (\"bf_isabella\", \"Isabella\", \"female\", \"en\"),\n    (\"bf_lily\", \"Lily\", \"female\", \"en\"),\n    # British English male\n    (\"bm_daniel\", \"Daniel\", \"male\", \"en\"),\n    (\"bm_fable\", \"Fable\", \"male\", \"en\"),\n    (\"bm_george\", \"George\", \"male\", \"en\"),\n    (\"bm_lewis\", \"Lewis\", \"male\", \"en\"),\n    # Spanish\n    (\"ef_dora\", \"Dora\", \"female\", \"es\"),\n    (\"em_alex\", \"Alex\", \"male\", \"es\"),\n    (\"em_santa\", \"Santa\", \"male\", \"es\"),\n    # French\n    (\"ff_siwis\", \"Siwis\", \"female\", \"fr\"),\n    # Hindi\n    (\"hf_alpha\", \"Alpha\", \"female\", \"hi\"),\n    (\"hf_beta\", \"Beta\", \"female\", \"hi\"),\n    (\"hm_omega\", \"Omega\", \"male\", \"hi\"),\n    (\"hm_psi\", \"Psi\", \"male\", \"hi\"),\n    # Italian\n    (\"if_sara\", \"Sara\", \"female\", \"it\"),\n    (\"im_nicola\", \"Nicola\", \"male\", \"it\"),\n    # Japanese\n    (\"jf_alpha\", \"Alpha\", \"female\", \"ja\"),\n    (\"jf_gongitsune\", \"Gongitsune\", \"female\", \"ja\"),\n    (\"jf_nezumi\", \"Nezumi\", \"female\", \"ja\"),\n    (\"jf_tebukuro\", \"Tebukuro\", \"female\", \"ja\"),\n    (\"jm_kumo\", \"Kumo\", \"male\", \"ja\"),\n    # Portuguese\n    (\"pf_dora\", \"Dora\", \"female\", \"pt\"),\n    (\"pm_alex\", \"Alex\", \"male\", \"pt\"),\n    (\"pm_santa\", \"Santa\", \"male\", \"pt\"),\n    # Chinese\n    (\"zf_xiaobei\", \"Xiaobei\", \"female\", \"zh\"),\n    (\"zf_xiaoni\", \"Xiaoni\", \"female\", \"zh\"),\n    (\"zf_xiaoxiao\", \"Xiaoxiao\", \"female\", \"zh\"),\n    (\"zf_xiaoyi\", \"Xiaoyi\", \"female\", \"zh\"),\n]\n\n# Map our ISO language codes to Kokoro lang_code characters\nLANG_CODE_MAP = {\n    \"en\": \"a\",  # American English\n    \"es\": \"e\",\n    \"fr\": \"f\",\n    \"hi\": \"h\",\n    \"it\": \"i\",\n    \"pt\": \"p\",\n    \"ja\": \"j\",\n    \"zh\": \"z\",\n}\n\n\nclass KokoroTTSBackend:\n    \"\"\"Kokoro-82M TTS backend — tiny, fast, CPU-friendly.\"\"\"\n\n    def __init__(self):\n        self._model = None\n        self._pipelines: dict = {}  # lang_code -> KPipeline\n        self._device: Optional[str] = None\n        self.model_size = \"default\"\n\n    def _get_device(self) -> str:\n        \"\"\"Select device. Kokoro supports CUDA and CPU. MPS needs fallback env var.\"\"\"\n        device = get_torch_device(allow_mps=False)\n        # Kokoro can use MPS but requires PYTORCH_ENABLE_MPS_FALLBACK=1\n        # For now, skip MPS to avoid user confusion — CPU is already realtime\n        return device\n\n    @property\n    def device(self) -> str:\n        if self._device is None:\n            self._device = self._get_device()\n        return self._device\n\n    def is_loaded(self) -> bool:\n        return self._model is not None\n\n    def _get_model_path(self, model_size: str) -> str:\n        return KOKORO_HF_REPO\n\n    def _is_model_cached(self, model_size: str = \"default\") -> bool:\n        \"\"\"Check if Kokoro model files are cached locally.\"\"\"\n        from .base import is_model_cached\n\n        return is_model_cached(\n            KOKORO_HF_REPO,\n            required_files=[\"config.json\", \"kokoro-v1_0.pth\"],\n        )\n\n    async def load_model(self, model_size: str = \"default\") -> None:\n        \"\"\"Load the Kokoro model.\"\"\"\n        if self._model is not None:\n            return\n        await asyncio.to_thread(self._load_model_sync)\n\n    def _load_model_sync(self):\n        \"\"\"Synchronous model loading.\"\"\"\n        model_name = \"kokoro\"\n        is_cached = self._is_model_cached()\n\n        with model_load_progress(model_name, is_cached):\n            from kokoro import KModel\n\n            device = self.device\n            logger.info(f\"Loading Kokoro-82M on {device}...\")\n\n            self._model = KModel(repo_id=KOKORO_HF_REPO).to(device).eval()\n\n        logger.info(\"Kokoro-82M loaded successfully\")\n\n    def _get_pipeline(self, lang_code: str):\n        \"\"\"Get or create a KPipeline for the given language code.\"\"\"\n        kokoro_lang = LANG_CODE_MAP.get(lang_code, \"a\")\n\n        if kokoro_lang not in self._pipelines:\n            from kokoro import KPipeline\n\n            # Create pipeline with our existing model (no redundant model loading)\n            self._pipelines[kokoro_lang] = KPipeline(\n                lang_code=kokoro_lang,\n                repo_id=KOKORO_HF_REPO,\n                model=self._model,\n            )\n\n        return self._pipelines[kokoro_lang]\n\n    def unload_model(self) -> None:\n        \"\"\"Unload model to free memory.\"\"\"\n        if self._model is not None:\n            del self._model\n            self._model = None\n            self._pipelines.clear()\n\n            import torch\n\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n\n            logger.info(\"Kokoro unloaded\")\n\n    async def create_voice_prompt(\n        self,\n        audio_path: str,\n        reference_text: str,\n        use_cache: bool = True,\n    ) -> tuple[dict, bool]:\n        \"\"\"\n        Create voice prompt for Kokoro.\n\n        Kokoro doesn't do traditional voice cloning from arbitrary audio.\n        When called for a cloned profile (fallback), uses the default voice.\n        For preset profiles, the voice_prompt dict is built by the profile\n        service and bypasses this method entirely.\n        \"\"\"\n        return {\n            \"voice_type\": \"preset\",\n            \"preset_engine\": \"kokoro\",\n            \"preset_voice_id\": KOKORO_DEFAULT_VOICE,\n        }, False\n\n    async def combine_voice_prompts(\n        self,\n        audio_paths: list[str],\n        reference_texts: list[str],\n    ) -> tuple[np.ndarray, str]:\n        \"\"\"Combine voice prompts — uses base implementation for audio concatenation.\"\"\"\n        return await _combine_voice_prompts(\n            audio_paths, reference_texts, sample_rate=KOKORO_SAMPLE_RATE\n        )\n\n    async def generate(\n        self,\n        text: str,\n        voice_prompt: dict,\n        language: str = \"en\",\n        seed: Optional[int] = None,\n        instruct: Optional[str] = None,\n    ) -> tuple[np.ndarray, int]:\n        \"\"\"\n        Generate audio from text using Kokoro.\n\n        Args:\n            text: Text to synthesize\n            voice_prompt: Dict with kokoro_voice key\n            language: Language code\n            seed: Random seed for reproducibility\n            instruct: Not supported by Kokoro (ignored)\n\n        Returns:\n            Tuple of (audio_array, sample_rate)\n        \"\"\"\n        await self.load_model()\n\n        voice_name = voice_prompt.get(\"preset_voice_id\") or voice_prompt.get(\"kokoro_voice\") or KOKORO_DEFAULT_VOICE\n\n        def _generate_sync():\n            import torch\n\n            if seed is not None:\n                torch.manual_seed(seed)\n                if torch.cuda.is_available():\n                    torch.cuda.manual_seed(seed)\n\n            pipeline = self._get_pipeline(language)\n\n            # Generate all chunks and concatenate\n            audio_chunks = []\n            for result in pipeline(text, voice=voice_name, speed=1.0):\n                if result.audio is not None:\n                    chunk = result.audio\n                    if isinstance(chunk, torch.Tensor):\n                        chunk = chunk.detach().cpu().numpy()\n                    audio_chunks.append(chunk.squeeze())\n\n            if not audio_chunks:\n                # Return 1 second of silence as fallback\n                return np.zeros(KOKORO_SAMPLE_RATE, dtype=np.float32), KOKORO_SAMPLE_RATE\n\n            audio = np.concatenate(audio_chunks)\n            return audio.astype(np.float32), KOKORO_SAMPLE_RATE\n\n        return await asyncio.to_thread(_generate_sync)\n"
  },
  {
    "path": "backend/backends/luxtts_backend.py",
    "content": "\"\"\"\nLuxTTS backend implementation.\n\nWraps the LuxTTS (ZipVoice) model for zero-shot voice cloning.\n~1GB VRAM, 48kHz output, 150x realtime on CPU.\n\"\"\"\n\nimport asyncio\nimport logging\nfrom typing import Optional, Tuple\n\nimport numpy as np\n\nfrom . import TTSBackend\nfrom .base import is_model_cached, get_torch_device, combine_voice_prompts as _combine_voice_prompts, model_load_progress\nfrom ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt\n\nlogger = logging.getLogger(__name__)\n\n# HuggingFace repo for model weight detection\nLUXTTS_HF_REPO = \"YatharthS/LuxTTS\"\n\n\nclass LuxTTSBackend:\n    \"\"\"LuxTTS backend for zero-shot voice cloning.\"\"\"\n\n    def __init__(self):\n        self.model = None\n        self.model_size = \"default\"  # LuxTTS has only one model size\n        self._device = None\n\n    def _get_device(self) -> str:\n        return get_torch_device(allow_mps=True)\n\n    def is_loaded(self) -> bool:\n        return self.model is not None\n\n    @property\n    def device(self) -> str:\n        if self._device is None:\n            self._device = self._get_device()\n        return self._device\n\n    def _get_model_path(self, model_size: str) -> str:\n        return LUXTTS_HF_REPO\n\n    def _is_model_cached(self, model_size: str = \"default\") -> bool:\n        return is_model_cached(\n            LUXTTS_HF_REPO,\n            weight_extensions=(\".pt\", \".safetensors\", \".onnx\", \".bin\"),\n        )\n\n    async def load_model(self, model_size: str = \"default\") -> None:\n        \"\"\"Load the LuxTTS model.\"\"\"\n        if self.model is not None:\n            return\n\n        await asyncio.to_thread(self._load_model_sync)\n\n    def _load_model_sync(self):\n        model_name = \"luxtts\"\n        is_cached = self._is_model_cached()\n\n        with model_load_progress(model_name, is_cached):\n            from zipvoice.luxvoice import LuxTTS\n\n            device = self.device\n            logger.info(f\"Loading LuxTTS on {device}...\")\n\n            if device == \"cpu\":\n                import os\n                threads = os.cpu_count() or 4\n                self.model = LuxTTS(\n                    model_path=LUXTTS_HF_REPO, device=\"cpu\", threads=min(threads, 8),\n                )\n            else:\n                self.model = LuxTTS(model_path=LUXTTS_HF_REPO, device=device)\n\n        logger.info(\"LuxTTS loaded successfully\")\n\n    def unload_model(self) -> None:\n        \"\"\"Unload model to free memory.\"\"\"\n        if self.model is not None:\n            del self.model\n            self.model = None\n\n            import torch\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n\n            logger.info(\"LuxTTS unloaded\")\n\n    async def create_voice_prompt(\n        self,\n        audio_path: str,\n        reference_text: str,\n        use_cache: bool = True,\n    ) -> Tuple[dict, bool]:\n        \"\"\"\n        Create voice prompt from reference audio.\n\n        LuxTTS uses its own encode_prompt() which runs Whisper ASR internally\n        to transcribe the reference. The reference_text parameter is not used\n        by LuxTTS itself, but we include it in the cache key for consistency.\n        \"\"\"\n        await self.load_model()\n\n        # Compute cache key once for both lookup and storage\n        cache_key = (\"luxtts_\" + get_cache_key(audio_path, reference_text)) if use_cache else None\n\n        if cache_key:\n            cached = get_cached_voice_prompt(cache_key)\n            if cached is not None and isinstance(cached, dict):\n                return cached, True\n\n        def _encode_sync():\n            return self.model.encode_prompt(\n                prompt_audio=str(audio_path),\n                duration=5,\n                rms=0.01,\n            )\n\n        encoded = await asyncio.to_thread(_encode_sync)\n\n        if cache_key:\n            cache_voice_prompt(cache_key, encoded)\n\n        return encoded, False\n\n    async def combine_voice_prompts(self, audio_paths, reference_texts):\n        return await _combine_voice_prompts(audio_paths, reference_texts, sample_rate=24000)\n\n    async def generate(\n        self,\n        text: str,\n        voice_prompt: dict,\n        language: str = \"en\",\n        seed: Optional[int] = None,\n        instruct: Optional[str] = None,\n    ) -> Tuple[np.ndarray, int]:\n        \"\"\"\n        Generate audio from text using LuxTTS.\n\n        Args:\n            text: Text to synthesize\n            voice_prompt: Encoded prompt dict from encode_prompt()\n            language: Language code (LuxTTS is English-focused)\n            seed: Random seed for reproducibility\n            instruct: Not supported by LuxTTS (ignored)\n\n        Returns:\n            Tuple of (audio_array, sample_rate)\n        \"\"\"\n        await self.load_model()\n\n        def _generate_sync():\n            import torch\n\n            if seed is not None:\n                torch.manual_seed(seed)\n                if torch.cuda.is_available():\n                    torch.cuda.manual_seed(seed)\n\n            wav = self.model.generate_speech(\n                text=text,\n                encode_dict=voice_prompt,\n                num_steps=4,\n                guidance_scale=3.0,\n                t_shift=0.5,\n                speed=1.0,\n                return_smooth=False,  # 48kHz output\n            )\n\n            # LuxTTS returns a tensor (may be on GPU/MPS), move to CPU first\n            audio = wav.detach().cpu().numpy().squeeze()\n            return audio, 48000\n\n        return await asyncio.to_thread(_generate_sync)\n"
  },
  {
    "path": "backend/backends/mlx_backend.py",
    "content": "\"\"\"\nMLX backend implementation for TTS and STT using mlx-audio.\n\"\"\"\n\nfrom typing import Optional, List, Tuple\nimport asyncio\nimport logging\nimport numpy as np\nimport os\nfrom pathlib import Path\n\nlogger = logging.getLogger(__name__)\n\n# PATCH: Import and apply offline patch BEFORE any huggingface_hub usage\n# This prevents mlx_audio from making network requests when models are cached\nfrom ..utils.hf_offline_patch import patch_huggingface_hub_offline, ensure_original_qwen_config_cached\n\npatch_huggingface_hub_offline()\nensure_original_qwen_config_cached()\n\nfrom . import TTSBackend, STTBackend, LANGUAGE_CODE_TO_NAME, WHISPER_HF_REPOS\nfrom .base import is_model_cached, combine_voice_prompts as _combine_voice_prompts, model_load_progress\nfrom ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt\n\n\nclass MLXTTSBackend:\n    \"\"\"MLX-based TTS backend using mlx-audio.\"\"\"\n\n    def __init__(self, model_size: str = \"1.7B\"):\n        self.model = None\n        self.model_size = model_size\n        self._current_model_size = None\n\n    def is_loaded(self) -> bool:\n        \"\"\"Check if model is loaded.\"\"\"\n        return self.model is not None\n\n    def _get_model_path(self, model_size: str) -> str:\n        \"\"\"\n        Get the MLX model path.\n\n        Args:\n            model_size: Model size (1.7B or 0.6B)\n\n        Returns:\n            HuggingFace Hub model ID for MLX\n        \"\"\"\n        # MLX model mapping\n        mlx_model_map = {\n            \"1.7B\": \"mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16\",\n            # 0.6B not yet converted to MLX format\n            \"0.6B\": \"mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16\",  # Fallback to 1.7B\n        }\n\n        if model_size not in mlx_model_map:\n            raise ValueError(f\"Unknown model size: {model_size}\")\n\n        hf_model_id = mlx_model_map[model_size]\n        logger.info(\"Will download MLX model from HuggingFace Hub: %s\", hf_model_id)\n\n        return hf_model_id\n\n    def _is_model_cached(self, model_size: str) -> bool:\n        return is_model_cached(\n            self._get_model_path(model_size),\n            weight_extensions=(\".safetensors\", \".bin\", \".npz\"),\n        )\n\n    async def load_model_async(self, model_size: Optional[str] = None):\n        \"\"\"\n        Lazy load the MLX TTS model.\n\n        Args:\n            model_size: Model size to load (1.7B or 0.6B)\n        \"\"\"\n        if model_size is None:\n            model_size = self.model_size\n\n        # If already loaded with correct size, return\n        if self.model is not None and self._current_model_size == model_size:\n            return\n\n        # Unload existing model if different size requested\n        if self.model is not None and self._current_model_size != model_size:\n            self.unload_model()\n\n        # Run blocking load in thread pool\n        await asyncio.to_thread(self._load_model_sync, model_size)\n\n    # Alias for compatibility\n    load_model = load_model_async\n\n    def _load_model_sync(self, model_size: str):\n        \"\"\"Synchronous model loading.\"\"\"\n        model_path = self._get_model_path(model_size)\n        model_name = f\"qwen-tts-{model_size}\"\n        is_cached = self._is_model_cached(model_size)\n\n        # Force offline mode when cached to avoid network requests\n        original_hf_hub_offline = os.environ.get(\"HF_HUB_OFFLINE\")\n        if is_cached:\n            os.environ[\"HF_HUB_OFFLINE\"] = \"1\"\n            logger.info(\"[PATCH] Model %s is cached, forcing HF_HUB_OFFLINE=1 to avoid network requests\", model_size)\n\n        try:\n            with model_load_progress(model_name, is_cached):\n                from mlx_audio.tts import load\n\n                logger.info(\"Loading MLX TTS model %s...\", model_size)\n\n                try:\n                    self.model = load(model_path)\n                except Exception as load_error:\n                    if is_cached and \"offline\" in str(load_error).lower():\n                        logger.warning(\"[PATCH] Offline load failed, trying with network: %s\", load_error)\n                        os.environ.pop(\"HF_HUB_OFFLINE\", None)\n                        self.model = load(model_path)\n                    else:\n                        raise\n        finally:\n            if original_hf_hub_offline is not None:\n                os.environ[\"HF_HUB_OFFLINE\"] = original_hf_hub_offline\n            else:\n                os.environ.pop(\"HF_HUB_OFFLINE\", None)\n\n        self._current_model_size = model_size\n        self.model_size = model_size\n        logger.info(\"MLX TTS model %s loaded successfully\", model_size)\n\n    def unload_model(self):\n        \"\"\"Unload the model to free memory.\"\"\"\n        if self.model is not None:\n            del self.model\n            self.model = None\n            self._current_model_size = None\n            logger.info(\"MLX TTS model unloaded\")\n\n    async def create_voice_prompt(\n        self,\n        audio_path: str,\n        reference_text: str,\n        use_cache: bool = True,\n    ) -> Tuple[dict, bool]:\n        \"\"\"\n        Create voice prompt from reference audio.\n\n        MLX backend stores voice prompt as a dict with audio path and text.\n        The actual voice prompt processing happens during generation.\n\n        Args:\n            audio_path: Path to reference audio file\n            reference_text: Transcript of reference audio\n            use_cache: Whether to use cached prompt if available\n\n        Returns:\n            Tuple of (voice_prompt_dict, was_cached)\n        \"\"\"\n        await self.load_model_async(None)\n\n        # Check cache if enabled\n        if use_cache:\n            cache_key = get_cache_key(audio_path, reference_text)\n            cached_prompt = get_cached_voice_prompt(cache_key)\n            if cached_prompt is not None:\n                # Return cached prompt (should be dict format)\n                if isinstance(cached_prompt, dict):\n                    # Validate that the cached audio file still exists\n                    cached_audio_path = cached_prompt.get(\"ref_audio\") or cached_prompt.get(\"ref_audio_path\")\n                    if cached_audio_path and Path(cached_audio_path).exists():\n                        return cached_prompt, True\n                    else:\n                        # Cached file no longer exists, invalidate cache\n                        logger.warning(\"Cached audio file not found: %s, regenerating prompt\", cached_audio_path)\n\n        # MLX voice prompt format - store audio path and text\n        # The model will process this during generation\n        voice_prompt_items = {\n            \"ref_audio\": str(audio_path),\n            \"ref_text\": reference_text,\n        }\n\n        # Cache if enabled\n        if use_cache:\n            cache_key = get_cache_key(audio_path, reference_text)\n            cache_voice_prompt(cache_key, voice_prompt_items)\n\n        return voice_prompt_items, False\n\n    async def combine_voice_prompts(self, audio_paths, reference_texts):\n        return await _combine_voice_prompts(audio_paths, reference_texts)\n\n    async def generate(\n        self,\n        text: str,\n        voice_prompt: dict,\n        language: str = \"en\",\n        seed: Optional[int] = None,\n        instruct: Optional[str] = None,\n    ) -> Tuple[np.ndarray, int]:\n        \"\"\"\n        Generate audio from text using voice prompt.\n\n        Args:\n            text: Text to synthesize\n            voice_prompt: Voice prompt dictionary with ref_audio and ref_text\n            language: Language code (en or zh) - may not be fully supported by MLX\n            seed: Random seed for reproducibility\n            instruct: Natural language instruction (may not be supported by MLX)\n\n        Returns:\n            Tuple of (audio_array, sample_rate)\n        \"\"\"\n        await self.load_model_async(None)\n\n        logger.info(\"Generating audio for text: %s\", text)\n\n        def _generate_sync():\n            \"\"\"Run synchronous generation in thread pool.\"\"\"\n            # MLX generate() returns a generator yielding GenerationResult objects\n            audio_chunks = []\n            sample_rate = 24000\n            lang = LANGUAGE_CODE_TO_NAME.get(language, \"auto\")\n\n            # Set seed if provided (MLX uses numpy random)\n            if seed is not None:\n                import mlx.core as mx\n\n                np.random.seed(seed)\n                mx.random.seed(seed)\n\n            # Extract voice prompt info\n            ref_audio = voice_prompt.get(\"ref_audio\") or voice_prompt.get(\"ref_audio_path\")\n            ref_text = voice_prompt.get(\"ref_text\", \"\")\n\n            # Validate that the audio file exists\n            if ref_audio and not Path(ref_audio).exists():\n                logger.warning(\"Audio file not found: %s\", ref_audio)\n                logger.warning(\"This may be due to a cached voice prompt referencing a deleted temp file.\")\n                logger.warning(\"Regenerating without voice prompt.\")\n                ref_audio = None\n\n            # Check if model supports voice cloning via generate method\n            # MLX API may support ref_audio parameter directly\n            try:\n                # Try with voice cloning parameters if supported\n                if ref_audio:\n                    # Check if generate accepts ref_audio parameter\n                    import inspect\n\n                    sig = inspect.signature(self.model.generate)\n                    if \"ref_audio\" in sig.parameters:\n                        # Generate with voice cloning\n                        for result in self.model.generate(text, ref_audio=ref_audio, ref_text=ref_text, lang_code=lang):\n                            audio_chunks.append(np.array(result.audio))\n                            sample_rate = result.sample_rate\n                    else:\n                        # Fallback: generate without voice cloning\n                        for result in self.model.generate(text, lang_code=lang):\n                            audio_chunks.append(np.array(result.audio))\n                            sample_rate = result.sample_rate\n                else:\n                    # No voice prompt, generate normally\n                    for result in self.model.generate(text, lang_code=lang):\n                        audio_chunks.append(np.array(result.audio))\n                        sample_rate = result.sample_rate\n            except Exception as e:\n                # If voice cloning fails, try without it\n                logger.warning(\"Voice cloning failed, generating without voice prompt: %s\", e)\n                for result in self.model.generate(text, lang_code=lang):\n                    audio_chunks.append(np.array(result.audio))\n                    sample_rate = result.sample_rate\n\n            # Concatenate all chunks\n            if audio_chunks:\n                audio = np.concatenate([np.asarray(chunk, dtype=np.float32) for chunk in audio_chunks])\n            else:\n                # Fallback: empty audio\n                audio = np.array([], dtype=np.float32)\n\n            return audio, sample_rate\n\n        # Run blocking inference in thread pool\n        audio, sample_rate = await asyncio.to_thread(_generate_sync)\n\n        return audio, sample_rate\n\n\nclass MLXSTTBackend:\n    \"\"\"MLX-based STT backend using mlx-audio Whisper.\"\"\"\n\n    def __init__(self, model_size: str = \"base\"):\n        self.model = None\n        self.model_size = model_size\n\n    def is_loaded(self) -> bool:\n        \"\"\"Check if model is loaded.\"\"\"\n        return self.model is not None\n\n    def _is_model_cached(self, model_size: str) -> bool:\n        hf_repo = WHISPER_HF_REPOS.get(model_size, f\"openai/whisper-{model_size}\")\n        return is_model_cached(hf_repo, weight_extensions=(\".safetensors\", \".bin\", \".npz\"))\n\n    async def load_model_async(self, model_size: Optional[str] = None):\n        \"\"\"\n        Lazy load the MLX Whisper model.\n\n        Args:\n            model_size: Model size (tiny, base, small, medium, large)\n        \"\"\"\n        if model_size is None:\n            model_size = self.model_size\n\n        if self.model is not None and self.model_size == model_size:\n            return\n\n        # Run blocking load in thread pool\n        await asyncio.to_thread(self._load_model_sync, model_size)\n\n    # Alias for compatibility\n    load_model = load_model_async\n\n    def _load_model_sync(self, model_size: str):\n        \"\"\"Synchronous model loading.\"\"\"\n        progress_model_name = f\"whisper-{model_size}\"\n        is_cached = self._is_model_cached(model_size)\n\n        with model_load_progress(progress_model_name, is_cached):\n            from mlx_audio.stt import load\n\n            model_name = WHISPER_HF_REPOS.get(model_size, f\"openai/whisper-{model_size}\")\n            logger.info(\"Loading MLX Whisper model %s...\", model_size)\n            self.model = load(model_name)\n\n        self.model_size = model_size\n        logger.info(\"MLX Whisper model %s loaded successfully\", model_size)\n\n    def unload_model(self):\n        \"\"\"Unload the model to free memory.\"\"\"\n        if self.model is not None:\n            del self.model\n            self.model = None\n            logger.info(\"MLX Whisper model unloaded\")\n\n    async def transcribe(\n        self,\n        audio_path: str,\n        language: Optional[str] = None,\n        model_size: Optional[str] = None,\n    ) -> str:\n        \"\"\"\n        Transcribe audio to text.\n\n        Args:\n            audio_path: Path to audio file\n            language: Optional language hint\n            model_size: Optional model size override\n\n        Returns:\n            Transcribed text\n        \"\"\"\n        await self.load_model_async(model_size)\n\n        def _transcribe_sync():\n            \"\"\"Run synchronous transcription in thread pool.\"\"\"\n            # MLX Whisper transcription using generate method\n            # The generate method accepts audio path directly\n            decode_options = {}\n            if language:\n                decode_options[\"language\"] = language\n\n            result = self.model.generate(str(audio_path), **decode_options)\n\n            # Extract text from result\n            if isinstance(result, str):\n                return result.strip()\n            elif isinstance(result, dict):\n                return result.get(\"text\", \"\").strip()\n            elif hasattr(result, \"text\"):\n                return result.text.strip()\n            else:\n                return str(result).strip()\n\n        # Run blocking transcription in thread pool\n        return await asyncio.to_thread(_transcribe_sync)\n"
  },
  {
    "path": "backend/backends/pytorch_backend.py",
    "content": "\"\"\"\nPyTorch backend implementation for TTS and STT.\n\"\"\"\n\nfrom typing import Optional, List, Tuple\nimport asyncio\nimport logging\nimport torch\nimport numpy as np\n\nlogger = logging.getLogger(__name__)\n\nfrom . import TTSBackend, STTBackend, LANGUAGE_CODE_TO_NAME, WHISPER_HF_REPOS\nfrom .base import (\n    is_model_cached,\n    get_torch_device,\n    combine_voice_prompts as _combine_voice_prompts,\n    model_load_progress,\n)\nfrom ..utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_prompt\nfrom ..utils.audio import load_audio\n\n\nclass PyTorchTTSBackend:\n    \"\"\"PyTorch-based TTS backend using Qwen3-TTS.\"\"\"\n\n    def __init__(self, model_size: str = \"1.7B\"):\n        self.model = None\n        self.model_size = model_size\n        self.device = self._get_device()\n        self._current_model_size = None\n\n    def _get_device(self) -> str:\n        \"\"\"Get the best available device.\"\"\"\n        return get_torch_device(allow_xpu=True, allow_directml=True)\n\n    def is_loaded(self) -> bool:\n        \"\"\"Check if model is loaded.\"\"\"\n        return self.model is not None\n\n    def _get_model_path(self, model_size: str) -> str:\n        \"\"\"\n        Get the HuggingFace Hub model ID.\n\n        Args:\n            model_size: Model size (1.7B or 0.6B)\n\n        Returns:\n            HuggingFace Hub model ID\n        \"\"\"\n        hf_model_map = {\n            \"1.7B\": \"Qwen/Qwen3-TTS-12Hz-1.7B-Base\",\n            \"0.6B\": \"Qwen/Qwen3-TTS-12Hz-0.6B-Base\",\n        }\n\n        if model_size not in hf_model_map:\n            raise ValueError(f\"Unknown model size: {model_size}\")\n\n        return hf_model_map[model_size]\n\n    def _is_model_cached(self, model_size: str) -> bool:\n        return is_model_cached(self._get_model_path(model_size))\n\n    async def load_model_async(self, model_size: Optional[str] = None):\n        \"\"\"\n        Lazy load the TTS model with automatic downloading from HuggingFace Hub.\n\n        Args:\n            model_size: Model size to load (1.7B or 0.6B)\n        \"\"\"\n        if model_size is None:\n            model_size = self.model_size\n\n        # If already loaded with correct size, return\n        if self.model is not None and self._current_model_size == model_size:\n            return\n\n        # Unload existing model if different size requested\n        if self.model is not None and self._current_model_size != model_size:\n            self.unload_model()\n\n        # Run blocking load in thread pool\n        await asyncio.to_thread(self._load_model_sync, model_size)\n\n    # Alias for compatibility\n    load_model = load_model_async\n\n    def _load_model_sync(self, model_size: str):\n        \"\"\"Synchronous model loading.\"\"\"\n        model_name = f\"qwen-tts-{model_size}\"\n        is_cached = self._is_model_cached(model_size)\n\n        with model_load_progress(model_name, is_cached):\n            from qwen_tts import Qwen3TTSModel\n\n            model_path = self._get_model_path(model_size)\n            logger.info(\"Loading TTS model %s on %s...\", model_size, self.device)\n\n            if self.device == \"cpu\":\n                self.model = Qwen3TTSModel.from_pretrained(\n                    model_path,\n                    torch_dtype=torch.float32,\n                    low_cpu_mem_usage=False,\n                )\n            else:\n                self.model = Qwen3TTSModel.from_pretrained(\n                    model_path,\n                    device_map=self.device,\n                    torch_dtype=torch.bfloat16,\n                )\n\n        self._current_model_size = model_size\n        self.model_size = model_size\n        logger.info(\"TTS model %s loaded successfully\", model_size)\n\n    def unload_model(self):\n        \"\"\"Unload the model to free memory.\"\"\"\n        if self.model is not None:\n            del self.model\n            self.model = None\n            self._current_model_size = None\n\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n\n            logger.info(\"TTS model unloaded\")\n\n    async def create_voice_prompt(\n        self,\n        audio_path: str,\n        reference_text: str,\n        use_cache: bool = True,\n    ) -> Tuple[dict, bool]:\n        \"\"\"\n        Create voice prompt from reference audio.\n\n        Args:\n            audio_path: Path to reference audio file\n            reference_text: Transcript of reference audio\n            use_cache: Whether to use cached prompt if available\n\n        Returns:\n            Tuple of (voice_prompt_dict, was_cached)\n        \"\"\"\n        await self.load_model_async(None)\n\n        # Check cache if enabled\n        if use_cache:\n            cache_key = get_cache_key(audio_path, reference_text)\n            cached_prompt = get_cached_voice_prompt(cache_key)\n            if cached_prompt is not None:\n                # Cache stores as torch.Tensor but actual prompt is dict\n                # Convert if needed\n                if isinstance(cached_prompt, dict):\n                    # For PyTorch backend, the dict should contain tensors, not file paths\n                    # So we can safely return it\n                    return cached_prompt, True\n                elif isinstance(cached_prompt, torch.Tensor):\n                    # Legacy cache format - convert to dict\n                    # This shouldn't happen in practice, but handle it\n                    return {\"prompt\": cached_prompt}, True\n\n        def _create_prompt_sync():\n            \"\"\"Run synchronous voice prompt creation in thread pool.\"\"\"\n            return self.model.create_voice_clone_prompt(\n                ref_audio=str(audio_path),\n                ref_text=reference_text,\n                x_vector_only_mode=False,\n            )\n\n        # Run blocking operation in thread pool\n        voice_prompt_items = await asyncio.to_thread(_create_prompt_sync)\n\n        # Cache if enabled\n        if use_cache:\n            cache_key = get_cache_key(audio_path, reference_text)\n            cache_voice_prompt(cache_key, voice_prompt_items)\n\n        return voice_prompt_items, False\n\n    async def combine_voice_prompts(\n        self,\n        audio_paths: List[str],\n        reference_texts: List[str],\n    ) -> Tuple[np.ndarray, str]:\n        return await _combine_voice_prompts(audio_paths, reference_texts)\n\n    async def generate(\n        self,\n        text: str,\n        voice_prompt: dict,\n        language: str = \"en\",\n        seed: Optional[int] = None,\n        instruct: Optional[str] = None,\n    ) -> Tuple[np.ndarray, int]:\n        \"\"\"\n        Generate audio from text using voice prompt.\n\n        Args:\n            text: Text to synthesize\n            voice_prompt: Voice prompt dictionary from create_voice_prompt\n            language: Language code (en or zh)\n            seed: Random seed for reproducibility\n            instruct: Natural language instruction for speech delivery control\n\n        Returns:\n            Tuple of (audio_array, sample_rate)\n        \"\"\"\n        # Load model\n        await self.load_model_async(None)\n\n        def _generate_sync():\n            \"\"\"Run synchronous generation in thread pool.\"\"\"\n            # Set seed if provided\n            if seed is not None:\n                torch.manual_seed(seed)\n                if torch.cuda.is_available():\n                    torch.cuda.manual_seed(seed)\n\n            # Generate audio - this is the blocking operation\n            wavs, sample_rate = self.model.generate_voice_clone(\n                text=text,\n                voice_clone_prompt=voice_prompt,\n                language=LANGUAGE_CODE_TO_NAME.get(language, \"auto\"),\n                instruct=instruct,\n            )\n            return wavs[0], sample_rate\n\n        # Run blocking inference in thread pool to avoid blocking event loop\n        audio, sample_rate = await asyncio.to_thread(_generate_sync)\n\n        return audio, sample_rate\n\n\nclass PyTorchSTTBackend:\n    \"\"\"PyTorch-based STT backend using Whisper.\"\"\"\n\n    def __init__(self, model_size: str = \"base\"):\n        self.model = None\n        self.processor = None\n        self.model_size = model_size\n        self.device = self._get_device()\n\n    def _get_device(self) -> str:\n        \"\"\"Get the best available device.\"\"\"\n        return get_torch_device(allow_xpu=True, allow_directml=True)\n\n    def is_loaded(self) -> bool:\n        \"\"\"Check if model is loaded.\"\"\"\n        return self.model is not None\n\n    def _is_model_cached(self, model_size: str) -> bool:\n        hf_repo = WHISPER_HF_REPOS.get(model_size, f\"openai/whisper-{model_size}\")\n        return is_model_cached(hf_repo)\n\n    async def load_model_async(self, model_size: Optional[str] = None):\n        \"\"\"\n        Lazy load the Whisper model.\n\n        Args:\n            model_size: Model size (tiny, base, small, medium, large)\n        \"\"\"\n        if model_size is None:\n            model_size = self.model_size\n\n        if self.model is not None and self.model_size == model_size:\n            return\n\n        await asyncio.to_thread(self._load_model_sync, model_size)\n\n    # Alias for compatibility\n    load_model = load_model_async\n\n    def _load_model_sync(self, model_size: str):\n        \"\"\"Synchronous model loading.\"\"\"\n        progress_model_name = f\"whisper-{model_size}\"\n        is_cached = self._is_model_cached(model_size)\n\n        with model_load_progress(progress_model_name, is_cached):\n            from transformers import WhisperProcessor, WhisperForConditionalGeneration\n\n            model_name = WHISPER_HF_REPOS.get(model_size, f\"openai/whisper-{model_size}\")\n            logger.info(\"Loading Whisper model %s on %s...\", model_size, self.device)\n\n            self.processor = WhisperProcessor.from_pretrained(model_name)\n            self.model = WhisperForConditionalGeneration.from_pretrained(model_name)\n\n        self.model.to(self.device)\n        self.model_size = model_size\n        logger.info(\"Whisper model %s loaded successfully\", model_size)\n\n    def unload_model(self):\n        \"\"\"Unload the model to free memory.\"\"\"\n        if self.model is not None:\n            del self.model\n            del self.processor\n            self.model = None\n            self.processor = None\n\n            if torch.cuda.is_available():\n                torch.cuda.empty_cache()\n\n            logger.info(\"Whisper model unloaded\")\n\n    async def transcribe(\n        self,\n        audio_path: str,\n        language: Optional[str] = None,\n        model_size: Optional[str] = None,\n    ) -> str:\n        \"\"\"\n        Transcribe audio to text.\n\n        Args:\n            audio_path: Path to audio file\n            language: Optional language hint\n            model_size: Optional model size override\n\n        Returns:\n            Transcribed text\n        \"\"\"\n        await self.load_model_async(model_size)\n\n        def _transcribe_sync():\n            \"\"\"Run synchronous transcription in thread pool.\"\"\"\n            # Load audio\n            audio, sr = load_audio(audio_path, sample_rate=16000)\n\n            # Process audio\n            inputs = self.processor(\n                audio,\n                sampling_rate=16000,\n                return_tensors=\"pt\",\n            )\n            inputs = inputs.to(self.device)\n\n            # Generate transcription\n            # If language is provided, force it; otherwise let Whisper auto-detect\n            generate_kwargs = {}\n            if language:\n                forced_decoder_ids = self.processor.get_decoder_prompt_ids(\n                    language=language,\n                    task=\"transcribe\",\n                )\n                generate_kwargs[\"forced_decoder_ids\"] = forced_decoder_ids\n\n            with torch.no_grad():\n                predicted_ids = self.model.generate(\n                    inputs[\"input_features\"],\n                    **generate_kwargs,\n                )\n\n            # Decode\n            transcription = self.processor.batch_decode(\n                predicted_ids,\n                skip_special_tokens=True,\n            )[0]\n\n            return transcription.strip()\n\n        # Run blocking transcription in thread pool\n        return await asyncio.to_thread(_transcribe_sync)\n"
  },
  {
    "path": "backend/build_binary.py",
    "content": "\"\"\"\nPyInstaller build script for creating standalone Python server binary.\n\nUsage:\n    python build_binary.py           # Build default (CPU) server binary\n    python build_binary.py --cuda    # Build CUDA-enabled server binary\n\"\"\"\n\nimport PyInstaller.__main__\nimport argparse\nimport logging\nimport os\nimport platform\nimport sys\nfrom pathlib import Path\n\nlogger = logging.getLogger(__name__)\n\n\ndef is_apple_silicon():\n    \"\"\"Check if running on Apple Silicon.\"\"\"\n    return platform.system() == \"Darwin\" and platform.machine() == \"arm64\"\n\n\ndef build_server(cuda=False):\n    \"\"\"Build Python server as standalone binary.\n\n    Args:\n        cuda: If True, build with CUDA support and name the binary\n              voicebox-server-cuda instead of voicebox-server.\n    \"\"\"\n    backend_dir = Path(__file__).parent\n\n    binary_name = \"voicebox-server-cuda\" if cuda else \"voicebox-server\"\n\n    # PyInstaller arguments\n    # CUDA builds use --onedir so we can split the output into two archives:\n    #   1. Server core (~200-400MB) — versioned with the app\n    #   2. CUDA libs (~2GB) — versioned independently (only redownloaded on\n    #      CUDA toolkit / torch major version changes)\n    # CPU builds remain --onefile for simplicity.\n    pack_mode = \"--onedir\" if cuda else \"--onefile\"\n    args = [\n        \"server.py\",  # Use server.py as entry point instead of main.py\n        pack_mode,\n        \"--name\",\n        binary_name,\n    ]\n\n    # Hide console window on Windows only. On macOS/Linux the sidecar needs\n    # stdout/stderr for Tauri to capture logs.\n    if platform.system() == \"Windows\":\n        args.append(\"--noconsole\")\n\n    # Add local qwen_tts path if specified (for editable installs)\n    qwen_tts_path = os.getenv(\"QWEN_TTS_PATH\")\n    if qwen_tts_path and Path(qwen_tts_path).exists():\n        args.extend([\"--paths\", str(qwen_tts_path)])\n        logger.info(\"Using local qwen_tts source from: %s\", qwen_tts_path)\n\n    # Add common hidden imports\n    args.extend(\n        [\n            \"--hidden-import\",\n            \"backend\",\n            \"--hidden-import\",\n            \"backend.main\",\n            \"--hidden-import\",\n            \"backend.config\",\n            \"--hidden-import\",\n            \"backend.database\",\n            \"--hidden-import\",\n            \"backend.models\",\n            \"--hidden-import\",\n            \"backend.services.profiles\",\n            \"--hidden-import\",\n            \"backend.services.history\",\n            \"--hidden-import\",\n            \"backend.services.tts\",\n            \"--hidden-import\",\n            \"backend.services.transcribe\",\n            \"--hidden-import\",\n            \"backend.utils.platform_detect\",\n            \"--hidden-import\",\n            \"backend.backends\",\n            \"--hidden-import\",\n            \"backend.backends.pytorch_backend\",\n            \"--hidden-import\",\n            \"backend.utils.audio\",\n            \"--hidden-import\",\n            \"backend.utils.cache\",\n            \"--hidden-import\",\n            \"backend.utils.progress\",\n            \"--hidden-import\",\n            \"backend.utils.hf_progress\",\n            \"--hidden-import\",\n            \"backend.services.cuda\",\n            \"--hidden-import\",\n            \"backend.services.effects\",\n            \"--hidden-import\",\n            \"backend.utils.effects\",\n            \"--hidden-import\",\n            \"backend.services.versions\",\n            \"--hidden-import\",\n            \"pedalboard\",\n            \"--hidden-import\",\n            \"chatterbox\",\n            \"--hidden-import\",\n            \"chatterbox.tts_turbo\",\n            \"--hidden-import\",\n            \"chatterbox.mtl_tts\",\n            \"--hidden-import\",\n            \"backend.backends.chatterbox_backend\",\n            \"--hidden-import\",\n            \"backend.backends.chatterbox_turbo_backend\",\n            \"--hidden-import\",\n            \"backend.backends.luxtts_backend\",\n            \"--hidden-import\",\n            \"zipvoice\",\n            \"--hidden-import\",\n            \"zipvoice.luxvoice\",\n            \"--collect-all\",\n            \"zipvoice\",\n            \"--collect-all\",\n            \"linacodec\",\n            \"--hidden-import\",\n            \"torch\",\n            \"--hidden-import\",\n            \"transformers\",\n            \"--hidden-import\",\n            \"fastapi\",\n            \"--hidden-import\",\n            \"uvicorn\",\n            \"--hidden-import\",\n            \"sqlalchemy\",\n            # librosa uses lazy_loader which generates .pyi stub files at\n            # install time and reads them at runtime to discover submodules.\n            # --hidden-import alone doesn't bundle the stubs, causing\n            # \"Cannot load imports from non-existent stub\" at runtime.\n            \"--collect-all\",\n            \"lazy_loader\",\n            \"--collect-all\",\n            \"librosa\",\n            \"--hidden-import\",\n            \"soundfile\",\n            \"--hidden-import\",\n            \"qwen_tts\",\n            \"--hidden-import\",\n            \"qwen_tts.inference\",\n            \"--hidden-import\",\n            \"qwen_tts.inference.qwen3_tts_model\",\n            \"--hidden-import\",\n            \"qwen_tts.inference.qwen3_tts_tokenizer\",\n            \"--hidden-import\",\n            \"qwen_tts.core\",\n            \"--hidden-import\",\n            \"qwen_tts.cli\",\n            \"--copy-metadata\",\n            \"qwen-tts\",\n            \"--copy-metadata\",\n            \"requests\",\n            \"--copy-metadata\",\n            \"transformers\",\n            \"--copy-metadata\",\n            \"huggingface-hub\",\n            \"--copy-metadata\",\n            \"tokenizers\",\n            \"--copy-metadata\",\n            \"safetensors\",\n            \"--copy-metadata\",\n            \"tqdm\",\n            \"--hidden-import\",\n            \"requests\",\n            # qwen_tts uses inspect.getsource() at runtime to locate\n            # modeling_qwen3_tts.py — needs physical .py source files bundled\n            \"--collect-all\",\n            \"qwen_tts\",\n            # Fix for pkg_resources and jaraco namespace packages\n            \"--hidden-import\",\n            \"pkg_resources.extern\",\n            \"--collect-submodules\",\n            \"jaraco\",\n            # inflect uses typeguard @typechecked which calls inspect.getsource()\n            # at import time — needs .py source files, not just .pyc bytecode\n            \"--collect-all\",\n            \"inflect\",\n            # perth ships pretrained watermark model files (hparams.yaml, .pth.tar)\n            # in perth/perth_net/pretrained/ — needed by chatterbox at runtime\n            \"--collect-all\",\n            \"perth\",\n            # piper_phonemize ships espeak-ng-data/ (phoneme tables, language dicts)\n            # needed by LuxTTS for text-to-phoneme conversion\n            \"--collect-all\",\n            \"piper_phonemize\",\n            # HumeAI TADA — speech-language model using Llama + flow matching\n            \"--hidden-import\",\n            \"backend.backends.hume_backend\",\n            \"--hidden-import\",\n            \"tada\",\n            \"--hidden-import\",\n            \"tada.modules\",\n            \"--hidden-import\",\n            \"tada.modules.tada\",\n            \"--hidden-import\",\n            \"tada.modules.encoder\",\n            \"--hidden-import\",\n            \"tada.modules.decoder\",\n            \"--hidden-import\",\n            \"tada.modules.aligner\",\n            \"--hidden-import\",\n            \"tada.modules.acoustic_spkr_verf\",\n            \"--hidden-import\",\n            \"tada.nn\",\n            \"--hidden-import\",\n            \"tada.nn.vibevoice\",\n            \"--hidden-import\",\n            \"tada.utils\",\n            \"--hidden-import\",\n            \"tada.utils.gray_code\",\n            \"--hidden-import\",\n            \"tada.utils.text\",\n            # DAC shim — provides dac.nn.layers.Snake1d without the real\n            # descript-audio-codec package (which pulls onnx/tensorboard via\n            # descript-audiotools). The shim is in backend/utils/dac_shim.py.\n            \"--hidden-import\",\n            \"backend.utils.dac_shim\",\n            \"--hidden-import\",\n            \"torchaudio\",\n            \"--collect-submodules\",\n            \"tada\",\n            # Kokoro 82M — lightweight TTS engine using misaki G2P\n            \"--hidden-import\",\n            \"backend.backends.kokoro_backend\",\n            \"--hidden-import\",\n            \"kokoro\",\n            \"--hidden-import\",\n            \"kokoro.pipeline\",\n            \"--hidden-import\",\n            \"kokoro.model\",\n            \"--hidden-import\",\n            \"kokoro.istftnet\",\n            \"--hidden-import\",\n            \"kokoro.modules\",\n            \"--hidden-import\",\n            \"kokoro.custom_stft\",\n            # misaki ships G2P data files (dictionaries, phoneme tables)\n            # that must be bundled for espeak/en/ja/zh G2P to work\n            \"--collect-all\",\n            \"misaki\",\n            # language_tags ships JSON data files (index.json etc.) loaded at\n            # runtime via: misaki → phonemizer → segments → csvw → language_tags\n            \"--collect-all\",\n            \"language_tags\",\n            # espeakng_loader ships the entire espeak-ng-data directory (369 files)\n            # loaded at import time by misaki.espeak via get_data_path()\n            \"--collect-all\",\n            \"espeakng_loader\",\n            # spacy en_core_web_sm model — misaki.en tries to spacy.cli.download()\n            # at runtime if not found, which calls pip as a subprocess and crashes\n            # the frozen binary. Bundle the model so spacy.util.is_package() passes.\n            \"--collect-all\",\n            \"en_core_web_sm\",\n            \"--copy-metadata\",\n            \"en_core_web_sm\",\n            \"--hidden-import\",\n            \"en_core_web_sm\",\n            \"--hidden-import\",\n            \"loguru\",\n        ]\n    )\n\n    # Add CUDA-specific hidden imports\n    if cuda:\n        logger.info(\"Building with CUDA support\")\n        args.extend(\n            [\n                \"--hidden-import\",\n                \"torch.cuda\",\n                \"--hidden-import\",\n                \"torch.backends.cudnn\",\n            ]\n        )\n    else:\n        # Exclude NVIDIA CUDA packages from CPU-only builds to keep binary small.\n        # When building from a venv with CUDA torch installed, PyInstaller would\n        # bundle ~3GB of NVIDIA shared libraries. We exclude both the Python\n        # modules and the binary DLLs.\n        nvidia_packages = [\n            \"nvidia\",\n            \"nvidia.cublas\",\n            \"nvidia.cuda_cupti\",\n            \"nvidia.cuda_nvrtc\",\n            \"nvidia.cuda_runtime\",\n            \"nvidia.cudnn\",\n            \"nvidia.cufft\",\n            \"nvidia.curand\",\n            \"nvidia.cusolver\",\n            \"nvidia.cusparse\",\n            \"nvidia.nccl\",\n            \"nvidia.nvjitlink\",\n            \"nvidia.nvtx\",\n        ]\n        for pkg in nvidia_packages:\n            args.extend([\"--exclude-module\", pkg])\n\n    # Add MLX-specific imports if building on Apple Silicon (never for CUDA builds)\n    if is_apple_silicon() and not cuda:\n        logger.info(\"Building for Apple Silicon - including MLX dependencies\")\n        args.extend(\n            [\n                \"--hidden-import\",\n                \"backend.backends.mlx_backend\",\n                \"--hidden-import\",\n                \"mlx\",\n                \"--hidden-import\",\n                \"mlx.core\",\n                \"--hidden-import\",\n                \"mlx.nn\",\n                \"--hidden-import\",\n                \"mlx_audio\",\n                \"--hidden-import\",\n                \"mlx_audio.tts\",\n                \"--hidden-import\",\n                \"mlx_audio.stt\",\n                \"--collect-submodules\",\n                \"mlx\",\n                \"--collect-submodules\",\n                \"mlx_audio\",\n                # Use --collect-all so PyInstaller bundles both data files AND\n                # native shared libraries (.dylib, .metallib) for MLX.\n                # Previously only --collect-data was used, which caused MLX to\n                # raise OSError at runtime inside the bundled binary because\n                # the Metal shader libraries were missing.\n                \"--collect-all\",\n                \"mlx\",\n                \"--collect-all\",\n                \"mlx_audio\",\n            ]\n        )\n    elif not cuda:\n        logger.info(\"Building for non-Apple Silicon platform - PyTorch only\")\n\n    dist_dir = str(backend_dir / \"dist\")\n    build_dir = str(backend_dir / \"build\")\n\n    args.extend(\n        [\n            \"--distpath\",\n            dist_dir,\n            \"--workpath\",\n            build_dir,\n            \"--noconfirm\",\n            \"--clean\",\n        ]\n    )\n\n    # Change to backend directory\n    os.chdir(backend_dir)\n\n    # For CPU builds on Windows, ensure we're using CPU-only torch.\n    # If CUDA torch is installed (local dev), swap to CPU torch before building,\n    # then restore CUDA torch after. This prevents PyInstaller from bundling\n    # ~3GB of CUDA DLLs into the CPU binary.\n    restore_cuda = False\n    if not cuda and platform.system() == \"Windows\":\n        import subprocess\n\n        result = subprocess.run(\n            [sys.executable, \"-c\", \"import torch; print(torch.version.cuda or '')\"], capture_output=True, text=True\n        )\n        has_cuda_torch = bool(result.stdout.strip())\n        if has_cuda_torch:\n            logger.info(\"CUDA torch detected — installing CPU torch for CPU build...\")\n            subprocess.run(\n                [\n                    sys.executable,\n                    \"-m\",\n                    \"pip\",\n                    \"install\",\n                    \"torch\",\n                    \"torchvision\",\n                    \"torchaudio\",\n                    \"--index-url\",\n                    \"https://download.pytorch.org/whl/cpu\",\n                    \"--force-reinstall\",\n                    \"-q\",\n                ],\n                check=True,\n            )\n            restore_cuda = True\n\n    # Run PyInstaller\n    try:\n        PyInstaller.__main__.run(args)\n    finally:\n        # Restore CUDA torch if we swapped it out (even on build failure)\n        if restore_cuda:\n            logger.info(\"Restoring CUDA torch...\")\n            import subprocess\n\n            subprocess.run(\n                [\n                    sys.executable,\n                    \"-m\",\n                    \"pip\",\n                    \"install\",\n                    \"torch\",\n                    \"torchvision\",\n                    \"torchaudio\",\n                    \"--index-url\",\n                    \"https://download.pytorch.org/whl/cu128\",\n                    \"--force-reinstall\",\n                    \"-q\",\n                ],\n                check=True,\n            )\n\n    logger.info(\"Binary built in %s\", backend_dir / \"dist\" / binary_name)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Build voicebox-server binary\")\n    parser.add_argument(\n        \"--cuda\",\n        action=\"store_true\",\n        help=\"Build CUDA-enabled binary (voicebox-server-cuda)\",\n    )\n    cli_args = parser.parse_args()\n    build_server(cuda=cli_args.cuda)\n"
  },
  {
    "path": "backend/config.py",
    "content": "\"\"\"\nConfiguration module for voicebox backend.\n\nHandles data directory configuration for production bundling.\n\"\"\"\n\nimport logging\nimport os\nfrom pathlib import Path\n\nlogger = logging.getLogger(__name__)\n\n# Allow users to override the HuggingFace model download directory.\n# Set VOICEBOX_MODELS_DIR to an absolute path before starting the server.\n# This sets HF_HUB_CACHE so all huggingface_hub downloads go to that path.\n_custom_models_dir = os.environ.get(\"VOICEBOX_MODELS_DIR\")\nif _custom_models_dir:\n    os.environ[\"HF_HUB_CACHE\"] = _custom_models_dir\n    logger.info(\"Model download path set to: %s\", _custom_models_dir)\n\n# Default data directory (used in development)\n_data_dir = Path(\"data\").resolve()\n\n\ndef set_data_dir(path: str | Path):\n    \"\"\"\n    Set the data directory path.\n\n    Args:\n        path: Path to the data directory\n    \"\"\"\n    global _data_dir\n    _data_dir = Path(path).resolve()\n    _data_dir.mkdir(parents=True, exist_ok=True)\n    logger.info(\"Data directory set to: %s\", _data_dir)\n\n\ndef get_data_dir() -> Path:\n    \"\"\"\n    Get the data directory path.\n\n    Returns:\n        Path to the data directory\n    \"\"\"\n    return _data_dir\n\n\ndef get_db_path() -> Path:\n    \"\"\"Get database file path.\"\"\"\n    return _data_dir / \"voicebox.db\"\n\n\ndef get_profiles_dir() -> Path:\n    \"\"\"Get profiles directory path.\"\"\"\n    path = _data_dir / \"profiles\"\n    path.mkdir(parents=True, exist_ok=True)\n    return path\n\n\ndef get_generations_dir() -> Path:\n    \"\"\"Get generations directory path.\"\"\"\n    path = _data_dir / \"generations\"\n    path.mkdir(parents=True, exist_ok=True)\n    return path\n\n\ndef get_cache_dir() -> Path:\n    \"\"\"Get cache directory path.\"\"\"\n    path = _data_dir / \"cache\"\n    path.mkdir(parents=True, exist_ok=True)\n    return path\n\n\ndef get_models_dir() -> Path:\n    \"\"\"Get models directory path.\"\"\"\n    path = _data_dir / \"models\"\n    path.mkdir(parents=True, exist_ok=True)\n    return path\n"
  },
  {
    "path": "backend/database/__init__.py",
    "content": "\"\"\"Database package — ORM models, session management, and migrations.\n\nRe-exports all public symbols so that ``from .database import get_db``\nand ``from .database import Generation as DBGeneration`` continue to work\nwithout changing any importers.\n\"\"\"\n\nfrom .models import (\n    Base,\n    AudioChannel,\n    ChannelDeviceMapping,\n    EffectPreset,\n    Generation,\n    GenerationVersion,\n    ProfileChannelMapping,\n    ProfileSample,\n    Project,\n    Story,\n    StoryItem,\n    VoiceProfile,\n)\nfrom .session import engine, SessionLocal, _db_path, init_db, get_db\n\n__all__ = [\n    # Models\n    \"Base\",\n    \"AudioChannel\",\n    \"ChannelDeviceMapping\",\n    \"EffectPreset\",\n    \"Generation\",\n    \"GenerationVersion\",\n    \"ProfileChannelMapping\",\n    \"ProfileSample\",\n    \"Project\",\n    \"Story\",\n    \"StoryItem\",\n    \"VoiceProfile\",\n    # Session\n    \"engine\",\n    \"SessionLocal\",\n    \"_db_path\",\n    \"init_db\",\n    \"get_db\",\n]\n"
  },
  {
    "path": "backend/database/migrations.py",
    "content": "\"\"\"Column-level migrations for the voicebox SQLite database.\n\nWhy not Alembic?  voicebox is a single-user desktop app shipping as a\nPyInstaller binary.  Every user has exactly one SQLite file.  Alembic's\nstrengths -- migration tracking across environments, rollback, team\ncoordination -- don't apply here and would add bundling complexity\n(alembic.ini, env.py, versions/ directory all need to survive\nPyInstaller).  The column-existence checks below are idempotent, run in\n<50 ms on startup, and have worked reliably across 12 schema changes.\nIf the project ever moves to a server-based deployment or Postgres, this\ndecision should be revisited.\n\nAdding a new migration:\n    1. Append a new ``_migrate_*`` helper at the bottom of this file.\n    2. Call it from ``run_migrations()`` in the appropriate spot.\n    3. The helper should check column/table existence before acting\n       (idempotent) and print a short message when it does real work.\n\"\"\"\n\nimport logging\n\nfrom sqlalchemy import inspect, text\n\nlogger = logging.getLogger(__name__)\n\n\ndef run_migrations(engine) -> None:\n    \"\"\"Run all schema migrations.  Safe to call on every startup.\"\"\"\n    inspector = inspect(engine)\n    tables = set(inspector.get_table_names())\n\n    _migrate_story_items(engine, inspector, tables)\n    _migrate_profiles(engine, inspector, tables)\n    _migrate_generations(engine, inspector, tables)\n    _migrate_effect_presets(engine, inspector, tables)\n    _migrate_generation_versions(engine, inspector, tables)\n    _resolve_relative_paths(engine, tables)\n\n\n# -- helpers ---------------------------------------------------------------\n\ndef _get_columns(inspector, table: str) -> set[str]:\n    return {col[\"name\"] for col in inspector.get_columns(table)}\n\n\ndef _add_column(engine, table: str, column_sql: str, label: str) -> None:\n    \"\"\"Add a column if it doesn't already exist.\"\"\"\n    with engine.connect() as conn:\n        conn.execute(text(f\"ALTER TABLE {table} ADD COLUMN {column_sql}\"))\n        conn.commit()\n    logger.info(\"Added %s column to %s\", label, table)\n\n\n# -- per-table migrations --------------------------------------------------\n\ndef _migrate_story_items(engine, inspector, tables: set[str]) -> None:\n    if \"story_items\" not in tables:\n        return\n\n    columns = _get_columns(inspector, \"story_items\")\n\n    # Replace position-based ordering with absolute timecodes\n    if \"position\" in columns:\n        logger.info(\"Migrating story_items: removing position column, using start_time_ms\")\n        with engine.connect() as conn:\n            if \"start_time_ms\" not in columns:\n                conn.execute(text(\n                    \"ALTER TABLE story_items ADD COLUMN start_time_ms INTEGER DEFAULT 0\"\n                ))\n                result = conn.execute(text(\"\"\"\n                    SELECT si.id, si.story_id, si.position, g.duration\n                    FROM story_items si\n                    JOIN generations g ON si.generation_id = g.id\n                    ORDER BY si.story_id, si.position\n                \"\"\"))\n                current_story_id = None\n                current_time_ms = 0\n                for item_id, story_id, _position, duration in result.fetchall():\n                    if story_id != current_story_id:\n                        current_story_id = story_id\n                        current_time_ms = 0\n                    conn.execute(\n                        text(\"UPDATE story_items SET start_time_ms = :time WHERE id = :id\"),\n                        {\"time\": current_time_ms, \"id\": item_id},\n                    )\n                    current_time_ms += int((duration or 0) * 1000) + 200\n                conn.commit()\n\n            # Recreate table without the position column (SQLite lacks DROP COLUMN)\n            conn.execute(text(\"\"\"\n                CREATE TABLE story_items_new (\n                    id VARCHAR PRIMARY KEY,\n                    story_id VARCHAR NOT NULL,\n                    generation_id VARCHAR NOT NULL,\n                    start_time_ms INTEGER NOT NULL DEFAULT 0,\n                    track INTEGER NOT NULL DEFAULT 0,\n                    trim_start_ms INTEGER NOT NULL DEFAULT 0,\n                    trim_end_ms INTEGER NOT NULL DEFAULT 0,\n                    version_id VARCHAR,\n                    created_at DATETIME,\n                    FOREIGN KEY (story_id) REFERENCES stories(id),\n                    FOREIGN KEY (generation_id) REFERENCES generations(id)\n                )\n            \"\"\"))\n            conn.execute(text(\"\"\"\n                INSERT INTO story_items_new (id, story_id, generation_id, start_time_ms, track, trim_start_ms, trim_end_ms, version_id, created_at)\n                SELECT id, story_id, generation_id, start_time_ms,\n                    COALESCE(track, 0), COALESCE(trim_start_ms, 0), COALESCE(trim_end_ms, 0), version_id, created_at\n                FROM story_items\n            \"\"\"))\n            conn.execute(text(\"DROP TABLE story_items\"))\n            conn.execute(text(\"ALTER TABLE story_items_new RENAME TO story_items\"))\n            conn.commit()\n\n        # Re-read after table recreation\n        columns = _get_columns(inspector, \"story_items\")\n\n    if \"track\" not in columns:\n        _add_column(engine, \"story_items\", \"track INTEGER NOT NULL DEFAULT 0\", \"track\")\n    # Re-read so subsequent checks see new columns\n    columns = _get_columns(inspector, \"story_items\")\n    if \"trim_start_ms\" not in columns:\n        _add_column(engine, \"story_items\", \"trim_start_ms INTEGER NOT NULL DEFAULT 0\", \"trim_start_ms\")\n    if \"trim_end_ms\" not in columns:\n        _add_column(engine, \"story_items\", \"trim_end_ms INTEGER NOT NULL DEFAULT 0\", \"trim_end_ms\")\n    if \"version_id\" not in columns:\n        _add_column(engine, \"story_items\", \"version_id VARCHAR\", \"version_id\")\n\n\ndef _migrate_profiles(engine, inspector, tables: set[str]) -> None:\n    if \"profiles\" not in tables:\n        return\n    columns = _get_columns(inspector, \"profiles\")\n    if \"avatar_path\" not in columns:\n        _add_column(engine, \"profiles\", \"avatar_path VARCHAR\", \"avatar_path\")\n    if \"effects_chain\" not in columns:\n        _add_column(engine, \"profiles\", \"effects_chain TEXT\", \"effects_chain\")\n    # Voice type system — v0.3.x\n    if \"voice_type\" not in columns:\n        _add_column(engine, \"profiles\", \"voice_type VARCHAR DEFAULT 'cloned'\", \"voice_type\")\n    if \"preset_engine\" not in columns:\n        _add_column(engine, \"profiles\", \"preset_engine VARCHAR\", \"preset_engine\")\n    if \"preset_voice_id\" not in columns:\n        _add_column(engine, \"profiles\", \"preset_voice_id VARCHAR\", \"preset_voice_id\")\n    if \"design_prompt\" not in columns:\n        _add_column(engine, \"profiles\", \"design_prompt TEXT\", \"design_prompt\")\n    if \"default_engine\" not in columns:\n        _add_column(engine, \"profiles\", \"default_engine VARCHAR\", \"default_engine\")\n\n\ndef _migrate_generations(engine, inspector, tables: set[str]) -> None:\n    if \"generations\" not in tables:\n        return\n    columns = _get_columns(inspector, \"generations\")\n    if \"status\" not in columns:\n        _add_column(engine, \"generations\", \"status VARCHAR DEFAULT 'completed'\", \"status\")\n    if \"error\" not in columns:\n        _add_column(engine, \"generations\", \"error TEXT\", \"error\")\n    if \"engine\" not in columns:\n        _add_column(engine, \"generations\", \"engine VARCHAR DEFAULT 'qwen'\", \"engine\")\n    # Re-read after engine column (variable name shadows outer scope in old code)\n    columns = _get_columns(inspector, \"generations\")\n    if \"model_size\" not in columns:\n        _add_column(engine, \"generations\", \"model_size VARCHAR\", \"model_size\")\n    if \"is_favorited\" not in columns:\n        _add_column(engine, \"generations\", \"is_favorited BOOLEAN DEFAULT 0\", \"is_favorited\")\n\n\ndef _migrate_effect_presets(engine, inspector, tables: set[str]) -> None:\n    if \"effect_presets\" not in tables:\n        return\n    columns = _get_columns(inspector, \"effect_presets\")\n    if \"sort_order\" not in columns:\n        _add_column(engine, \"effect_presets\", \"sort_order INTEGER DEFAULT 100\", \"sort_order\")\n\n\ndef _migrate_generation_versions(engine, inspector, tables: set[str]) -> None:\n    if \"generation_versions\" not in tables:\n        return\n    columns = _get_columns(inspector, \"generation_versions\")\n    if \"source_version_id\" not in columns:\n        _add_column(engine, \"generation_versions\", \"source_version_id VARCHAR\", \"source_version_id\")\n\n\ndef _resolve_relative_paths(engine, tables: set[str]) -> None:\n    \"\"\"Resolve any relative file paths in the database to absolute paths.\n\n    Earlier versions stored paths relative to CWD (e.g. \"data/generations/abc.wav\").\n    These break when the production binary's CWD differs from the data directory.\n    This migration converts them to absolute paths using the configured data dir.\n    Idempotent: absolute paths are left untouched.\n\n    Strategy: paths like \"data/generations/abc.wav\" are rebased onto the\n    configured data directory. If the path starts with \"data/\", strip that\n    prefix and prepend get_data_dir(). Otherwise, join the relative path\n    directly under get_data_dir().\n    \"\"\"\n    from pathlib import Path\n\n    from ..config import get_data_dir\n\n    data_dir = get_data_dir()\n\n    path_columns = [\n        (\"generations\", \"audio_path\"),\n        (\"generation_versions\", \"audio_path\"),\n        (\"profile_samples\", \"audio_path\"),\n        (\"profiles\", \"avatar_path\"),\n    ]\n\n    total_fixed = 0\n    with engine.connect() as conn:\n        for table, column in path_columns:\n            if table not in tables:\n                continue\n            rows = conn.execute(\n                text(f\"SELECT id, {column} FROM {table} WHERE {column} IS NOT NULL\")\n            ).fetchall()\n            for row_id, path_val in rows:\n                if not path_val:\n                    continue\n                p = Path(path_val)\n                if p.is_absolute():\n                    continue\n\n                # Try rebasing: \"data/generations/abc.wav\" → data_dir / \"generations/abc.wav\"\n                parts = p.parts\n                if parts and parts[0] == \"data\":\n                    rebased = data_dir / Path(*parts[1:])\n                else:\n                    rebased = data_dir / p\n\n                resolved = rebased.resolve()\n\n                if resolved.exists():\n                    conn.execute(\n                        text(f\"UPDATE {table} SET {column} = :path WHERE id = :id\"),\n                        {\"path\": str(resolved), \"id\": row_id},\n                    )\n                    total_fixed += 1\n        if total_fixed > 0:\n            conn.commit()\n            logger.info(\"Resolved %d relative file paths to absolute\", total_fixed)\n"
  },
  {
    "path": "backend/database/models.py",
    "content": "\"\"\"ORM model definitions for the voicebox SQLite database.\"\"\"\n\nfrom datetime import datetime\nimport uuid\n\nfrom sqlalchemy import Column, String, Integer, Float, DateTime, Text, ForeignKey, Boolean\nfrom sqlalchemy.ext.declarative import declarative_base\n\nBase = declarative_base()\n\n\nclass VoiceProfile(Base):\n    \"\"\"Voice profile.\n\n    voice_type discriminates three flavours:\n      - \"cloned\"   — traditional reference-audio profiles (all cloning engines)\n      - \"preset\"   — engine-specific pre-built voice (e.g. Kokoro voices)\n      - \"designed\"  — text-described voice (e.g. Qwen CustomVoice, future)\n    \"\"\"\n\n    __tablename__ = \"profiles\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    name = Column(String, unique=True, nullable=False)\n    description = Column(Text)\n    language = Column(String, default=\"en\")\n    avatar_path = Column(String, nullable=True)\n    effects_chain = Column(Text, nullable=True)\n\n    # Voice type system — added v0.3.x\n    voice_type = Column(String, default=\"cloned\")  # \"cloned\" | \"preset\" | \"designed\"\n    preset_engine = Column(String, nullable=True)   # e.g. \"kokoro\" — only for preset\n    preset_voice_id = Column(String, nullable=True)  # e.g. \"am_adam\" — only for preset\n    design_prompt = Column(Text, nullable=True)      # text description — only for designed\n    default_engine = Column(String, nullable=True)   # auto-selected engine, locked for preset\n\n    created_at = Column(DateTime, default=datetime.utcnow)\n    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)\n\n\nclass ProfileSample(Base):\n    \"\"\"Audio sample attached to a voice profile.\"\"\"\n\n    __tablename__ = \"profile_samples\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    profile_id = Column(String, ForeignKey(\"profiles.id\"), nullable=False)\n    audio_path = Column(String, nullable=False)\n    reference_text = Column(Text, nullable=False)\n\n\nclass Generation(Base):\n    \"\"\"A single TTS generation.\"\"\"\n\n    __tablename__ = \"generations\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    profile_id = Column(String, ForeignKey(\"profiles.id\"), nullable=False)\n    text = Column(Text, nullable=False)\n    language = Column(String, default=\"en\")\n    audio_path = Column(String, nullable=True)\n    duration = Column(Float, nullable=True)\n    seed = Column(Integer)\n    instruct = Column(Text)\n    engine = Column(String, default=\"qwen\")\n    model_size = Column(String, nullable=True)\n    status = Column(String, default=\"completed\")\n    error = Column(Text, nullable=True)\n    is_favorited = Column(Boolean, default=False)\n    created_at = Column(DateTime, default=datetime.utcnow)\n\n\nclass Story(Base):\n    \"\"\"A story that sequences multiple generations.\"\"\"\n\n    __tablename__ = \"stories\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    name = Column(String, nullable=False)\n    description = Column(Text)\n    created_at = Column(DateTime, default=datetime.utcnow)\n    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)\n\n\nclass StoryItem(Base):\n    \"\"\"Links a generation to a story at a specific timecode.\"\"\"\n\n    __tablename__ = \"story_items\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    story_id = Column(String, ForeignKey(\"stories.id\"), nullable=False)\n    generation_id = Column(String, ForeignKey(\"generations.id\"), nullable=False)\n    version_id = Column(String, ForeignKey(\"generation_versions.id\"), nullable=True)\n    start_time_ms = Column(Integer, nullable=False, default=0)\n    track = Column(Integer, nullable=False, default=0)\n    trim_start_ms = Column(Integer, nullable=False, default=0)\n    trim_end_ms = Column(Integer, nullable=False, default=0)\n    created_at = Column(DateTime, default=datetime.utcnow)\n\n\nclass Project(Base):\n    \"\"\"Audio studio project (JSON blob).\"\"\"\n\n    __tablename__ = \"projects\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    name = Column(String, nullable=False)\n    data = Column(Text)\n    created_at = Column(DateTime, default=datetime.utcnow)\n    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)\n\n\nclass GenerationVersion(Base):\n    \"\"\"A version of a generation's audio (original, processed, alternate takes).\"\"\"\n\n    __tablename__ = \"generation_versions\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    generation_id = Column(String, ForeignKey(\"generations.id\"), nullable=False)\n    label = Column(String, nullable=False)\n    audio_path = Column(String, nullable=False)\n    effects_chain = Column(Text, nullable=True)\n    source_version_id = Column(String, ForeignKey(\"generation_versions.id\"), nullable=True)\n    is_default = Column(Boolean, default=False)\n    created_at = Column(DateTime, default=datetime.utcnow)\n\n\nclass EffectPreset(Base):\n    \"\"\"Saved effect chain preset.\"\"\"\n\n    __tablename__ = \"effect_presets\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    name = Column(String, unique=True, nullable=False)\n    description = Column(Text, nullable=True)\n    effects_chain = Column(Text, nullable=False)\n    is_builtin = Column(Boolean, default=False)\n    sort_order = Column(Integer, default=100)\n    created_at = Column(DateTime, default=datetime.utcnow)\n\n\nclass AudioChannel(Base):\n    \"\"\"Audio output channel (bus).\"\"\"\n\n    __tablename__ = \"audio_channels\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    name = Column(String, nullable=False)\n    is_default = Column(Boolean, default=False)\n    created_at = Column(DateTime, default=datetime.utcnow)\n\n\nclass ChannelDeviceMapping(Base):\n    \"\"\"Mapping between a channel and an OS audio device.\"\"\"\n\n    __tablename__ = \"channel_device_mappings\"\n\n    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))\n    channel_id = Column(String, ForeignKey(\"audio_channels.id\"), nullable=False)\n    device_id = Column(String, nullable=False)\n\n\nclass ProfileChannelMapping(Base):\n    \"\"\"Many-to-many mapping between voice profiles and audio channels.\"\"\"\n\n    __tablename__ = \"profile_channel_mappings\"\n\n    profile_id = Column(String, ForeignKey(\"profiles.id\"), primary_key=True)\n    channel_id = Column(String, ForeignKey(\"audio_channels.id\"), primary_key=True)\n"
  },
  {
    "path": "backend/database/seed.py",
    "content": "\"\"\"Post-migration data seeding and backfills.\"\"\"\n\nimport json\nimport logging\nimport uuid\nfrom pathlib import Path\n\nlogger = logging.getLogger(__name__)\n\n\ndef backfill_generation_versions(SessionLocal, Generation, GenerationVersion) -> None:\n    \"\"\"Create 'clean' version entries for generations that predate the versions feature.\"\"\"\n    db = SessionLocal()\n    try:\n        existing_version_gen_ids = {\n            row[0] for row in db.query(GenerationVersion.generation_id).all()\n        }\n        generations = db.query(Generation).filter(\n            Generation.status == \"completed\",\n            Generation.audio_path.isnot(None),\n            Generation.audio_path != \"\",\n        ).all()\n\n        count = 0\n        for gen in generations:\n            if gen.id in existing_version_gen_ids:\n                continue\n            if not Path(gen.audio_path).exists():\n                continue\n            version = GenerationVersion(\n                id=str(uuid.uuid4()),\n                generation_id=gen.id,\n                label=\"clean\",\n                audio_path=gen.audio_path,\n                effects_chain=None,\n                is_default=True,\n            )\n            db.add(version)\n            count += 1\n\n        if count > 0:\n            db.commit()\n            logger.info(\"Backfilled %d generation version entries\", count)\n    finally:\n        db.close()\n\n\ndef seed_builtin_presets(SessionLocal, EffectPreset) -> None:\n    \"\"\"Ensure built-in effect presets exist in the database.\"\"\"\n    from ..utils.effects import BUILTIN_PRESETS\n\n    db = SessionLocal()\n    try:\n        for idx, (_key, preset_data) in enumerate(BUILTIN_PRESETS.items()):\n            sort_order = preset_data.get(\"sort_order\", idx)\n            existing = db.query(EffectPreset).filter_by(name=preset_data[\"name\"]).first()\n            if not existing:\n                preset = EffectPreset(\n                    id=str(uuid.uuid4()),\n                    name=preset_data[\"name\"],\n                    description=preset_data.get(\"description\"),\n                    effects_chain=json.dumps(preset_data[\"effects_chain\"]),\n                    is_builtin=True,\n                    sort_order=sort_order,\n                )\n                db.add(preset)\n            elif existing.sort_order != sort_order:\n                existing.sort_order = sort_order\n        db.commit()\n    finally:\n        db.close()\n"
  },
  {
    "path": "backend/database/session.py",
    "content": "\"\"\"Engine creation, initialization, and session management.\"\"\"\n\nimport logging\nimport uuid\n\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\nfrom .. import config\nfrom .models import (\n    Base,\n    AudioChannel,\n    EffectPreset,\n    Generation,\n    GenerationVersion,\n    ProfileChannelMapping,\n    VoiceProfile,\n)\nfrom .migrations import run_migrations\nfrom .seed import backfill_generation_versions, seed_builtin_presets\n\nlogger = logging.getLogger(__name__)\n\n# Initialized by init_db()\nengine = None\nSessionLocal = None\n_db_path = None\n\n\ndef init_db() -> None:\n    \"\"\"Initialize the database engine, run migrations, create tables, and seed data.\"\"\"\n    global engine, SessionLocal, _db_path\n\n    _db_path = config.get_db_path()\n    _db_path.parent.mkdir(parents=True, exist_ok=True)\n\n    engine = create_engine(\n        f\"sqlite:///{_db_path}\",\n        connect_args={\"check_same_thread\": False},\n    )\n\n    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n    run_migrations(engine)\n    Base.metadata.create_all(bind=engine)\n\n    # Create default audio channel if it doesn't exist\n    db = SessionLocal()\n    try:\n        default_channel = db.query(AudioChannel).filter(AudioChannel.is_default == True).first()\n        if not default_channel:\n            default_channel = AudioChannel(\n                id=str(uuid.uuid4()),\n                name=\"Default\",\n                is_default=True,\n            )\n            db.add(default_channel)\n\n            for profile in db.query(VoiceProfile).all():\n                db.add(ProfileChannelMapping(\n                    profile_id=profile.id,\n                    channel_id=default_channel.id,\n                ))\n            db.commit()\n    finally:\n        db.close()\n\n    backfill_generation_versions(SessionLocal, Generation, GenerationVersion)\n    seed_builtin_presets(SessionLocal, EffectPreset)\n\n\ndef get_db():\n    \"\"\"Yield a database session (FastAPI dependency).\"\"\"\n    db = SessionLocal()\n    try:\n        yield db\n    finally:\n        db.close()\n"
  },
  {
    "path": "backend/main.py",
    "content": "\"\"\"Entry point for the voicebox backend.\n\nImports the configured FastAPI app and provides a ``python -m backend.main``\nentry point for development.\n\"\"\"\n\nimport argparse\nimport uvicorn\n\nfrom .app import app  # noqa: F401 -- re-export for uvicorn \"backend.main:app\"\nfrom . import config, database\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"voicebox backend server\")\n    parser.add_argument(\n        \"--host\",\n        type=str,\n        default=\"127.0.0.1\",\n        help=\"Host to bind to (use 0.0.0.0 for remote access)\",\n    )\n    parser.add_argument(\n        \"--port\",\n        type=int,\n        default=8000,\n        help=\"Port to bind to\",\n    )\n    parser.add_argument(\n        \"--data-dir\",\n        type=str,\n        default=None,\n        help=\"Data directory for database, profiles, and generated audio\",\n    )\n    args = parser.parse_args()\n\n    if args.data_dir:\n        config.set_data_dir(args.data_dir)\n\n    database.init_db()\n\n    uvicorn.run(\n        \"backend.main:app\",\n        host=args.host,\n        port=args.port,\n        reload=False,\n    )\n"
  },
  {
    "path": "backend/models.py",
    "content": "\"\"\"\nPydantic models for request/response validation.\n\"\"\"\n\nfrom pydantic import BaseModel, Field\nfrom typing import Optional, List\nfrom datetime import datetime\n\n\nclass VoiceProfileCreate(BaseModel):\n    \"\"\"Request model for creating a voice profile.\"\"\"\n\n    name: str = Field(..., min_length=1, max_length=100)\n    description: Optional[str] = Field(None, max_length=500)\n    language: str = Field(\n        default=\"en\", pattern=\"^(zh|en|ja|ko|de|fr|ru|pt|es|it|he|ar|da|el|fi|hi|ms|nl|no|pl|sv|sw|tr)$\"\n    )\n    voice_type: Optional[str] = Field(default=\"cloned\", pattern=\"^(cloned|preset|designed)$\")\n    preset_engine: Optional[str] = Field(None, max_length=50)\n    preset_voice_id: Optional[str] = Field(None, max_length=100)\n    design_prompt: Optional[str] = Field(None, max_length=2000)\n    default_engine: Optional[str] = Field(None, max_length=50)\n\n\nclass VoiceProfileResponse(BaseModel):\n    \"\"\"Response model for voice profile.\"\"\"\n\n    id: str\n    name: str\n    description: Optional[str]\n    language: str\n    avatar_path: Optional[str] = None\n    effects_chain: Optional[List[\"EffectConfig\"]] = None\n    voice_type: str = \"cloned\"\n    preset_engine: Optional[str] = None\n    preset_voice_id: Optional[str] = None\n    design_prompt: Optional[str] = None\n    default_engine: Optional[str] = None\n    generation_count: int = 0\n    sample_count: int = 0\n    created_at: datetime\n    updated_at: datetime\n\n    class Config:\n        from_attributes = True\n\n\nclass ProfileSampleCreate(BaseModel):\n    \"\"\"Request model for adding a sample to a profile.\"\"\"\n\n    reference_text: str = Field(..., min_length=1, max_length=1000)\n\n\nclass ProfileSampleUpdate(BaseModel):\n    \"\"\"Request model for updating a profile sample.\"\"\"\n\n    reference_text: str = Field(..., min_length=1, max_length=1000)\n\n\nclass ProfileSampleResponse(BaseModel):\n    \"\"\"Response model for profile sample.\"\"\"\n\n    id: str\n    profile_id: str\n    audio_path: str\n    reference_text: str\n\n    class Config:\n        from_attributes = True\n\n\nclass GenerationRequest(BaseModel):\n    \"\"\"Request model for voice generation.\"\"\"\n\n    profile_id: str\n    text: str = Field(..., min_length=1, max_length=50000)\n    language: str = Field(default=\"en\", pattern=\"^(zh|en|ja|ko|de|fr|ru|pt|es|it|he|ar|da|el|fi|hi|ms|nl|no|pl|sv|sw|tr)$\")\n    seed: Optional[int] = Field(None, ge=0)\n    model_size: Optional[str] = Field(default=\"1.7B\", pattern=\"^(1\\\\.7B|0\\\\.6B|1B|3B)$\")\n    instruct: Optional[str] = Field(None, max_length=500)\n    engine: Optional[str] = Field(default=\"qwen\", pattern=\"^(qwen|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$\")\n    max_chunk_chars: int = Field(\n        default=800, ge=100, le=5000, description=\"Max characters per chunk for long text splitting\"\n    )\n    crossfade_ms: int = Field(\n        default=50, ge=0, le=500, description=\"Crossfade duration in ms between chunks (0 for hard cut)\"\n    )\n    normalize: bool = Field(default=True, description=\"Normalize output audio volume\")\n    effects_chain: Optional[List[\"EffectConfig\"]] = Field(\n        None, description=\"Effects chain to apply after generation (overrides profile default)\"\n    )\n\n\nclass GenerationResponse(BaseModel):\n    \"\"\"Response model for voice generation.\"\"\"\n\n    id: str\n    profile_id: str\n    text: str\n    language: str\n    audio_path: Optional[str] = None\n    duration: Optional[float] = None\n    seed: Optional[int] = None\n    instruct: Optional[str] = None\n    engine: Optional[str] = \"qwen\"\n    model_size: Optional[str] = None\n    status: str = \"completed\"\n    error: Optional[str] = None\n    is_favorited: bool = False\n    created_at: datetime\n    versions: Optional[List[\"GenerationVersionResponse\"]] = None\n    active_version_id: Optional[str] = None\n\n    class Config:\n        from_attributes = True\n\n\nclass HistoryQuery(BaseModel):\n    \"\"\"Query model for generation history.\"\"\"\n\n    profile_id: Optional[str] = None\n    search: Optional[str] = None\n    limit: int = Field(default=50, ge=1, le=100)\n    offset: int = Field(default=0, ge=0)\n\n\nclass HistoryResponse(BaseModel):\n    \"\"\"Response model for history entry (includes profile name).\"\"\"\n\n    id: str\n    profile_id: str\n    profile_name: str\n    text: str\n    language: str\n    audio_path: Optional[str] = None\n    duration: Optional[float] = None\n    seed: Optional[int] = None\n    instruct: Optional[str] = None\n    engine: Optional[str] = \"qwen\"\n    model_size: Optional[str] = None\n    status: str = \"completed\"\n    error: Optional[str] = None\n    is_favorited: bool = False\n    created_at: datetime\n    versions: Optional[List[\"GenerationVersionResponse\"]] = None\n    active_version_id: Optional[str] = None\n\n    class Config:\n        from_attributes = True\n\n\nclass HistoryListResponse(BaseModel):\n    \"\"\"Response model for history list.\"\"\"\n\n    items: List[HistoryResponse]\n    total: int\n\n\nclass TranscriptionRequest(BaseModel):\n    \"\"\"Request model for audio transcription.\"\"\"\n\n    language: Optional[str] = Field(None, pattern=\"^(en|zh|ja|ko|de|fr|ru|pt|es|it)$\")\n    model: Optional[str] = Field(None, pattern=\"^(base|small|medium|large|turbo)$\")\n\n\nclass TranscriptionResponse(BaseModel):\n    \"\"\"Response model for transcription.\"\"\"\n\n    text: str\n    duration: float\n\n\nclass HealthResponse(BaseModel):\n    \"\"\"Response model for health check.\"\"\"\n\n    status: str\n    model_loaded: bool\n    model_downloaded: Optional[bool] = None  # Whether model is cached/downloaded\n    model_size: Optional[str] = None  # Current model size if loaded\n    gpu_available: bool\n    gpu_type: Optional[str] = None  # GPU type (CUDA, MPS, or None)\n    vram_used_mb: Optional[float] = None\n    backend_type: Optional[str] = None  # Backend type (mlx or pytorch)\n    backend_variant: Optional[str] = None  # Binary variant (cpu or cuda)\n\n\nclass DirectoryCheck(BaseModel):\n    \"\"\"Health status for a single directory.\"\"\"\n\n    path: str\n    exists: bool\n    writable: bool\n    error: Optional[str] = None\n\n\nclass FilesystemHealthResponse(BaseModel):\n    \"\"\"Response model for filesystem health check.\"\"\"\n\n    healthy: bool\n    disk_free_mb: Optional[float] = None\n    disk_total_mb: Optional[float] = None\n    directories: List[DirectoryCheck]\n\n\nclass ModelStatus(BaseModel):\n    \"\"\"Response model for model status.\"\"\"\n\n    model_name: str\n    display_name: str\n    hf_repo_id: Optional[str] = None  # HuggingFace repository ID\n    downloaded: bool\n    downloading: bool = False  # True if download is in progress\n    size_mb: Optional[float] = None\n    loaded: bool = False\n\n\nclass ModelStatusListResponse(BaseModel):\n    \"\"\"Response model for model status list.\"\"\"\n\n    models: List[ModelStatus]\n\n\nclass ModelDownloadRequest(BaseModel):\n    \"\"\"Request model for triggering model download.\"\"\"\n\n    model_name: str\n\n\nclass ModelMigrateRequest(BaseModel):\n    \"\"\"Request model for migrating models to a new directory.\"\"\"\n\n    destination: str\n\n\nclass ActiveDownloadTask(BaseModel):\n    \"\"\"Response model for active download task.\"\"\"\n\n    model_name: str\n    status: str\n    started_at: datetime\n    error: Optional[str] = None\n    progress: Optional[float] = None  # 0-100 percentage\n    current: Optional[int] = None  # bytes downloaded\n    total: Optional[int] = None  # total bytes\n    filename: Optional[str] = None  # current file being downloaded\n\n\nclass ActiveGenerationTask(BaseModel):\n    \"\"\"Response model for active generation task.\"\"\"\n\n    task_id: str\n    profile_id: str\n    text_preview: str\n    started_at: datetime\n\n\nclass ActiveTasksResponse(BaseModel):\n    \"\"\"Response model for active tasks.\"\"\"\n\n    downloads: List[ActiveDownloadTask]\n    generations: List[ActiveGenerationTask]\n\n\nclass AudioChannelCreate(BaseModel):\n    \"\"\"Request model for creating an audio channel.\"\"\"\n\n    name: str = Field(..., min_length=1, max_length=100)\n    device_ids: List[str] = Field(default_factory=list)\n\n\nclass AudioChannelUpdate(BaseModel):\n    \"\"\"Request model for updating an audio channel.\"\"\"\n\n    name: Optional[str] = Field(None, min_length=1, max_length=100)\n    device_ids: Optional[List[str]] = None\n\n\nclass AudioChannelResponse(BaseModel):\n    \"\"\"Response model for audio channel.\"\"\"\n\n    id: str\n    name: str\n    is_default: bool\n    device_ids: List[str]\n    created_at: datetime\n\n    class Config:\n        from_attributes = True\n\n\nclass ChannelVoiceAssignment(BaseModel):\n    \"\"\"Request model for assigning voices to a channel.\"\"\"\n\n    profile_ids: List[str]\n\n\nclass ProfileChannelAssignment(BaseModel):\n    \"\"\"Request model for assigning channels to a profile.\"\"\"\n\n    channel_ids: List[str]\n\n\nclass StoryCreate(BaseModel):\n    \"\"\"Request model for creating a story.\"\"\"\n\n    name: str = Field(..., min_length=1, max_length=100)\n    description: Optional[str] = Field(None, max_length=500)\n\n\nclass StoryResponse(BaseModel):\n    \"\"\"Response model for story (list view).\"\"\"\n\n    id: str\n    name: str\n    description: Optional[str]\n    created_at: datetime\n    updated_at: datetime\n    item_count: int = 0\n\n    class Config:\n        from_attributes = True\n\n\nclass StoryItemDetail(BaseModel):\n    \"\"\"Detail model for story item with generation info.\"\"\"\n\n    id: str\n    story_id: str\n    generation_id: str\n    version_id: Optional[str] = None\n    start_time_ms: int\n    track: int = 0\n    trim_start_ms: int = 0\n    trim_end_ms: int = 0\n    created_at: datetime\n    # Generation details\n    profile_id: str\n    profile_name: str\n    text: str\n    language: str\n    audio_path: str\n    duration: float\n    seed: Optional[int]\n    instruct: Optional[str]\n    generation_created_at: datetime\n    # Versions available for this generation\n    versions: Optional[List[\"GenerationVersionResponse\"]] = None\n    active_version_id: Optional[str] = None\n\n    class Config:\n        from_attributes = True\n\n\nclass StoryDetailResponse(BaseModel):\n    \"\"\"Response model for story with items.\"\"\"\n\n    id: str\n    name: str\n    description: Optional[str]\n    created_at: datetime\n    updated_at: datetime\n    items: List[StoryItemDetail] = []\n\n    class Config:\n        from_attributes = True\n\n\nclass StoryItemCreate(BaseModel):\n    \"\"\"Request model for adding a generation to a story.\"\"\"\n\n    generation_id: str\n    start_time_ms: Optional[int] = None  # If not provided, will be calculated automatically\n    track: Optional[int] = 0  # Track number (0 = main track)\n\n\nclass StoryItemUpdateTime(BaseModel):\n    \"\"\"Request model for updating a story item's timecode.\"\"\"\n\n    generation_id: str\n    start_time_ms: int = Field(..., ge=0)\n\n\nclass StoryItemBatchUpdate(BaseModel):\n    \"\"\"Request model for batch updating story item timecodes.\"\"\"\n\n    updates: List[StoryItemUpdateTime]\n\n\nclass StoryItemReorder(BaseModel):\n    \"\"\"Request model for reordering story items.\"\"\"\n\n    generation_ids: List[str] = Field(..., min_length=1)\n\n\nclass StoryItemMove(BaseModel):\n    \"\"\"Request model for moving a story item (position and/or track).\"\"\"\n\n    start_time_ms: int = Field(..., ge=0)\n    track: int = 0\n\n\nclass StoryItemTrim(BaseModel):\n    \"\"\"Request model for trimming a story item.\"\"\"\n\n    trim_start_ms: int = Field(..., ge=0)\n    trim_end_ms: int = Field(..., ge=0)\n\n\nclass StoryItemSplit(BaseModel):\n    \"\"\"Request model for splitting a story item.\"\"\"\n\n    split_time_ms: int = Field(..., ge=0)  # Time within the clip to split at (relative to clip start)\n\n\nclass StoryItemVersionUpdate(BaseModel):\n    \"\"\"Request model for setting a story item's pinned version.\"\"\"\n\n    version_id: Optional[str] = None  # null = use generation default\n\n\nclass EffectConfig(BaseModel):\n    \"\"\"A single effect in an effects chain.\"\"\"\n\n    type: str\n    enabled: bool = True\n    params: dict = Field(default_factory=dict)\n\n\nclass EffectsChain(BaseModel):\n    \"\"\"An ordered list of effects to apply.\"\"\"\n\n    effects: List[EffectConfig] = Field(default_factory=list)\n\n\nclass EffectPresetCreate(BaseModel):\n    \"\"\"Request model for creating an effect preset.\"\"\"\n\n    name: str = Field(..., min_length=1, max_length=100)\n    description: Optional[str] = Field(None, max_length=500)\n    effects_chain: List[EffectConfig]\n\n\nclass EffectPresetUpdate(BaseModel):\n    \"\"\"Request model for updating an effect preset.\"\"\"\n\n    name: Optional[str] = Field(None, min_length=1, max_length=100)\n    description: Optional[str] = None\n    effects_chain: Optional[List[EffectConfig]] = None\n\n\nclass EffectPresetResponse(BaseModel):\n    \"\"\"Response model for effect preset.\"\"\"\n\n    id: str\n    name: str\n    description: Optional[str] = None\n    effects_chain: List[EffectConfig]\n    is_builtin: bool = False\n    created_at: datetime\n\n    class Config:\n        from_attributes = True\n\n\nclass GenerationVersionResponse(BaseModel):\n    \"\"\"Response model for a generation version.\"\"\"\n\n    id: str\n    generation_id: str\n    label: str\n    audio_path: str\n    effects_chain: Optional[List[EffectConfig]] = None\n    source_version_id: Optional[str] = None\n    is_default: bool\n    created_at: datetime\n\n    class Config:\n        from_attributes = True\n\n\nclass ApplyEffectsRequest(BaseModel):\n    \"\"\"Request to apply effects to an existing generation.\"\"\"\n\n    effects_chain: List[EffectConfig]\n    source_version_id: Optional[str] = Field(\n        None, description=\"Version to use as source audio (defaults to clean/original)\"\n    )\n    label: Optional[str] = Field(None, max_length=100, description=\"Label for this version (auto-generated if omitted)\")\n    set_as_default: bool = Field(default=True, description=\"Set this version as the default\")\n\n\nclass ProfileEffectsUpdate(BaseModel):\n    \"\"\"Request to update the default effects chain on a profile.\"\"\"\n\n    effects_chain: Optional[List[EffectConfig]] = Field(None, description=\"Effects chain (null to remove)\")\n\n\nclass AvailableEffectParam(BaseModel):\n    \"\"\"Description of a single effect parameter.\"\"\"\n\n    default: float\n    min: float\n    max: float\n    step: float\n    description: str\n\n\nclass AvailableEffect(BaseModel):\n    \"\"\"Description of an available effect type.\"\"\"\n\n    type: str\n    label: str\n    description: str\n    params: dict  # param_name -> AvailableEffectParam\n\n\nclass AvailableEffectsResponse(BaseModel):\n    \"\"\"Response listing all available effect types.\"\"\"\n\n    effects: List[AvailableEffect]\n"
  },
  {
    "path": "backend/pyproject.toml",
    "content": "[project]\nname = \"voicebox-backend\"\nversion = \"0.2.3\"\nrequires-python = \">=3.12\"\n\n# ---------------------------------------------------------------------------\n# Ruff – linter + formatter\n# ---------------------------------------------------------------------------\n\n[tool.ruff]\ntarget-version = \"py312\"\nline-length = 120\nsrc = [\".\"]\n\n# Files/dirs to skip entirely.\nextend-exclude = [\n    \"voicebox-server.spec\",\n    \"build_binary.py\",\n]\n\n[tool.ruff.lint]\nselect = [\n    \"F\",     # pyflakes\n    \"E\",     # pycodestyle errors\n    \"W\",     # pycodestyle warnings\n    \"I\",     # isort\n    \"N\",     # pep8-naming\n    \"UP\",    # pyupgrade (modernize syntax for 3.12)\n    \"B\",     # flake8-bugbear\n    \"A\",     # flake8-builtins (shadowing built-in names)\n    \"SIM\",   # flake8-simplify\n    \"T20\",   # flake8-print (flag print() calls)\n    \"RET\",   # flake8-return\n    \"PIE\",   # misc lints\n    \"PT\",    # flake8-pytest-style\n    \"RUF\",   # ruff-specific rules\n    \"ERA\",   # commented-out code detection\n    \"FIX\",   # flag TODO/FIXME/HACK/XXX for review\n]\n\nignore = [\n    # Allow print() in existing code -- remove items from this list as files\n    # are migrated to logging during the refactor.\n    \"T201\",   # print() found\n\n    # These conflict with the formatter or are too noisy during migration:\n    \"E501\",   # line too long (formatter handles this)\n    \"RET504\", # unnecessary assignment before return\n    \"SIM108\", # use ternary operator (sometimes less readable)\n    \"B008\",   # function call in default argument (FastAPI Depends() pattern)\n    \"UP007\",  # use X | Y for union (auto-fixed by UP, but noisy on big diffs)\n]\n\n# Per-file rule overrides.\n[tool.ruff.lint.per-file-ignores]\n# Tests can use assert, print, and magic values freely.\n\"tests/**\" = [\"S101\", \"T201\", \"PLR2004\", \"ERA001\"]\n# __init__.py re-exports are expected to have unused imports.\n\"**/__init__.py\" = [\"F401\"]\n# Entry points and scripts legitimately use print.\n\"server.py\" = [\"T201\"]\n\"main.py\" = [\"T201\"]\n# AMD GPU env vars must be set before torch import.\n\"app.py\" = [\"E402\"]\n\n[tool.ruff.lint.isort]\nknown-first-party = [\"backend\"]\n# Group \"from backend.*\" imports into the first-party section.\nforce-single-line = false\ncombine-as-imports = true\n\n[tool.ruff.format]\nquote-style = \"double\"\nindent-style = \"space\"\ndocstring-code-format = true\n\n# ---------------------------------------------------------------------------\n# pytest\n# ---------------------------------------------------------------------------\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\nasyncio_mode = \"auto\"\n"
  },
  {
    "path": "backend/requirements-mlx.txt",
    "content": "# MLX-specific dependencies (Apple Silicon only)\n# These should only be installed on aarch64-apple-darwin platforms\n\nmlx>=0.30.0\nmlx-audio>=0.3.1\n"
  },
  {
    "path": "backend/requirements.txt",
    "content": "# FastAPI and server\nfastapi>=0.109.0\nuvicorn[standard]>=0.27.0\npydantic>=2.5.0\n\n# Database\nsqlalchemy>=2.0.0\nalembic>=1.13.0\n\n# ML models\ntorch>=2.7.0\ntransformers>=4.36.0,<=4.57.6\naccelerate>=0.26.0\nhuggingface_hub>=0.20.0\nqwen-tts>=0.0.5\n\n# LuxTTS (voice cloning engine)\n# piper-phonemize needs custom index (no PyPI wheels)\n--find-links https://k2-fsa.github.io/icefall/piper_phonemize.html\n# linacodec is a git-only dep of Zipvoice (uv-only source, pip can't resolve it)\nlinacodec @ git+https://github.com/ysharma3501/LinaCodec.git\nZipvoice @ git+https://github.com/ysharma3501/LuxTTS.git\n\n# Chatterbox TTS sub-dependencies (chatterbox-tts itself is installed\n# --no-deps in the setup script because it pins numpy<1.26 / torch==2.6\n# which are incompatible with Python 3.12+)\nconformer>=0.3.2\ndiffusers>=0.29.0\nomegaconf\npykakasi\nresemble-perth>=1.0.1\ns3tokenizer\nspacy-pkuseg\npyloudnorm\n\n# HumeAI TADA sub-dependencies (hume-tada itself is installed\n# --no-deps in the setup script because it pins torch>=2.7,<2.8.\n# descript-audio-codec is NOT installed — it pulls onnx/tensorboard\n# via descript-audiotools.  A lightweight shim in utils/dac_shim.py\n# provides the only class TADA uses: Snake1d.)\ntorchaudio\n\n# Kokoro TTS (lightweight 82M-param engine)\nkokoro>=0.9.4\nmisaki[en]>=0.9.4\n# spacy model for misaki English G2P — must be pre-installed or misaki\n# tries spacy.cli.download() at runtime which crashes frozen builds\nen_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl\n\n# Audio processing\nlibrosa>=0.10.0\nsoundfile>=0.12.0\nnumpy>=1.24.0\nnumba>=0.60.0,<0.61.0\npedalboard>=0.9.0\n\n# HTTP client (for CUDA backend download)\nhttpx>=0.27.0\n\n# Utilities\npython-multipart>=0.0.6\nPillow>=10.0.0\n"
  },
  {
    "path": "backend/routes/__init__.py",
    "content": "\"\"\"Route registration for the voicebox API.\"\"\"\n\nfrom fastapi import FastAPI\n\n\ndef register_routers(app: FastAPI) -> None:\n    \"\"\"Include all domain routers on the application.\"\"\"\n    from .health import router as health_router\n    from .profiles import router as profiles_router\n    from .channels import router as channels_router\n    from .generations import router as generations_router\n    from .history import router as history_router\n    from .transcription import router as transcription_router\n    from .stories import router as stories_router\n    from .effects import router as effects_router\n    from .audio import router as audio_router\n    from .models import router as models_router\n    from .tasks import router as tasks_router\n    from .cuda import router as cuda_router\n\n    app.include_router(health_router)\n    app.include_router(profiles_router)\n    app.include_router(channels_router)\n    app.include_router(generations_router)\n    app.include_router(history_router)\n    app.include_router(transcription_router)\n    app.include_router(stories_router)\n    app.include_router(effects_router)\n    app.include_router(audio_router)\n    app.include_router(models_router)\n    app.include_router(tasks_router)\n    app.include_router(cuda_router)\n"
  },
  {
    "path": "backend/routes/audio.py",
    "content": "\"\"\"Audio file serving endpoints.\"\"\"\n\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.responses import FileResponse\nfrom sqlalchemy.orm import Session\n\nfrom .. import models\nfrom ..services import history\nfrom ..database import get_db\n\nrouter = APIRouter()\n\n\n@router.get(\"/audio/version/{version_id}\")\nasync def get_version_audio(version_id: str, db: Session = Depends(get_db)):\n    \"\"\"Serve audio for a specific version.\"\"\"\n    from ..services import versions as versions_mod\n\n    version = versions_mod.get_version(version_id, db)\n    if not version:\n        raise HTTPException(status_code=404, detail=\"Version not found\")\n\n    audio_path = Path(version.audio_path)\n    if not audio_path.exists():\n        raise HTTPException(status_code=404, detail=\"Audio file not found\")\n\n    return FileResponse(\n        audio_path,\n        media_type=\"audio/wav\",\n        filename=f\"generation_{version.generation_id}_{version.label}.wav\",\n    )\n\n\n@router.get(\"/audio/{generation_id}\")\nasync def get_audio(generation_id: str, db: Session = Depends(get_db)):\n    \"\"\"Serve generated audio file (serves the default version).\"\"\"\n    generation = await history.get_generation(generation_id, db)\n    if not generation:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    audio_path = Path(generation.audio_path)\n    if not audio_path.exists():\n        raise HTTPException(status_code=404, detail=\"Audio file not found\")\n\n    return FileResponse(\n        audio_path,\n        media_type=\"audio/wav\",\n        filename=f\"generation_{generation_id}.wav\",\n    )\n\n\n@router.get(\"/samples/{sample_id}\")\nasync def get_sample_audio(sample_id: str, db: Session = Depends(get_db)):\n    \"\"\"Serve profile sample audio file.\"\"\"\n    from ..database import ProfileSample as DBProfileSample\n\n    sample = db.query(DBProfileSample).filter_by(id=sample_id).first()\n    if not sample:\n        raise HTTPException(status_code=404, detail=\"Sample not found\")\n\n    audio_path = Path(sample.audio_path)\n    if not audio_path.exists():\n        raise HTTPException(status_code=404, detail=\"Audio file not found\")\n\n    return FileResponse(\n        audio_path,\n        media_type=\"audio/wav\",\n        filename=f\"sample_{sample_id}.wav\",\n    )\n"
  },
  {
    "path": "backend/routes/channels.py",
    "content": "\"\"\"Audio channel endpoints.\"\"\"\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy.orm import Session\n\nfrom .. import models\nfrom ..services import channels\nfrom ..database import get_db\n\nrouter = APIRouter()\n\n\n@router.get(\"/channels\", response_model=list[models.AudioChannelResponse])\nasync def list_channels(db: Session = Depends(get_db)):\n    \"\"\"List all audio channels.\"\"\"\n    return await channels.list_channels(db)\n\n\n@router.post(\"/channels\", response_model=models.AudioChannelResponse)\nasync def create_channel(\n    data: models.AudioChannelCreate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Create a new audio channel.\"\"\"\n    try:\n        return await channels.create_channel(data, db)\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.get(\"/channels/{channel_id}\", response_model=models.AudioChannelResponse)\nasync def get_channel(\n    channel_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get an audio channel by ID.\"\"\"\n    channel = await channels.get_channel(channel_id, db)\n    if not channel:\n        raise HTTPException(status_code=404, detail=\"Channel not found\")\n    return channel\n\n\n@router.put(\"/channels/{channel_id}\", response_model=models.AudioChannelResponse)\nasync def update_channel(\n    channel_id: str,\n    data: models.AudioChannelUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Update an audio channel.\"\"\"\n    try:\n        channel = await channels.update_channel(channel_id, data, db)\n        if not channel:\n            raise HTTPException(status_code=404, detail=\"Channel not found\")\n        return channel\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.delete(\"/channels/{channel_id}\")\nasync def delete_channel(\n    channel_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete an audio channel.\"\"\"\n    try:\n        success = await channels.delete_channel(channel_id, db)\n        if not success:\n            raise HTTPException(status_code=404, detail=\"Channel not found\")\n        return {\"message\": \"Channel deleted successfully\"}\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.get(\"/channels/{channel_id}/voices\")\nasync def get_channel_voices(\n    channel_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get list of profile IDs assigned to a channel.\"\"\"\n    try:\n        profile_ids = await channels.get_channel_voices(channel_id, db)\n        return {\"profile_ids\": profile_ids}\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.put(\"/channels/{channel_id}/voices\")\nasync def set_channel_voices(\n    channel_id: str,\n    data: models.ChannelVoiceAssignment,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Set which voices are assigned to a channel.\"\"\"\n    try:\n        await channels.set_channel_voices(channel_id, data, db)\n        return {\"message\": \"Channel voices updated successfully\"}\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n"
  },
  {
    "path": "backend/routes/cuda.py",
    "content": "\"\"\"CUDA backend management endpoints.\"\"\"\n\nimport logging\n\nfrom fastapi import APIRouter, HTTPException\nfrom fastapi.responses import StreamingResponse\n\nfrom ..services.task_queue import create_background_task\nfrom ..utils.progress import get_progress_manager\n\nrouter = APIRouter()\n\nlogger = logging.getLogger(__name__)\n\n\n@router.get(\"/backend/cuda-status\")\nasync def get_cuda_status():\n    \"\"\"Get CUDA backend download/availability status.\"\"\"\n    from ..services import cuda\n\n    return cuda.get_cuda_status()\n\n\n@router.post(\"/backend/download-cuda\")\nasync def download_cuda_backend():\n    \"\"\"Download the CUDA backend binary.\"\"\"\n    from ..services import cuda\n\n    if cuda.get_cuda_binary_path() is not None:\n        raise HTTPException(status_code=409, detail=\"CUDA backend already downloaded\")\n\n    progress_manager = get_progress_manager()\n    existing = progress_manager.get_progress(cuda.PROGRESS_KEY)\n    if existing and existing.get(\"status\") == \"downloading\":\n        raise HTTPException(status_code=409, detail=\"CUDA backend download already in progress\")\n\n    async def _download():\n        try:\n            await cuda.download_cuda_binary()\n        except Exception as e:\n            logger.error(\"CUDA download failed: %s\", e)\n\n    create_background_task(_download())\n    return {\"message\": \"CUDA backend download started\", \"progress_key\": \"cuda-backend\"}\n\n\n@router.delete(\"/backend/cuda\")\nasync def delete_cuda_backend():\n    \"\"\"Delete the downloaded CUDA backend binary.\"\"\"\n    from ..services import cuda\n\n    if cuda.is_cuda_active():\n        raise HTTPException(\n            status_code=409,\n            detail=\"Cannot delete CUDA backend while it is active. Switch to CPU first.\",\n        )\n\n    deleted = await cuda.delete_cuda_binary()\n    if not deleted:\n        raise HTTPException(status_code=404, detail=\"No CUDA backend found to delete\")\n\n    return {\"message\": \"CUDA backend deleted\"}\n\n\n@router.get(\"/backend/cuda-progress\")\nasync def get_cuda_download_progress():\n    \"\"\"Get CUDA backend download progress via Server-Sent Events.\"\"\"\n    progress_manager = get_progress_manager()\n\n    async def event_generator():\n        async for event in progress_manager.subscribe(\"cuda-backend\"):\n            yield event\n\n    return StreamingResponse(\n        event_generator(),\n        media_type=\"text/event-stream\",\n        headers={\n            \"Cache-Control\": \"no-cache\",\n            \"Connection\": \"keep-alive\",\n            \"X-Accel-Buffering\": \"no\",\n        },\n    )\n"
  },
  {
    "path": "backend/routes/effects.py",
    "content": "\"\"\"Effects presets and generation version endpoints.\"\"\"\n\nimport asyncio\nimport io\nimport uuid\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.responses import StreamingResponse\nfrom sqlalchemy.orm import Session\n\nfrom .. import config, models\nfrom ..services import history\nfrom ..database import Generation as DBGeneration, get_db\n\nrouter = APIRouter()\n\n\n@router.post(\"/effects/preview/{generation_id}\")\nasync def preview_effects(\n    generation_id: str,\n    data: models.ApplyEffectsRequest,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Apply effects to a generation's clean audio and stream back without saving.\"\"\"\n    gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n    if (gen.status or \"completed\") != \"completed\":\n        raise HTTPException(status_code=400, detail=\"Generation is not completed\")\n\n    from ..services import versions as versions_mod\n    from ..utils.effects import apply_effects, validate_effects_chain\n    from ..utils.audio import load_audio\n\n    chain_dicts = [e.model_dump() for e in data.effects_chain]\n    error = validate_effects_chain(chain_dicts)\n    if error:\n        raise HTTPException(status_code=400, detail=error)\n\n    all_versions = versions_mod.list_versions(generation_id, db)\n    clean_version = next((v for v in all_versions if v.effects_chain is None), None)\n    source_path = clean_version.audio_path if clean_version else gen.audio_path\n    if not source_path or not Path(source_path).exists():\n        raise HTTPException(status_code=404, detail=\"Source audio file not found\")\n\n    audio, sample_rate = await asyncio.to_thread(load_audio, source_path)\n    processed = await asyncio.to_thread(apply_effects, audio, sample_rate, chain_dicts)\n\n    import soundfile as sf\n\n    buf = io.BytesIO()\n    await asyncio.to_thread(lambda: sf.write(buf, processed, sample_rate, format=\"WAV\"))\n    buf.seek(0)\n\n    return StreamingResponse(\n        buf,\n        media_type=\"audio/wav\",\n        headers={\n            \"Content-Disposition\": f'inline; filename=\"preview_{generation_id}.wav\"',\n            \"Cache-Control\": \"no-cache, no-store\",\n        },\n    )\n\n\n@router.get(\"/effects/available\", response_model=models.AvailableEffectsResponse)\nasync def get_available_effects():\n    \"\"\"List all available effect types with parameter definitions.\"\"\"\n    from ..utils.effects import get_available_effects as _get_effects\n\n    return models.AvailableEffectsResponse(effects=[models.AvailableEffect(**e) for e in _get_effects()])\n\n\n@router.get(\"/effects/presets\", response_model=list[models.EffectPresetResponse])\nasync def list_effect_presets(db: Session = Depends(get_db)):\n    \"\"\"List all effect presets (built-in + user-created).\"\"\"\n    from ..services import effects as effects_mod\n\n    return effects_mod.list_presets(db)\n\n\n@router.get(\"/effects/presets/{preset_id}\", response_model=models.EffectPresetResponse)\nasync def get_effect_preset(preset_id: str, db: Session = Depends(get_db)):\n    \"\"\"Get a specific effect preset.\"\"\"\n    from ..services import effects as effects_mod\n\n    preset = effects_mod.get_preset(preset_id, db)\n    if not preset:\n        raise HTTPException(status_code=404, detail=\"Preset not found\")\n    return preset\n\n\n@router.post(\"/effects/presets\", response_model=models.EffectPresetResponse)\nasync def create_effect_preset(\n    data: models.EffectPresetCreate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Create a new effect preset.\"\"\"\n    from ..services import effects as effects_mod\n\n    try:\n        return effects_mod.create_preset(data, db)\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.put(\"/effects/presets/{preset_id}\", response_model=models.EffectPresetResponse)\nasync def update_effect_preset(\n    preset_id: str,\n    data: models.EffectPresetUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Update an effect preset.\"\"\"\n    from ..services import effects as effects_mod\n\n    try:\n        result = effects_mod.update_preset(preset_id, data, db)\n        if not result:\n            raise HTTPException(status_code=404, detail=\"Preset not found\")\n        return result\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.delete(\"/effects/presets/{preset_id}\")\nasync def delete_effect_preset(preset_id: str, db: Session = Depends(get_db)):\n    \"\"\"Delete a user effect preset.\"\"\"\n    from ..services import effects as effects_mod\n\n    try:\n        if not effects_mod.delete_preset(preset_id, db):\n            raise HTTPException(status_code=404, detail=\"Preset not found\")\n        return {\"status\": \"deleted\"}\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.get(\n    \"/generations/{generation_id}/versions\",\n    response_model=list[models.GenerationVersionResponse],\n)\nasync def list_generation_versions(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"List all versions for a generation.\"\"\"\n    gen = await history.get_generation(generation_id, db)\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    from ..services import versions as versions_mod\n\n    return versions_mod.list_versions(generation_id, db)\n\n\n@router.post(\n    \"/generations/{generation_id}/versions/apply-effects\",\n    response_model=models.GenerationVersionResponse,\n)\nasync def apply_effects_to_generation(\n    generation_id: str,\n    data: models.ApplyEffectsRequest,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Apply an effects chain to an existing generation, creating a new version.\"\"\"\n    gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n    if (gen.status or \"completed\") != \"completed\":\n        raise HTTPException(status_code=400, detail=\"Generation is not completed\")\n\n    from ..services import versions as versions_mod\n    from ..utils.effects import apply_effects, validate_effects_chain\n    from ..utils.audio import load_audio, save_audio\n\n    chain_dicts = [e.model_dump() for e in data.effects_chain]\n    error = validate_effects_chain(chain_dicts)\n    if error:\n        raise HTTPException(status_code=400, detail=error)\n\n    all_versions = versions_mod.list_versions(generation_id, db)\n    source_version_id = data.source_version_id\n    if source_version_id:\n        source_version = next((v for v in all_versions if v.id == source_version_id), None)\n        if not source_version:\n            raise HTTPException(status_code=404, detail=\"Source version not found\")\n        source_path = source_version.audio_path\n    else:\n        clean_version = next((v for v in all_versions if v.effects_chain is None), None)\n        if not clean_version:\n            source_path = gen.audio_path\n        else:\n            source_path = clean_version.audio_path\n            source_version_id = clean_version.id\n\n    if not source_path or not Path(source_path).exists():\n        raise HTTPException(status_code=404, detail=\"Source audio file not found\")\n\n    audio, sample_rate = await asyncio.to_thread(load_audio, source_path)\n    processed_audio = await asyncio.to_thread(apply_effects, audio, sample_rate, chain_dicts)\n\n    version_id = str(uuid.uuid4())\n    processed_path = config.get_generations_dir() / f\"{generation_id}_{version_id[:8]}.wav\"\n    await asyncio.to_thread(save_audio, processed_audio, str(processed_path), sample_rate)\n\n    label = data.label or f\"version-{len(all_versions) + 1}\"\n\n    version = versions_mod.create_version(\n        generation_id=generation_id,\n        label=label,\n        audio_path=str(processed_path),\n        db=db,\n        effects_chain=chain_dicts,\n        is_default=data.set_as_default,\n        source_version_id=source_version_id,\n    )\n\n    return version\n\n\n@router.put(\n    \"/generations/{generation_id}/versions/{version_id}/set-default\",\n    response_model=models.GenerationVersionResponse,\n)\nasync def set_default_version(\n    generation_id: str,\n    version_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Set a specific version as the default for a generation.\"\"\"\n    from ..services import versions as versions_mod\n\n    version = versions_mod.get_version(version_id, db)\n    if not version or version.generation_id != generation_id:\n        raise HTTPException(status_code=404, detail=\"Version not found\")\n\n    result = versions_mod.set_default_version(version_id, db)\n    if not result:\n        raise HTTPException(status_code=404, detail=\"Version not found\")\n    return result\n\n\n@router.delete(\"/generations/{generation_id}/versions/{version_id}\")\nasync def delete_generation_version(\n    generation_id: str,\n    version_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete a version. Cannot delete the last remaining version.\"\"\"\n    from ..services import versions as versions_mod\n\n    version = versions_mod.get_version(version_id, db)\n    if not version or version.generation_id != generation_id:\n        raise HTTPException(status_code=404, detail=\"Version not found\")\n\n    if not versions_mod.delete_version(version_id, db):\n        raise HTTPException(\n            status_code=400,\n            detail=\"Cannot delete the last remaining version\",\n        )\n    return {\"status\": \"deleted\"}\n"
  },
  {
    "path": "backend/routes/generations.py",
    "content": "\"\"\"TTS generation endpoints.\"\"\"\n\nimport asyncio\nimport logging\nimport uuid\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.responses import StreamingResponse\nfrom sqlalchemy.orm import Session\n\nlogger = logging.getLogger(__name__)\n\nfrom .. import models\nfrom ..services import history, profiles, tts\nfrom ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db\nfrom ..services.generation import run_generation\nfrom ..services.task_queue import enqueue_generation\nfrom ..utils.tasks import get_task_manager\n\nrouter = APIRouter()\n\n\n@router.post(\"/generate\", response_model=models.GenerationResponse)\nasync def generate_speech(\n    data: models.GenerationRequest,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Generate speech from text using a voice profile.\"\"\"\n    task_manager = get_task_manager()\n    generation_id = str(uuid.uuid4())\n\n    profile = await profiles.get_profile(data.profile_id, db)\n    if not profile:\n        raise HTTPException(status_code=404, detail=\"Profile not found\")\n\n    from ..backends import engine_has_model_sizes\n\n    engine = data.engine or \"qwen\"\n    model_size = (data.model_size or \"1.7B\") if engine_has_model_sizes(engine) else None\n\n    generation = await history.create_generation(\n        profile_id=data.profile_id,\n        text=data.text,\n        language=data.language,\n        audio_path=\"\",\n        duration=0,\n        seed=data.seed,\n        db=db,\n        instruct=data.instruct,\n        generation_id=generation_id,\n        status=\"generating\",\n        engine=engine,\n        model_size=model_size if engine_has_model_sizes(engine) else None,\n    )\n\n    task_manager.start_generation(\n        task_id=generation_id,\n        profile_id=data.profile_id,\n        text=data.text,\n    )\n\n    effects_chain_config = None\n    if data.effects_chain is not None:\n        effects_chain_config = [e.model_dump() for e in data.effects_chain]\n    else:\n        import json as _json\n\n        profile_obj = db.query(DBVoiceProfile).filter_by(id=data.profile_id).first()\n        if profile_obj and profile_obj.effects_chain:\n            try:\n                effects_chain_config = _json.loads(profile_obj.effects_chain)\n            except Exception:\n                pass\n\n    enqueue_generation(\n        run_generation(\n            generation_id=generation_id,\n            profile_id=data.profile_id,\n            text=data.text,\n            language=data.language,\n            engine=engine,\n            model_size=model_size,\n            seed=data.seed,\n            normalize=data.normalize,\n            effects_chain=effects_chain_config,\n            instruct=data.instruct,\n            mode=\"generate\",\n            max_chunk_chars=data.max_chunk_chars,\n            crossfade_ms=data.crossfade_ms,\n        )\n    )\n\n    return generation\n\n\n@router.post(\"/generate/{generation_id}/retry\", response_model=models.GenerationResponse)\nasync def retry_generation(generation_id: str, db: Session = Depends(get_db)):\n    \"\"\"Retry a failed generation using the same parameters.\"\"\"\n    gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    if (gen.status or \"completed\") != \"failed\":\n        raise HTTPException(status_code=400, detail=\"Only failed generations can be retried\")\n\n    gen.status = \"generating\"\n    gen.error = None\n    gen.audio_path = \"\"\n    gen.duration = 0\n    db.commit()\n    db.refresh(gen)\n\n    task_manager = get_task_manager()\n    task_manager.start_generation(\n        task_id=generation_id,\n        profile_id=gen.profile_id,\n        text=gen.text,\n    )\n\n    enqueue_generation(\n        run_generation(\n            generation_id=generation_id,\n            profile_id=gen.profile_id,\n            text=gen.text,\n            language=gen.language,\n            engine=gen.engine or \"qwen\",\n            model_size=gen.model_size or \"1.7B\",\n            seed=gen.seed,\n            instruct=gen.instruct,\n            mode=\"retry\",\n        )\n    )\n\n    return models.GenerationResponse.model_validate(gen)\n\n\n@router.post(\n    \"/generate/{generation_id}/regenerate\",\n    response_model=models.GenerationResponse,\n)\nasync def regenerate_generation(generation_id: str, db: Session = Depends(get_db)):\n    \"\"\"Re-run TTS with the same parameters and save the result as a new version.\"\"\"\n    gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n    if (gen.status or \"completed\") != \"completed\":\n        raise HTTPException(status_code=400, detail=\"Generation must be completed to regenerate\")\n\n    gen.status = \"generating\"\n    gen.error = None\n    db.commit()\n    db.refresh(gen)\n\n    task_manager = get_task_manager()\n    task_manager.start_generation(\n        task_id=generation_id,\n        profile_id=gen.profile_id,\n        text=gen.text,\n    )\n\n    version_id = str(uuid.uuid4())\n\n    enqueue_generation(\n        run_generation(\n            generation_id=generation_id,\n            profile_id=gen.profile_id,\n            text=gen.text,\n            language=gen.language,\n            engine=gen.engine or \"qwen\",\n            model_size=gen.model_size or \"1.7B\",\n            seed=gen.seed,\n            instruct=gen.instruct,\n            mode=\"regenerate\",\n            version_id=version_id,\n        )\n    )\n\n    return models.GenerationResponse.model_validate(gen)\n\n\n@router.get(\"/generate/{generation_id}/status\")\nasync def get_generation_status(generation_id: str, db: Session = Depends(get_db)):\n    \"\"\"SSE endpoint that streams generation status updates.\"\"\"\n    import json\n\n    async def event_stream():\n        try:\n            while True:\n                db.expire_all()\n                gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n                if not gen:\n                    yield f\"data: {json.dumps({'status': 'not_found', 'id': generation_id})}\\n\\n\"\n                    return\n\n                payload = {\n                    \"id\": gen.id,\n                    \"status\": gen.status or \"completed\",\n                    \"duration\": gen.duration,\n                    \"error\": gen.error,\n                }\n                yield f\"data: {json.dumps(payload)}\\n\\n\"\n\n                if (gen.status or \"completed\") in (\"completed\", \"failed\"):\n                    return\n\n                await asyncio.sleep(1)\n        except (BrokenPipeError, ConnectionResetError, asyncio.CancelledError):\n            logger.debug(\"SSE client disconnected for generation %s\", generation_id)\n\n    return StreamingResponse(\n        event_stream(),\n        media_type=\"text/event-stream\",\n        headers={\n            \"Cache-Control\": \"no-cache\",\n            \"Connection\": \"keep-alive\",\n            \"X-Accel-Buffering\": \"no\",\n        },\n    )\n\n\n@router.post(\"/generate/stream\")\nasync def stream_speech(\n    data: models.GenerationRequest,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Generate speech and stream the WAV audio directly without saving to disk.\"\"\"\n    from ..backends import get_tts_backend_for_engine, ensure_model_cached_or_raise, load_engine_model, engine_needs_trim\n\n    profile = await profiles.get_profile(data.profile_id, db)\n    if not profile:\n        raise HTTPException(status_code=404, detail=\"Profile not found\")\n\n    # Mirror the regular /generate endpoint behavior more closely:\n    # if the caller doesn't specify an engine, prefer the profile's default\n    # engine (or preset engine) before falling back to qwen.\n    engine = (\n        data.engine\n        or getattr(profile, \"default_engine\", None)\n        or getattr(profile, \"preset_engine\", None)\n        or \"qwen\"\n    )\n    tts_model = get_tts_backend_for_engine(engine)\n    model_size = data.model_size or \"1.7B\"\n\n    await ensure_model_cached_or_raise(engine, model_size)\n    await load_engine_model(engine, model_size)\n\n    voice_prompt = await profiles.create_voice_prompt_for_profile(\n        data.profile_id,\n        db,\n        engine=engine,\n    )\n\n    from ..utils.chunked_tts import generate_chunked\n\n    trim_fn = None\n    if engine_needs_trim(engine):\n        from ..utils.audio import trim_tts_output\n\n        trim_fn = trim_tts_output\n\n    audio, sample_rate = await generate_chunked(\n        tts_model,\n        data.text,\n        voice_prompt,\n        language=data.language,\n        seed=data.seed,\n        instruct=data.instruct,\n        max_chunk_chars=data.max_chunk_chars,\n        crossfade_ms=data.crossfade_ms,\n        trim_fn=trim_fn,\n    )\n\n    effects_chain_config = None\n    if data.effects_chain is not None:\n        effects_chain_config = [e.model_dump() for e in data.effects_chain]\n    elif profile.effects_chain:\n        import json as _json\n\n        try:\n            effects_chain_config = _json.loads(profile.effects_chain)\n        except Exception:\n            effects_chain_config = None\n\n    if effects_chain_config:\n        from ..utils.effects import apply_effects\n\n        audio = apply_effects(audio, sample_rate, effects_chain_config)\n\n    if data.normalize:\n        from ..utils.audio import normalize_audio\n\n        audio = normalize_audio(audio)\n\n    wav_bytes = tts.audio_to_wav_bytes(audio, sample_rate)\n\n    async def _wav_stream():\n        try:\n            chunk_size = 64 * 1024\n            for i in range(0, len(wav_bytes), chunk_size):\n                yield wav_bytes[i : i + chunk_size]\n        except (BrokenPipeError, ConnectionResetError, asyncio.CancelledError):\n            logger.debug(\"Client disconnected during audio stream\")\n\n    return StreamingResponse(\n        _wav_stream(),\n        media_type=\"audio/wav\",\n        headers={\"Content-Disposition\": 'attachment; filename=\"speech.wav\"'},\n    )\n"
  },
  {
    "path": "backend/routes/health.py",
    "content": "\"\"\"Health and infrastructure endpoints.\"\"\"\n\nimport asyncio\nimport os\nimport signal\nfrom pathlib import Path\n\nimport torch\nfrom fastapi import APIRouter, Depends\nfrom fastapi.responses import FileResponse\nfrom sqlalchemy.orm import Session\n\nfrom .. import config, models\nfrom ..services import tts\nfrom ..database import get_db\nfrom ..utils.platform_detect import get_backend_type\n\nrouter = APIRouter()\n\n# Frontend build directory — present in Docker, absent in dev/API-only mode\n_frontend_dir = Path(__file__).resolve().parent.parent.parent / \"frontend\"\n\n\n@router.get(\"/\")\nasync def root():\n    \"\"\"Root endpoint — serves SPA index.html in Docker, JSON otherwise.\"\"\"\n    from .. import __version__\n\n    index = _frontend_dir / \"index.html\"\n    if index.is_file():\n        return FileResponse(index, media_type=\"text/html\")\n    return {\"message\": \"voicebox API\", \"version\": __version__}\n\n\n@router.post(\"/shutdown\")\nasync def shutdown():\n    \"\"\"Gracefully shutdown the server.\"\"\"\n\n    async def shutdown_async():\n        await asyncio.sleep(0.1)\n        os.kill(os.getpid(), signal.SIGTERM)\n\n    asyncio.create_task(shutdown_async())\n    return {\"message\": \"Shutting down...\"}\n\n\n@router.post(\"/watchdog/disable\")\nasync def watchdog_disable():\n    \"\"\"Disable the parent process watchdog so the server keeps running.\"\"\"\n    from backend.server import disable_watchdog\n\n    disable_watchdog()\n    return {\"message\": \"Watchdog disabled\"}\n\n\n@router.get(\"/health\", response_model=models.HealthResponse)\nasync def health():\n    \"\"\"Health check endpoint.\"\"\"\n    from huggingface_hub import constants as hf_constants\n    from pathlib import Path\n\n    tts_model = tts.get_tts_model()\n    backend_type = get_backend_type()\n\n    has_cuda = torch.cuda.is_available()\n    has_mps = hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available()\n\n    has_xpu = False\n    xpu_name = None\n    try:\n        import intel_extension_for_pytorch as ipex  # noqa: F401 -- side-effect import enables XPU\n\n        if hasattr(torch, \"xpu\") and torch.xpu.is_available():\n            has_xpu = True\n            try:\n                xpu_name = torch.xpu.get_device_name(0)\n            except Exception:\n                xpu_name = \"Intel GPU\"\n    except ImportError:\n        pass\n\n    has_directml = False\n    directml_name = None\n    try:\n        import torch_directml\n\n        if torch_directml.device_count() > 0:\n            has_directml = True\n            try:\n                directml_name = torch_directml.device_name(0)\n            except Exception:\n                directml_name = \"DirectML GPU\"\n    except ImportError:\n        pass\n\n    gpu_available = has_cuda or has_mps or has_xpu or has_directml or backend_type == \"mlx\"\n\n    gpu_type = None\n    if has_cuda:\n        gpu_type = f\"CUDA ({torch.cuda.get_device_name(0)})\"\n    elif has_mps:\n        gpu_type = \"MPS (Apple Silicon)\"\n    elif backend_type == \"mlx\":\n        gpu_type = \"Metal (Apple Silicon via MLX)\"\n    elif has_xpu:\n        gpu_type = f\"XPU ({xpu_name})\"\n    elif has_directml:\n        gpu_type = f\"DirectML ({directml_name})\"\n\n    vram_used = None\n    if has_cuda:\n        vram_used = torch.cuda.memory_allocated() / 1024 / 1024\n\n    model_loaded = False\n    model_size = None\n    try:\n        if tts_model.is_loaded():\n            model_loaded = True\n            model_size = getattr(tts_model, \"_current_model_size\", None)\n            if not model_size:\n                model_size = getattr(tts_model, \"model_size\", None)\n    except Exception:\n        model_loaded = False\n        model_size = None\n\n    model_downloaded = None\n    try:\n        from ..backends import get_model_config\n\n        default_config = get_model_config(\"qwen-tts-1.7B\")\n        default_model_id = default_config.hf_repo_id if default_config else \"Qwen/Qwen3-TTS-12Hz-1.7B-Base\"\n\n        try:\n            from huggingface_hub import scan_cache_dir\n\n            cache_info = scan_cache_dir()\n            for repo in cache_info.repos:\n                if repo.repo_id == default_model_id:\n                    model_downloaded = True\n                    break\n        except (ImportError, Exception):\n            cache_dir = hf_constants.HF_HUB_CACHE\n            repo_cache = Path(cache_dir) / (\"models--\" + default_model_id.replace(\"/\", \"--\"))\n            if repo_cache.exists():\n                has_model_files = (\n                    any(repo_cache.rglob(\"*.bin\"))\n                    or any(repo_cache.rglob(\"*.safetensors\"))\n                    or any(repo_cache.rglob(\"*.pt\"))\n                    or any(repo_cache.rglob(\"*.pth\"))\n                    or any(repo_cache.rglob(\"*.npz\"))\n                )\n                model_downloaded = has_model_files\n    except Exception:\n        pass\n\n    return models.HealthResponse(\n        status=\"healthy\",\n        model_loaded=model_loaded,\n        model_downloaded=model_downloaded,\n        model_size=model_size,\n        gpu_available=gpu_available,\n        gpu_type=gpu_type,\n        vram_used_mb=vram_used,\n        backend_type=backend_type,\n        backend_variant=os.environ.get(\"VOICEBOX_BACKEND_VARIANT\", \"cuda\" if torch.cuda.is_available() else \"cpu\"),\n    )\n\n\n@router.get(\"/health/filesystem\", response_model=models.FilesystemHealthResponse)\nasync def filesystem_health():\n    \"\"\"Check filesystem health: directory existence, write permissions, and disk space.\"\"\"\n    import shutil\n\n    dirs_to_check = {\n        \"generations\": config.get_generations_dir(),\n        \"profiles\": config.get_profiles_dir(),\n        \"data\": config.get_data_dir(),\n    }\n\n    checks: list[models.DirectoryCheck] = []\n    all_ok = True\n\n    for _label, dir_path in dirs_to_check.items():\n        exists = dir_path.exists()\n        writable = False\n        error = None\n        if exists:\n            probe = dir_path / \".voicebox_probe\"\n            try:\n                probe.write_text(\"ok\")\n                probe.unlink()\n                writable = True\n            except PermissionError:\n                error = \"Permission denied\"\n            except OSError as e:\n                error = str(e)\n            finally:\n                try:\n                    probe.unlink(missing_ok=True)\n                except Exception:\n                    pass\n        else:\n            error = \"Directory does not exist\"\n\n        if not exists or not writable:\n            all_ok = False\n\n        checks.append(\n            models.DirectoryCheck(\n                path=str(dir_path.resolve()),\n                exists=exists,\n                writable=writable,\n                error=error,\n            )\n        )\n\n    disk_free_mb = None\n    disk_total_mb = None\n    try:\n        usage = shutil.disk_usage(str(config.get_data_dir()))\n        disk_free_mb = round(usage.free / (1024 * 1024), 1)\n        disk_total_mb = round(usage.total / (1024 * 1024), 1)\n        if disk_free_mb < 500:\n            all_ok = False\n    except OSError:\n        all_ok = False\n\n    return models.FilesystemHealthResponse(\n        healthy=all_ok,\n        disk_free_mb=disk_free_mb,\n        disk_total_mb=disk_total_mb,\n        directories=checks,\n    )\n"
  },
  {
    "path": "backend/routes/history.py",
    "content": "\"\"\"Generation history endpoints.\"\"\"\n\nimport io\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, Depends, File, HTTPException, UploadFile\nfrom fastapi.responses import FileResponse, StreamingResponse\nfrom sqlalchemy.orm import Session\n\nfrom .. import models\nfrom ..services import export_import, history\nfrom ..app import safe_content_disposition\nfrom ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db\n\nrouter = APIRouter()\n\n\n@router.get(\"/history\", response_model=models.HistoryListResponse)\nasync def list_history(\n    profile_id: str | None = None,\n    search: str | None = None,\n    limit: int = 50,\n    offset: int = 0,\n    db: Session = Depends(get_db),\n):\n    \"\"\"List generation history with optional filters.\"\"\"\n    query = models.HistoryQuery(\n        profile_id=profile_id,\n        search=search,\n        limit=limit,\n        offset=offset,\n    )\n    return await history.list_generations(query, db)\n\n\n@router.get(\"/history/stats\")\nasync def get_stats(db: Session = Depends(get_db)):\n    \"\"\"Get generation statistics.\"\"\"\n    return await history.get_generation_stats(db)\n\n\n@router.post(\"/history/import\")\nasync def import_generation(\n    file: UploadFile = File(...),\n    db: Session = Depends(get_db),\n):\n    \"\"\"Import a generation from a ZIP archive.\"\"\"\n    MAX_FILE_SIZE = 50 * 1024 * 1024\n\n    content = await file.read()\n\n    if len(content) > MAX_FILE_SIZE:\n        raise HTTPException(\n            status_code=400, detail=f\"File too large. Maximum size is {MAX_FILE_SIZE / (1024 * 1024)}MB\"\n        )\n\n    try:\n        result = await export_import.import_generation_from_zip(content, db)\n        return result\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@router.get(\"/history/{generation_id}\", response_model=models.HistoryResponse)\nasync def get_generation(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get a generation by ID.\"\"\"\n    result = (\n        db.query(DBGeneration, DBVoiceProfile.name.label(\"profile_name\"))\n        .join(DBVoiceProfile, DBGeneration.profile_id == DBVoiceProfile.id)\n        .filter(DBGeneration.id == generation_id)\n        .first()\n    )\n\n    if not result:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    gen, profile_name = result\n    return models.HistoryResponse(\n        id=gen.id,\n        profile_id=gen.profile_id,\n        profile_name=profile_name,\n        text=gen.text,\n        language=gen.language,\n        audio_path=gen.audio_path,\n        duration=gen.duration,\n        seed=gen.seed,\n        instruct=gen.instruct,\n        created_at=gen.created_at,\n    )\n\n\n@router.post(\"/history/{generation_id}/favorite\")\nasync def toggle_favorite(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Toggle the favorite status of a generation.\"\"\"\n    gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n    gen.is_favorited = not gen.is_favorited\n    db.commit()\n    return {\"is_favorited\": gen.is_favorited}\n\n\n@router.delete(\"/history/{generation_id}\")\nasync def delete_generation(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete a generation.\"\"\"\n    success = await history.delete_generation(generation_id, db)\n    if not success:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n    return {\"message\": \"Generation deleted successfully\"}\n\n\n@router.get(\"/history/{generation_id}/export\")\nasync def export_generation(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Export a generation as a ZIP archive.\"\"\"\n    generation = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not generation:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    try:\n        zip_bytes = export_import.export_generation_to_zip(generation_id, db)\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n    safe_text = \"\".join(c for c in generation.text[:30] if c.isalnum() or c in (\" \", \"-\", \"_\")).strip()\n    if not safe_text:\n        safe_text = \"generation\"\n    filename = f\"generation-{safe_text}.voicebox.zip\"\n\n    return StreamingResponse(\n        io.BytesIO(zip_bytes),\n        media_type=\"application/zip\",\n        headers={\"Content-Disposition\": safe_content_disposition(\"attachment\", filename)},\n    )\n\n\n@router.get(\"/history/{generation_id}/export-audio\")\nasync def export_generation_audio(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Export only the audio file from a generation.\"\"\"\n    generation = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not generation:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    if not generation.audio_path:\n        raise HTTPException(status_code=404, detail=\"Generation has no audio file\")\n\n    audio_path = Path(generation.audio_path)\n    if not audio_path.is_file():\n        raise HTTPException(status_code=404, detail=\"Audio file not found\")\n\n    safe_text = \"\".join(c for c in generation.text[:30] if c.isalnum() or c in (\" \", \"-\", \"_\")).strip()\n    if not safe_text:\n        safe_text = \"generation\"\n    filename = f\"{safe_text}.wav\"\n\n    return FileResponse(\n        audio_path,\n        media_type=\"audio/wav\",\n        headers={\"Content-Disposition\": safe_content_disposition(\"attachment\", filename)},\n    )\n"
  },
  {
    "path": "backend/routes/models.py",
    "content": "\"\"\"Model management endpoints.\"\"\"\n\nimport asyncio\nimport shutil\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.responses import StreamingResponse\nfrom sqlalchemy.orm import Session\n\nfrom .. import models\nfrom ..utils.platform_detect import get_backend_type\nfrom ..services.task_queue import create_background_task\nfrom ..utils.progress import get_progress_manager\nfrom ..utils.tasks import get_task_manager\n\nrouter = APIRouter()\n\n\ndef _get_dir_size(path: Path) -> int:\n    \"\"\"Get total size of a directory in bytes.\"\"\"\n    total = 0\n    for f in path.rglob(\"*\"):\n        if f.is_file():\n            total += f.stat().st_size\n    return total\n\n\ndef _copy_with_progress(src: Path, dst: Path, progress_manager, copied_so_far: int, total_bytes: int) -> int:\n    \"\"\"Copy a directory tree with byte-level progress tracking.\"\"\"\n    dst.mkdir(parents=True, exist_ok=True)\n    for item in src.iterdir():\n        dest_item = dst / item.name\n        if item.is_dir():\n            copied_so_far = _copy_with_progress(item, dest_item, progress_manager, copied_so_far, total_bytes)\n        else:\n            size = item.stat().st_size\n            shutil.copy2(str(item), str(dest_item))\n            copied_so_far += size\n            progress_manager.update_progress(\n                \"migration\",\n                copied_so_far,\n                total_bytes,\n                filename=item.name,\n                status=\"downloading\",\n            )\n    return copied_so_far\n\n\n@router.post(\"/models/load\")\nasync def load_model(model_size: str = \"1.7B\"):\n    \"\"\"Manually load TTS model.\"\"\"\n    from ..services import tts\n\n    try:\n        tts_model = tts.get_tts_model()\n        await tts_model.load_model_async(model_size)\n        return {\"message\": f\"Model {model_size} loaded successfully\"}\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@router.post(\"/models/unload\")\nasync def unload_model():\n    \"\"\"Unload the default Qwen TTS model to free memory.\"\"\"\n    from ..services import tts\n\n    try:\n        tts.unload_tts_model()\n        return {\"message\": \"Model unloaded successfully\"}\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@router.post(\"/models/{model_name}/unload\")\nasync def unload_model_by_name(model_name: str):\n    \"\"\"Unload a specific model from memory without deleting it from disk.\"\"\"\n    from ..backends import get_model_config, unload_model_by_config\n\n    config = get_model_config(model_name)\n    if not config:\n        raise HTTPException(status_code=400, detail=f\"Unknown model: {model_name}\")\n\n    try:\n        was_loaded = unload_model_by_config(config)\n        if not was_loaded:\n            return {\"message\": f\"Model {model_name} is not loaded\"}\n        return {\"message\": f\"Model {model_name} unloaded successfully\"}\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e)) from e\n\n\n@router.get(\"/models/progress/{model_name}\")\nasync def get_model_progress(model_name: str):\n    \"\"\"Get model download progress via Server-Sent Events.\"\"\"\n    progress_manager = get_progress_manager()\n\n    async def event_generator():\n        async for event in progress_manager.subscribe(model_name):\n            yield event\n\n    return StreamingResponse(\n        event_generator(),\n        media_type=\"text/event-stream\",\n        headers={\n            \"Cache-Control\": \"no-cache\",\n            \"Connection\": \"keep-alive\",\n            \"X-Accel-Buffering\": \"no\",\n        },\n    )\n\n\n@router.get(\"/models/cache-dir\")\nasync def get_models_cache_dir():\n    \"\"\"Get the path to the HuggingFace model cache directory.\"\"\"\n    from huggingface_hub import constants as hf_constants\n\n    return {\"path\": str(Path(hf_constants.HF_HUB_CACHE))}\n\n\n@router.post(\"/models/migrate\")\nasync def migrate_models(request: models.ModelMigrateRequest):\n    \"\"\"Move all downloaded models to a new directory with byte-level progress via SSE.\"\"\"\n    from huggingface_hub import constants as hf_constants\n\n    source = Path(hf_constants.HF_HUB_CACHE)\n    destination = Path(request.destination)\n\n    if not source.exists():\n        raise HTTPException(status_code=404, detail=\"Current model cache directory not found\")\n\n    if source.resolve() == destination.resolve():\n        raise HTTPException(status_code=400, detail=\"Source and destination are the same directory\")\n\n    if destination.resolve().is_relative_to(source.resolve()):\n        raise HTTPException(status_code=400, detail=\"Destination cannot be inside the current cache directory\")\n\n    model_dirs = [d for d in source.iterdir() if d.name.startswith(\"models--\") and d.is_dir()]\n    if not model_dirs:\n        return {\"moved\": 0, \"errors\": [], \"source\": str(source), \"destination\": str(destination)}\n\n    destination.mkdir(parents=True, exist_ok=True)\n\n    progress_manager = get_progress_manager()\n\n    same_fs = False\n    try:\n        same_fs = source.stat().st_dev == destination.stat().st_dev\n    except OSError:\n        pass\n\n    async def migrate_background():\n        moved = 0\n        errors = []\n        try:\n            if same_fs:\n                total = len(model_dirs)\n                for i, item in enumerate(model_dirs):\n                    dest_item = destination / item.name\n                    try:\n                        if dest_item.exists():\n                            shutil.rmtree(dest_item)\n                        shutil.move(str(item), str(dest_item))\n                        moved += 1\n                        progress_manager.update_progress(\n                            \"migration\",\n                            i + 1,\n                            total,\n                            filename=item.name,\n                            status=\"downloading\",\n                        )\n                    except Exception as e:\n                        errors.append(f\"{item.name}: {str(e)}\")\n            else:\n                total_bytes = sum(_get_dir_size(d) for d in model_dirs)\n                progress_manager.update_progress(\n                    \"migration\", 0, total_bytes, filename=\"Calculating...\", status=\"downloading\"\n                )\n\n                copied = 0\n                for item in model_dirs:\n                    dest_item = destination / item.name\n                    try:\n                        if dest_item.exists():\n                            shutil.rmtree(dest_item)\n                        copied = await asyncio.to_thread(\n                            _copy_with_progress, item, dest_item, progress_manager, copied, total_bytes\n                        )\n                        await asyncio.to_thread(shutil.rmtree, str(item))\n                        moved += 1\n                    except Exception as e:\n                        errors.append(f\"{item.name}: {str(e)}\")\n\n            progress_manager.update_progress(\"migration\", 1, 1, status=\"complete\")\n            progress_manager.mark_complete(\"migration\")\n        except Exception as e:\n            progress_manager.update_progress(\"migration\", 0, 0, status=\"error\")\n            progress_manager.mark_error(\"migration\", str(e))\n\n    create_background_task(migrate_background())\n\n    return {\"source\": str(source), \"destination\": str(destination)}\n\n\n@router.get(\"/models/migrate/progress\")\nasync def get_migration_progress():\n    \"\"\"Get model migration progress via Server-Sent Events.\"\"\"\n    progress_manager = get_progress_manager()\n\n    async def event_generator():\n        async for event in progress_manager.subscribe(\"migration\"):\n            yield event\n\n    return StreamingResponse(\n        event_generator(),\n        media_type=\"text/event-stream\",\n        headers={\n            \"Cache-Control\": \"no-cache\",\n            \"Connection\": \"keep-alive\",\n            \"X-Accel-Buffering\": \"no\",\n        },\n    )\n\n\n@router.get(\"/models/status\", response_model=models.ModelStatusListResponse)\nasync def get_model_status():\n    \"\"\"Get status of all available models.\"\"\"\n    from huggingface_hub import constants as hf_constants\n\n    backend_type = get_backend_type()\n    task_manager = get_task_manager()\n\n    active_download_names = {task.model_name for task in task_manager.get_active_downloads()}\n\n    try:\n        from huggingface_hub import scan_cache_dir\n\n        use_scan_cache = True\n    except ImportError:\n        use_scan_cache = False\n\n    from ..backends import get_all_model_configs, check_model_loaded\n\n    registry_configs = get_all_model_configs()\n    model_configs = [\n        {\n            \"model_name\": cfg.model_name,\n            \"display_name\": cfg.display_name,\n            \"hf_repo_id\": cfg.hf_repo_id,\n            \"model_size\": cfg.model_size,\n            \"check_loaded\": lambda c=cfg: check_model_loaded(c),\n        }\n        for cfg in registry_configs\n    ]\n\n    model_to_repo = {cfg[\"model_name\"]: cfg[\"hf_repo_id\"] for cfg in model_configs}\n    active_download_repos = {model_to_repo.get(name) for name in active_download_names if name in model_to_repo}\n\n    cache_info = None\n    if use_scan_cache:\n        try:\n            cache_info = scan_cache_dir()\n        except Exception:\n            pass\n\n    statuses = []\n\n    for config in model_configs:\n        try:\n            downloaded = False\n            size_mb = None\n            loaded = False\n\n            if cache_info:\n                repo_id = config[\"hf_repo_id\"]\n                for repo in cache_info.repos:\n                    if repo.repo_id == repo_id:\n                        has_model_weights = False\n                        for rev in repo.revisions:\n                            for f in rev.files:\n                                fname = f.file_name.lower()\n                                if fname.endswith((\".safetensors\", \".bin\", \".pt\", \".pth\", \".npz\")):\n                                    has_model_weights = True\n                                    break\n                            if has_model_weights:\n                                break\n\n                        has_incomplete = False\n                        try:\n                            cache_dir = hf_constants.HF_HUB_CACHE\n                            blobs_dir = Path(cache_dir) / (\"models--\" + repo_id.replace(\"/\", \"--\")) / \"blobs\"\n                            if blobs_dir.exists():\n                                has_incomplete = any(blobs_dir.glob(\"*.incomplete\"))\n                        except Exception:\n                            pass\n\n                        if has_model_weights and not has_incomplete:\n                            downloaded = True\n                            try:\n                                total_size = sum(revision.size_on_disk for revision in repo.revisions)\n                                size_mb = total_size / (1024 * 1024)\n                            except Exception:\n                                pass\n                        break\n\n            if not downloaded:\n                try:\n                    cache_dir = hf_constants.HF_HUB_CACHE\n                    repo_cache = Path(cache_dir) / (\"models--\" + config[\"hf_repo_id\"].replace(\"/\", \"--\"))\n\n                    if repo_cache.exists():\n                        blobs_dir = repo_cache / \"blobs\"\n                        has_incomplete = blobs_dir.exists() and any(blobs_dir.glob(\"*.incomplete\"))\n\n                        if not has_incomplete:\n                            snapshots_dir = repo_cache / \"snapshots\"\n                            has_model_files = False\n                            if snapshots_dir.exists():\n                                has_model_files = (\n                                    any(snapshots_dir.rglob(\"*.bin\"))\n                                    or any(snapshots_dir.rglob(\"*.safetensors\"))\n                                    or any(snapshots_dir.rglob(\"*.pt\"))\n                                    or any(snapshots_dir.rglob(\"*.pth\"))\n                                    or any(snapshots_dir.rglob(\"*.npz\"))\n                                )\n\n                            if has_model_files:\n                                downloaded = True\n                                try:\n                                    total_size = sum(\n                                        f.stat().st_size\n                                        for f in repo_cache.rglob(\"*\")\n                                        if f.is_file() and not f.name.endswith(\".incomplete\")\n                                    )\n                                    size_mb = total_size / (1024 * 1024)\n                                except Exception:\n                                    pass\n                except Exception:\n                    pass\n\n            try:\n                loaded = config[\"check_loaded\"]()\n            except Exception:\n                loaded = False\n\n            is_downloading = config[\"hf_repo_id\"] in active_download_repos\n\n            if is_downloading:\n                downloaded = False\n                size_mb = None\n\n            statuses.append(\n                models.ModelStatus(\n                    model_name=config[\"model_name\"],\n                    display_name=config[\"display_name\"],\n                    hf_repo_id=config[\"hf_repo_id\"],\n                    downloaded=downloaded,\n                    downloading=is_downloading,\n                    size_mb=size_mb,\n                    loaded=loaded,\n                )\n            )\n        except Exception:\n            try:\n                loaded = config[\"check_loaded\"]()\n            except Exception:\n                loaded = False\n\n            is_downloading = config[\"hf_repo_id\"] in active_download_repos\n\n            statuses.append(\n                models.ModelStatus(\n                    model_name=config[\"model_name\"],\n                    display_name=config[\"display_name\"],\n                    hf_repo_id=config[\"hf_repo_id\"],\n                    downloaded=False,\n                    downloading=is_downloading,\n                    size_mb=None,\n                    loaded=loaded,\n                )\n            )\n\n    return models.ModelStatusListResponse(models=statuses)\n\n\n@router.post(\"/models/download\")\nasync def trigger_model_download(request: models.ModelDownloadRequest):\n    \"\"\"Trigger download of a specific model.\"\"\"\n    from ..backends import get_model_config, get_model_load_func\n\n    task_manager = get_task_manager()\n    progress_manager = get_progress_manager()\n\n    config = get_model_config(request.model_name)\n    if not config:\n        raise HTTPException(status_code=400, detail=f\"Unknown model: {request.model_name}\")\n\n    load_func = get_model_load_func(config)\n\n    async def download_in_background():\n        try:\n            result = load_func()\n            if asyncio.iscoroutine(result):\n                await result\n            task_manager.complete_download(request.model_name)\n        except Exception as e:\n            task_manager.error_download(request.model_name, str(e))\n\n    task_manager.start_download(request.model_name)\n\n    progress_manager.update_progress(\n        model_name=request.model_name,\n        current=0,\n        total=0,\n        filename=\"Connecting to HuggingFace...\",\n        status=\"downloading\",\n    )\n\n    create_background_task(download_in_background())\n\n    return {\"message\": f\"Model {request.model_name} download started\"}\n\n\n@router.post(\"/models/download/cancel\")\nasync def cancel_model_download(request: models.ModelDownloadRequest):\n    \"\"\"Cancel or dismiss an errored/stale download task.\"\"\"\n    task_manager = get_task_manager()\n    progress_manager = get_progress_manager()\n\n    removed = task_manager.cancel_download(request.model_name)\n\n    progress_removed = False\n    with progress_manager._lock:\n        if request.model_name in progress_manager._progress:\n            del progress_manager._progress[request.model_name]\n            progress_removed = True\n\n    if removed or progress_removed:\n        return {\"message\": f\"Download task for {request.model_name} cancelled\"}\n    return {\"message\": f\"No active task found for {request.model_name}\"}\n\n\n@router.delete(\"/models/{model_name}\")\nasync def delete_model(model_name: str):\n    \"\"\"Delete a downloaded model from the HuggingFace cache.\"\"\"\n    from huggingface_hub import constants as hf_constants\n    from ..backends import get_model_config, unload_model_by_config\n\n    config = get_model_config(model_name)\n    if not config:\n        raise HTTPException(status_code=400, detail=f\"Unknown model: {model_name}\")\n\n    hf_repo_id = config.hf_repo_id\n\n    try:\n        unload_model_by_config(config)\n\n        cache_dir = hf_constants.HF_HUB_CACHE\n        repo_cache_dir = Path(cache_dir) / (\"models--\" + hf_repo_id.replace(\"/\", \"--\"))\n\n        if not repo_cache_dir.exists():\n            raise HTTPException(status_code=404, detail=f\"Model {model_name} not found in cache\")\n\n        try:\n            shutil.rmtree(repo_cache_dir)\n        except OSError as e:\n            raise HTTPException(status_code=500, detail=f\"Failed to delete model cache directory: {str(e)}\")\n\n        return {\"message\": f\"Model {model_name} deleted successfully\"}\n\n    except HTTPException:\n        raise\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=f\"Failed to delete model: {str(e)}\")\n"
  },
  {
    "path": "backend/routes/profiles.py",
    "content": "\"\"\"Voice profile endpoints.\"\"\"\n\nimport io\nimport json as _json\nimport logging\nimport tempfile\nimport uuid\nfrom datetime import datetime\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile\nfrom fastapi.responses import FileResponse, StreamingResponse\nfrom sqlalchemy.orm import Session\n\nfrom .. import models\nfrom ..app import safe_content_disposition\nfrom ..database import VoiceProfile as DBVoiceProfile, get_db\nfrom ..services import channels, export_import, profiles\nfrom ..services.profiles import _profile_to_response\n\nlogger = logging.getLogger(__name__)\n\nrouter = APIRouter()\n\n\n@router.post(\"/profiles\", response_model=models.VoiceProfileResponse)\nasync def create_profile(\n    data: models.VoiceProfileCreate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Create a new voice profile.\"\"\"\n    try:\n        return await profiles.create_profile(data, db)\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n    except Exception as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.get(\"/profiles\", response_model=list[models.VoiceProfileResponse])\nasync def list_profiles(db: Session = Depends(get_db)):\n    \"\"\"List all voice profiles.\"\"\"\n    return await profiles.list_profiles(db)\n\n\n@router.post(\"/profiles/import\", response_model=models.VoiceProfileResponse)\nasync def import_profile(\n    file: UploadFile = File(...),\n    db: Session = Depends(get_db),\n):\n    \"\"\"Import a voice profile from a ZIP archive.\"\"\"\n    MAX_FILE_SIZE = 100 * 1024 * 1024\n\n    content = await file.read()\n\n    if len(content) > MAX_FILE_SIZE:\n        raise HTTPException(\n            status_code=400, detail=f\"File too large. Maximum size is {MAX_FILE_SIZE / (1024 * 1024)}MB\"\n        )\n\n    try:\n        profile = await export_import.import_profile_from_zip(content, db)\n        return profile\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n# ── Preset Voice Endpoints ───────────────────────────────────────────\n# These MUST be declared before /profiles/{profile_id} to avoid the\n# wildcard swallowing \"presets\" as a profile_id.\n\n\n@router.get(\"/profiles/presets/{engine}\")\nasync def list_preset_voices(engine: str):\n    \"\"\"List available preset voices for an engine.\"\"\"\n    if engine == \"kokoro\":\n        from ..backends.kokoro_backend import KOKORO_VOICES\n\n        return {\n            \"engine\": engine,\n            \"voices\": [\n                {\n                    \"voice_id\": vid,\n                    \"name\": name,\n                    \"gender\": gender,\n                    \"language\": lang,\n                }\n                for vid, name, gender, lang in KOKORO_VOICES\n            ],\n        }\n    return {\"engine\": engine, \"voices\": []}\n\n\n@router.post(\"/profiles/presets/{engine}/seed\")\nasync def seed_preset_profiles_route(\n    engine: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Seed preset voice profiles for an engine.\n\n    Creates profiles for all available preset voices that don't already exist.\n    Returns the count of newly created profiles.\n    \"\"\"\n    if engine != \"kokoro\":\n        raise HTTPException(status_code=400, detail=f\"No presets available for engine: {engine}\")\n\n    try:\n        from ..backends.kokoro_backend import KOKORO_VOICES\n\n        created = 0\n        for voice_id, display_name, gender, lang in KOKORO_VOICES:\n            profile_name = display_name\n\n            # Disambiguate duplicate display names across languages\n            # (e.g. \"Alpha\" exists in Hindi and Japanese, \"Dora\" in Spanish and Portuguese)\n            dupes = [v for v in KOKORO_VOICES if v[1] == display_name]\n            if len(dupes) > 1:\n                lang_labels = {\"en\": \"English\", \"es\": \"Spanish\", \"fr\": \"French\", \"hi\": \"Hindi\",\n                               \"it\": \"Italian\", \"pt\": \"Portuguese\", \"ja\": \"Japanese\", \"zh\": \"Chinese\"}\n                profile_name = f\"{display_name} {lang_labels.get(lang, lang)}\"\n\n            # Skip if preset already exists\n            existing = (\n                db.query(DBVoiceProfile)\n                .filter_by(preset_engine=\"kokoro\", preset_voice_id=voice_id)\n                .first()\n            )\n            if existing:\n                continue\n\n            unique_name = profile_name\n            suffix = 2\n            while db.query(DBVoiceProfile).filter_by(name=unique_name).first():\n                unique_name = f\"{profile_name} {suffix}\"\n                suffix += 1\n\n            profile = DBVoiceProfile(\n                id=str(uuid.uuid4()),\n                name=unique_name,\n                description=f\"Kokoro preset voice — {display_name} ({gender})\",\n                language=lang,\n                voice_type=\"preset\",\n                preset_engine=\"kokoro\",\n                preset_voice_id=voice_id,\n                created_at=datetime.utcnow(),\n                updated_at=datetime.utcnow(),\n            )\n            db.add(profile)\n            created += 1\n\n        if created > 0:\n            db.commit()\n            logger.info(f\"Seeded {created} Kokoro preset profiles\")\n\n        return {\"engine\": engine, \"created\": created, \"total_available\": len(KOKORO_VOICES)}\n    except Exception as e:\n        logger.exception(f\"Failed to seed Kokoro profiles: {e}\")\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@router.get(\"/profiles/{profile_id}\", response_model=models.VoiceProfileResponse)\nasync def get_profile(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get a voice profile by ID.\"\"\"\n    profile = await profiles.get_profile(profile_id, db)\n    if not profile:\n        raise HTTPException(status_code=404, detail=\"Profile not found\")\n    return profile\n\n\n@router.put(\"/profiles/{profile_id}\", response_model=models.VoiceProfileResponse)\nasync def update_profile(\n    profile_id: str,\n    data: models.VoiceProfileCreate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Update a voice profile.\"\"\"\n    try:\n        profile = await profiles.update_profile(profile_id, data, db)\n        if not profile:\n            raise HTTPException(status_code=404, detail=\"Profile not found\")\n        return profile\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.delete(\"/profiles/{profile_id}\")\nasync def delete_profile(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete a voice profile.\"\"\"\n    success = await profiles.delete_profile(profile_id, db)\n    if not success:\n        raise HTTPException(status_code=404, detail=\"Profile not found\")\n    return {\"message\": \"Profile deleted successfully\"}\n\n\nSAMPLE_MAX_FILE_SIZE = 50 * 1024 * 1024  # 50 MB\nSAMPLE_UPLOAD_CHUNK_SIZE = 1024 * 1024  # 1 MB\n\n\n@router.post(\"/profiles/{profile_id}/samples\", response_model=models.ProfileSampleResponse)\nasync def add_profile_sample(\n    profile_id: str,\n    file: UploadFile = File(...),\n    reference_text: str = Form(...),\n    db: Session = Depends(get_db),\n):\n    \"\"\"Add a sample to a voice profile.\"\"\"\n    _allowed_audio_exts = {\".wav\", \".mp3\", \".m4a\", \".ogg\", \".flac\", \".aac\", \".webm\", \".opus\"}\n    _uploaded_ext = Path(file.filename or \"\").suffix.lower()\n    file_suffix = _uploaded_ext if _uploaded_ext in _allowed_audio_exts else \".wav\"\n\n    with tempfile.NamedTemporaryFile(suffix=file_suffix, delete=False) as tmp:\n        total_size = 0\n        while chunk := await file.read(SAMPLE_UPLOAD_CHUNK_SIZE):\n            total_size += len(chunk)\n            if total_size > SAMPLE_MAX_FILE_SIZE:\n                Path(tmp.name).unlink(missing_ok=True)\n                raise HTTPException(\n                    status_code=413,\n                    detail=f\"File too large (max {SAMPLE_MAX_FILE_SIZE // (1024 * 1024)} MB)\",\n                )\n            tmp.write(chunk)\n        tmp_path = tmp.name\n\n    try:\n        sample = await profiles.add_profile_sample(\n            profile_id,\n            tmp_path,\n            reference_text,\n            db,\n        )\n        return sample\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=f\"Failed to process audio file: {str(e)}\")\n    finally:\n        Path(tmp_path).unlink(missing_ok=True)\n\n\n@router.get(\"/profiles/{profile_id}/samples\", response_model=list[models.ProfileSampleResponse])\nasync def get_profile_samples(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get all samples for a profile.\"\"\"\n    return await profiles.get_profile_samples(profile_id, db)\n\n\n@router.delete(\"/profiles/samples/{sample_id}\")\nasync def delete_profile_sample(\n    sample_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete a profile sample.\"\"\"\n    success = await profiles.delete_profile_sample(sample_id, db)\n    if not success:\n        raise HTTPException(status_code=404, detail=\"Sample not found\")\n    return {\"message\": \"Sample deleted successfully\"}\n\n\n@router.put(\"/profiles/samples/{sample_id}\", response_model=models.ProfileSampleResponse)\nasync def update_profile_sample(\n    sample_id: str,\n    data: models.ProfileSampleUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Update a profile sample's reference text.\"\"\"\n    sample = await profiles.update_profile_sample(sample_id, data.reference_text, db)\n    if not sample:\n        raise HTTPException(status_code=404, detail=\"Sample not found\")\n    return sample\n\n\n@router.post(\"/profiles/{profile_id}/avatar\", response_model=models.VoiceProfileResponse)\nasync def upload_profile_avatar(\n    profile_id: str,\n    file: UploadFile = File(...),\n    db: Session = Depends(get_db),\n):\n    \"\"\"Upload or update avatar image for a profile.\"\"\"\n    with tempfile.NamedTemporaryFile(delete=False, suffix=Path(file.filename).suffix) as tmp:\n        content = await file.read()\n        tmp.write(content)\n        tmp_path = tmp.name\n\n    try:\n        profile = await profiles.upload_avatar(profile_id, tmp_path, db)\n        return profile\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n    finally:\n        Path(tmp_path).unlink(missing_ok=True)\n\n\n@router.get(\"/profiles/{profile_id}/avatar\")\nasync def get_profile_avatar(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get avatar image for a profile.\"\"\"\n    profile = await profiles.get_profile(profile_id, db)\n    if not profile:\n        raise HTTPException(status_code=404, detail=\"Profile not found\")\n\n    if not profile.avatar_path:\n        raise HTTPException(status_code=404, detail=\"No avatar found for this profile\")\n\n    avatar_path = Path(profile.avatar_path)\n    if not avatar_path.exists():\n        raise HTTPException(status_code=404, detail=\"Avatar file not found\")\n\n    return FileResponse(avatar_path)\n\n\n@router.delete(\"/profiles/{profile_id}/avatar\")\nasync def delete_profile_avatar(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete avatar image for a profile.\"\"\"\n    success = await profiles.delete_avatar(profile_id, db)\n    if not success:\n        raise HTTPException(status_code=404, detail=\"Profile not found or no avatar to delete\")\n    return {\"message\": \"Avatar deleted successfully\"}\n\n\n@router.get(\"/profiles/{profile_id}/export\")\nasync def export_profile(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Export a voice profile as a ZIP archive.\"\"\"\n    try:\n        profile = await profiles.get_profile(profile_id, db)\n        if not profile:\n            raise HTTPException(status_code=404, detail=\"Profile not found\")\n\n        zip_bytes = export_import.export_profile_to_zip(profile_id, db)\n\n        safe_name = \"\".join(c for c in profile.name if c.isalnum() or c in (\" \", \"-\", \"_\")).strip()\n        if not safe_name:\n            safe_name = \"profile\"\n        filename = f\"profile-{safe_name}.voicebox.zip\"\n\n        return StreamingResponse(\n            io.BytesIO(zip_bytes),\n            media_type=\"application/zip\",\n            headers={\"Content-Disposition\": safe_content_disposition(\"attachment\", filename)},\n        )\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n\n\n@router.get(\"/profiles/{profile_id}/channels\")\nasync def get_profile_channels(\n    profile_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get list of channel IDs assigned to a profile.\"\"\"\n    try:\n        channel_ids = await channels.get_profile_channels(profile_id, db)\n        return {\"channel_ids\": channel_ids}\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.put(\"/profiles/{profile_id}/channels\")\nasync def set_profile_channels(\n    profile_id: str,\n    data: models.ProfileChannelAssignment,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Set which channels a profile is assigned to.\"\"\"\n    try:\n        await channels.set_profile_channels(profile_id, data, db)\n        return {\"message\": \"Profile channels updated successfully\"}\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.put(\"/profiles/{profile_id}/effects\", response_model=models.VoiceProfileResponse)\nasync def update_profile_effects(\n    profile_id: str,\n    data: models.ProfileEffectsUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Set or clear the default effects chain for a voice profile.\"\"\"\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        raise HTTPException(status_code=404, detail=\"Profile not found\")\n\n    if data.effects_chain is not None:\n        from ..utils.effects import validate_effects_chain\n\n        chain_dicts = [e.model_dump() for e in data.effects_chain]\n        error = validate_effects_chain(chain_dicts)\n        if error:\n            raise HTTPException(status_code=400, detail=error)\n        profile.effects_chain = _json.dumps(chain_dicts)\n    else:\n        profile.effects_chain = None\n\n    profile.updated_at = datetime.utcnow()\n    db.commit()\n    db.refresh(profile)\n\n    return _profile_to_response(profile)\n"
  },
  {
    "path": "backend/routes/stories.py",
    "content": "\"\"\"Story endpoints.\"\"\"\n\nimport io\n\nfrom fastapi import APIRouter, Depends, HTTPException\nfrom fastapi.responses import StreamingResponse\nfrom sqlalchemy.orm import Session\n\nfrom .. import database, models\nfrom ..services import stories\nfrom ..app import safe_content_disposition\nfrom ..database import get_db\n\nrouter = APIRouter()\n\n\n@router.get(\"/stories\", response_model=list[models.StoryResponse])\nasync def list_stories(db: Session = Depends(get_db)):\n    \"\"\"List all stories.\"\"\"\n    return await stories.list_stories(db)\n\n\n@router.post(\"/stories\", response_model=models.StoryResponse)\nasync def create_story(\n    data: models.StoryCreate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Create a new story.\"\"\"\n    try:\n        return await stories.create_story(data, db)\n    except Exception as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n\n@router.get(\"/stories/{story_id}\", response_model=models.StoryDetailResponse)\nasync def get_story(\n    story_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Get a story with all its items.\"\"\"\n    story = await stories.get_story(story_id, db)\n    if not story:\n        raise HTTPException(status_code=404, detail=\"Story not found\")\n    return story\n\n\n@router.put(\"/stories/{story_id}\", response_model=models.StoryResponse)\nasync def update_story(\n    story_id: str,\n    data: models.StoryCreate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Update a story.\"\"\"\n    story = await stories.update_story(story_id, data, db)\n    if not story:\n        raise HTTPException(status_code=404, detail=\"Story not found\")\n    return story\n\n\n@router.delete(\"/stories/{story_id}\")\nasync def delete_story(\n    story_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Delete a story.\"\"\"\n    success = await stories.delete_story(story_id, db)\n    if not success:\n        raise HTTPException(status_code=404, detail=\"Story not found\")\n    return {\"message\": \"Story deleted successfully\"}\n\n\n@router.post(\"/stories/{story_id}/items\", response_model=models.StoryItemDetail)\nasync def add_story_item(\n    story_id: str,\n    data: models.StoryItemCreate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Add a generation to a story.\"\"\"\n    item = await stories.add_item_to_story(story_id, data, db)\n    if not item:\n        raise HTTPException(status_code=404, detail=\"Story or generation not found\")\n    return item\n\n\n@router.delete(\"/stories/{story_id}/items/{item_id}\")\nasync def remove_story_item(\n    story_id: str,\n    item_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Remove a story item from a story.\"\"\"\n    success = await stories.remove_item_from_story(story_id, item_id, db)\n    if not success:\n        raise HTTPException(status_code=404, detail=\"Story item not found\")\n    return {\"message\": \"Item removed successfully\"}\n\n\n@router.put(\"/stories/{story_id}/items/times\")\nasync def update_story_item_times(\n    story_id: str,\n    data: models.StoryItemBatchUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Update story item timecodes.\"\"\"\n    success = await stories.update_story_item_times(story_id, data, db)\n    if not success:\n        raise HTTPException(status_code=400, detail=\"Invalid timecode update request\")\n    return {\"message\": \"Item timecodes updated successfully\"}\n\n\n@router.put(\"/stories/{story_id}/items/reorder\", response_model=list[models.StoryItemDetail])\nasync def reorder_story_items(\n    story_id: str,\n    data: models.StoryItemReorder,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Reorder story items and recalculate timecodes.\"\"\"\n    items = await stories.reorder_story_items(story_id, data.generation_ids, db)\n    if items is None:\n        raise HTTPException(\n            status_code=400, detail=\"Invalid reorder request - ensure all generation IDs belong to this story\"\n        )\n    return items\n\n\n@router.put(\"/stories/{story_id}/items/{item_id}/move\", response_model=models.StoryItemDetail)\nasync def move_story_item(\n    story_id: str,\n    item_id: str,\n    data: models.StoryItemMove,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Move a story item (update position and/or track).\"\"\"\n    item = await stories.move_story_item(story_id, item_id, data, db)\n    if item is None:\n        raise HTTPException(status_code=404, detail=\"Story item not found\")\n    return item\n\n\n@router.put(\"/stories/{story_id}/items/{item_id}/trim\", response_model=models.StoryItemDetail)\nasync def trim_story_item(\n    story_id: str,\n    item_id: str,\n    data: models.StoryItemTrim,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Trim a story item.\"\"\"\n    item = await stories.trim_story_item(story_id, item_id, data, db)\n    if item is None:\n        raise HTTPException(status_code=404, detail=\"Story item not found or invalid trim values\")\n    return item\n\n\n@router.post(\"/stories/{story_id}/items/{item_id}/split\", response_model=list[models.StoryItemDetail])\nasync def split_story_item(\n    story_id: str,\n    item_id: str,\n    data: models.StoryItemSplit,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Split a story item at a given time, creating two clips.\"\"\"\n    items = await stories.split_story_item(story_id, item_id, data, db)\n    if items is None:\n        raise HTTPException(status_code=404, detail=\"Story item not found or invalid split point\")\n    return items\n\n\n@router.post(\"/stories/{story_id}/items/{item_id}/duplicate\", response_model=models.StoryItemDetail)\nasync def duplicate_story_item(\n    story_id: str,\n    item_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Duplicate a story item.\"\"\"\n    item = await stories.duplicate_story_item(story_id, item_id, db)\n    if item is None:\n        raise HTTPException(status_code=404, detail=\"Story item not found\")\n    return item\n\n\n@router.put(\"/stories/{story_id}/items/{item_id}/version\", response_model=models.StoryItemDetail)\nasync def set_story_item_version(\n    story_id: str,\n    item_id: str,\n    data: models.StoryItemVersionUpdate,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Pin a story item to a specific generation version.\"\"\"\n    item = await stories.set_story_item_version(story_id, item_id, data, db)\n    if item is None:\n        raise HTTPException(status_code=404, detail=\"Story item or version not found\")\n    return item\n\n\n@router.get(\"/stories/{story_id}/export-audio\")\nasync def export_story_audio(\n    story_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Export story as single mixed audio file.\"\"\"\n    try:\n        story = db.query(database.Story).filter_by(id=story_id).first()\n        if not story:\n            raise HTTPException(status_code=404, detail=\"Story not found\")\n\n        audio_bytes = await stories.export_story_audio(story_id, db)\n        if not audio_bytes:\n            raise HTTPException(status_code=400, detail=\"Story has no audio items\")\n\n        safe_name = \"\".join(c for c in story.name if c.isalnum() or c in (\" \", \"-\", \"_\")).strip()\n        if not safe_name:\n            safe_name = \"story\"\n        filename = f\"{safe_name}.wav\"\n\n        return StreamingResponse(\n            io.BytesIO(audio_bytes),\n            media_type=\"audio/wav\",\n            headers={\"Content-Disposition\": safe_content_disposition(\"attachment\", filename)},\n        )\n    except HTTPException:\n        raise\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n"
  },
  {
    "path": "backend/routes/tasks.py",
    "content": "\"\"\"Task and cache management endpoints.\"\"\"\n\nfrom datetime import datetime\n\nfrom fastapi import APIRouter\n\nfrom .. import models\nfrom ..utils.cache import clear_voice_prompt_cache\nfrom ..utils.progress import get_progress_manager\nfrom ..utils.tasks import get_task_manager\nfrom fastapi import HTTPException\n\nrouter = APIRouter()\n\n\n@router.post(\"/tasks/clear\")\nasync def clear_all_tasks():\n    \"\"\"Clear all download tasks and progress state.\"\"\"\n    task_manager = get_task_manager()\n    progress_manager = get_progress_manager()\n\n    task_manager.clear_all()\n\n    with progress_manager._lock:\n        progress_manager._progress.clear()\n        progress_manager._last_notify_time.clear()\n        progress_manager._last_notify_progress.clear()\n\n    return {\"message\": \"All task state cleared\"}\n\n\n@router.post(\"/cache/clear\")\nasync def clear_cache():\n    \"\"\"Clear all voice prompt caches (memory and disk).\"\"\"\n    try:\n        deleted_count = clear_voice_prompt_cache()\n        return {\n            \"message\": \"Voice prompt cache cleared successfully\",\n            \"files_deleted\": deleted_count,\n        }\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=f\"Failed to clear cache: {str(e)}\")\n\n\n@router.get(\"/tasks/active\", response_model=models.ActiveTasksResponse)\nasync def get_active_tasks():\n    \"\"\"Return all currently active downloads and generations.\"\"\"\n    task_manager = get_task_manager()\n    progress_manager = get_progress_manager()\n\n    active_downloads = []\n    task_manager_downloads = task_manager.get_active_downloads()\n    progress_active = progress_manager.get_all_active()\n\n    download_map = {task.model_name: task for task in task_manager_downloads}\n    progress_map = {p[\"model_name\"]: p for p in progress_active}\n\n    all_model_names = set(download_map.keys()) | set(progress_map.keys())\n    for model_name in all_model_names:\n        task = download_map.get(model_name)\n        progress = progress_map.get(model_name)\n\n        if task:\n            error = task.error\n            if not error:\n                with progress_manager._lock:\n                    pm_data = progress_manager._progress.get(model_name)\n                    if pm_data:\n                        error = pm_data.get(\"error\")\n            prog = progress or {}\n            if not prog:\n                with progress_manager._lock:\n                    pm_data = progress_manager._progress.get(model_name)\n                    if pm_data:\n                        prog = pm_data\n            active_downloads.append(\n                models.ActiveDownloadTask(\n                    model_name=model_name,\n                    status=task.status,\n                    started_at=task.started_at,\n                    error=error,\n                    progress=prog.get(\"progress\"),\n                    current=prog.get(\"current\"),\n                    total=prog.get(\"total\"),\n                    filename=prog.get(\"filename\"),\n                )\n            )\n        elif progress:\n            timestamp_str = progress.get(\"timestamp\")\n            if timestamp_str:\n                try:\n                    started_at = datetime.fromisoformat(timestamp_str.replace(\"Z\", \"+00:00\"))\n                except (ValueError, AttributeError):\n                    started_at = datetime.utcnow()\n            else:\n                started_at = datetime.utcnow()\n\n            active_downloads.append(\n                models.ActiveDownloadTask(\n                    model_name=model_name,\n                    status=progress.get(\"status\", \"downloading\"),\n                    started_at=started_at,\n                    error=progress.get(\"error\"),\n                    progress=progress.get(\"progress\"),\n                    current=progress.get(\"current\"),\n                    total=progress.get(\"total\"),\n                    filename=progress.get(\"filename\"),\n                )\n            )\n\n    active_generations = []\n    for gen_task in task_manager.get_active_generations():\n        active_generations.append(\n            models.ActiveGenerationTask(\n                task_id=gen_task.task_id,\n                profile_id=gen_task.profile_id,\n                text_preview=gen_task.text_preview,\n                started_at=gen_task.started_at,\n            )\n        )\n\n    return models.ActiveTasksResponse(\n        downloads=active_downloads,\n        generations=active_generations,\n    )\n"
  },
  {
    "path": "backend/routes/transcription.py",
    "content": "\"\"\"Transcription endpoints.\"\"\"\n\nimport asyncio\nimport tempfile\nfrom pathlib import Path\n\nfrom fastapi import APIRouter, File, Form, HTTPException, UploadFile\n\nfrom .. import models\nfrom ..services import transcribe\nfrom ..services.task_queue import create_background_task\nfrom ..utils.tasks import get_task_manager\n\nrouter = APIRouter()\n\nUPLOAD_CHUNK_SIZE = 1024 * 1024  # 1MB\n\n\n@router.post(\"/transcribe\", response_model=models.TranscriptionResponse)\nasync def transcribe_audio(\n    file: UploadFile = File(...),\n    language: str | None = Form(None),\n    model: str | None = Form(None),\n):\n    \"\"\"Transcribe audio file to text.\"\"\"\n    with tempfile.NamedTemporaryFile(suffix=\".wav\", delete=False) as tmp:\n        while chunk := await file.read(UPLOAD_CHUNK_SIZE):\n            tmp.write(chunk)\n        tmp_path = tmp.name\n\n    try:\n        from ..utils.audio import load_audio\n        from ..backends import WHISPER_HF_REPOS\n\n        audio, sr = await asyncio.to_thread(load_audio, tmp_path)\n        duration = len(audio) / sr\n\n        whisper_model = transcribe.get_whisper_model()\n        model_size = model if model else whisper_model.model_size\n\n        valid_sizes = list(WHISPER_HF_REPOS.keys())\n        if model_size not in valid_sizes:\n            raise HTTPException(\n                status_code=400,\n                detail=f\"Invalid model size '{model_size}'. Must be one of: {', '.join(valid_sizes)}\",\n            )\n\n        already_loaded = whisper_model.is_loaded() and whisper_model.model_size == model_size\n        if not already_loaded and not whisper_model._is_model_cached(model_size):\n            progress_model_name = f\"whisper-{model_size}\"\n            task_manager = get_task_manager()\n\n            async def download_whisper_background():\n                try:\n                    await whisper_model.load_model_async(model_size)\n                    task_manager.complete_download(progress_model_name)\n                except Exception as e:\n                    task_manager.error_download(progress_model_name, str(e))\n\n            task_manager.start_download(progress_model_name)\n            create_background_task(download_whisper_background())\n\n            raise HTTPException(\n                status_code=202,\n                detail={\n                    \"message\": f\"Whisper model {model_size} is being downloaded. Please wait and try again.\",\n                    \"model_name\": progress_model_name,\n                    \"downloading\": True,\n                },\n            )\n\n        text = await whisper_model.transcribe(tmp_path, language, model_size)\n\n        return models.TranscriptionResponse(\n            text=text,\n            duration=duration,\n        )\n\n    except HTTPException:\n        raise\n    except Exception as e:\n        raise HTTPException(status_code=500, detail=str(e))\n    finally:\n        Path(tmp_path).unlink(missing_ok=True)\n"
  },
  {
    "path": "backend/server.py",
    "content": "\"\"\"\nEntry point for PyInstaller-bundled voicebox server.\n\nThis module provides an entry point that works with PyInstaller by using\nabsolute imports instead of relative imports.\n\"\"\"\n\nimport sys\nimport os\n\n# On Windows with --noconsole (PyInstaller), sys.stdout/stderr are None.\n# They can also be broken file objects in some edge cases.\n# Redirect to devnull to prevent crashes from print()/tqdm/logging.\ndef _is_writable(stream):\n    \"\"\"Check if a stream is usable for writing.\"\"\"\n    if stream is None:\n        return False\n    try:\n        stream.write(\"\")\n        return True\n    except Exception:\n        return False\n\nif not _is_writable(sys.stdout):\n    sys.stdout = open(os.devnull, 'w')\nif not _is_writable(sys.stderr):\n    sys.stderr = open(os.devnull, 'w')\n\n# PyInstaller + multiprocessing: child processes re-execute the frozen binary\n# with internal arguments. freeze_support() handles this and exits early.\nimport multiprocessing\nmultiprocessing.freeze_support()\n\n# In frozen builds, piper_phonemize's espeak-ng C library falls back to\n# /usr/share/espeak-ng-data/ which doesn't exist.  Point it at the bundled\n# data directory instead.\nif getattr(sys, 'frozen', False):\n    _meipass = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))\n    _espeak_data = os.path.join(_meipass, 'piper_phonemize', 'espeak-ng-data')\n    if os.path.isdir(_espeak_data):\n        os.environ.setdefault('ESPEAK_DATA_PATH', _espeak_data)\n\n# Fast path: handle --version before any heavy imports so the Rust\n# version check doesn't block for 30+ seconds loading torch etc.\nif \"--version\" in sys.argv:\n    from backend import __version__\n    print(f\"voicebox-server {__version__}\")\n    sys.exit(0)\n\nimport logging\n\n# Set up logging FIRST, before any imports that might fail\nlogging.basicConfig(\n    level=logging.INFO,\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n    stream=sys.stderr,  # Log to stderr so it's captured by Tauri\n)\nlogger = logging.getLogger(__name__)\n\n# Log startup immediately to confirm binary execution\nlogger.info(\"=\" * 60)\nlogger.info(\"voicebox-server starting up...\")\nlogger.info(f\"Python version: {sys.version}\")\nlogger.info(f\"Executable: {sys.executable}\")\nlogger.info(f\"Arguments: {sys.argv}\")\nlogger.info(\"=\" * 60)\n\ntry:\n    logger.info(\"Importing argparse...\")\n    import argparse\n    logger.info(\"Importing uvicorn...\")\n    import uvicorn\n    logger.info(\"Standard library imports successful\")\n\n    # Import the FastAPI app from the backend package\n    logger.info(\"Importing backend.config...\")\n    from backend import config\n    logger.info(\"Importing backend.database...\")\n    from backend import database\n    logger.info(\"Importing backend.main (this may take a while due to torch/transformers)...\")\n    from backend.main import app\n    logger.info(\"Backend imports successful\")\nexcept Exception as e:\n    logger.error(f\"Failed to import required modules: {e}\", exc_info=True)\n    sys.exit(1)\n\n_watchdog_disabled = False\n\n\ndef disable_watchdog():\n    \"\"\"Disable the parent watchdog so the server keeps running after parent exits.\"\"\"\n    global _watchdog_disabled\n    _watchdog_disabled = True\n    # Ignore SIGHUP so the server survives when the parent Tauri process exits.\n    # On Unix, child processes receive SIGHUP when the parent's session leader\n    # exits, which would kill the server even though we want it to persist.\n    if sys.platform != \"win32\":\n        import signal\n        signal.signal(signal.SIGHUP, signal.SIG_IGN)\n\n\ndef _start_parent_watchdog(parent_pid, data_dir=None):\n    \"\"\"Monitor parent process and exit if it dies.\n\n    This is the clean shutdown mechanism: instead of the Tauri app trying to\n    forcefully kill the server (which spawns console windows on Windows),\n    the server monitors its parent and shuts itself down gracefully.\n    \"\"\"\n    import os\n    import signal\n    import threading\n    import time\n\n    # Set up a file logger so we can debug in production\n    watchdog_logger = logging.getLogger(\"watchdog\")\n    if data_dir:\n        try:\n            log_dir = os.path.join(data_dir, \"logs\")\n            os.makedirs(log_dir, exist_ok=True)\n            fh = logging.FileHandler(os.path.join(log_dir, \"watchdog.log\"))\n            fh.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))\n            watchdog_logger.addHandler(fh)\n        except Exception:\n            pass\n    watchdog_logger.setLevel(logging.INFO)\n\n    def _is_pid_alive(pid):\n        \"\"\"Check if a process with the given PID exists (cross-platform).\"\"\"\n        try:\n            if sys.platform == \"win32\":\n                import ctypes\n                kernel32 = ctypes.windll.kernel32\n                PROCESS_QUERY_LIMITED_INFORMATION = 0x1000\n                handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)\n                if handle:\n                    # Check if process has actually exited\n                    STILL_ACTIVE = 259\n                    exit_code = ctypes.c_ulong()\n                    result = kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code))\n                    kernel32.CloseHandle(handle)\n                    if result and exit_code.value == STILL_ACTIVE:\n                        return True\n                    watchdog_logger.info(f\"PID {pid}: exited with code {exit_code.value}\")\n                    return False\n                # OpenProcess failed — check if it's an access error (process exists\n                # but we can't open it) vs process not found\n                error = ctypes.GetLastError()\n                ACCESS_DENIED = 5\n                if error == ACCESS_DENIED:\n                    return True  # process exists, we just can't open it\n                watchdog_logger.info(f\"PID {pid}: OpenProcess failed, error={error}\")\n                return False\n            else:\n                os.kill(pid, 0)\n                return True\n        except (OSError, PermissionError):\n            return False\n\n    def _watch():\n        watchdog_logger.info(f\"Parent watchdog started, monitoring PID {parent_pid}, server PID {os.getpid()}\")\n        # Verify parent is alive before starting the loop\n        alive = _is_pid_alive(parent_pid)\n        watchdog_logger.info(f\"Parent PID {parent_pid} initial check: alive={alive}\")\n        if not alive:\n            watchdog_logger.warning(f\"Parent PID {parent_pid} not found on first check — disabling watchdog\")\n            return\n        while True:\n            if _watchdog_disabled:\n                watchdog_logger.info(\"Watchdog disabled (keep server running), stopping monitor\")\n                return\n            if not _is_pid_alive(parent_pid):\n                # Parent is gone. Before shutting down, give the app a moment\n                # to send /watchdog/disable — there is a race where the Tauri\n                # RunEvent::Exit handler sends the disable request while we are\n                # mid-iteration (already past the _watchdog_disabled check above).\n                watchdog_logger.info(f\"Parent process {parent_pid} gone, waiting for possible disable request...\")\n                time.sleep(1)\n                if _watchdog_disabled:\n                    watchdog_logger.info(\"Watchdog was disabled during grace period, keeping server alive\")\n                    return\n                watchdog_logger.info(\"Watchdog still enabled after grace period, shutting down server...\")\n                if sys.platform == \"win32\":\n                    # sys.exit triggers SystemExit, allowing uvicorn to run\n                    # shutdown handlers. os.kill(SIGTERM) on Windows calls\n                    # TerminateProcess which hard-kills without cleanup.\n                    os._exit(0)\n                else:\n                    os.kill(os.getpid(), signal.SIGTERM)\n                return\n            time.sleep(2)\n\n    t = threading.Thread(target=_watch, daemon=True)\n    t.start()\n\n\nif __name__ == \"__main__\":\n    try:\n        parser = argparse.ArgumentParser(description=\"voicebox backend server\")\n        parser.add_argument(\n            \"--host\",\n            type=str,\n            default=\"127.0.0.1\",\n            help=\"Host to bind to (use 0.0.0.0 for remote access)\",\n        )\n        parser.add_argument(\n            \"--port\",\n            type=int,\n            default=8000,\n            help=\"Port to bind to\",\n        )\n        parser.add_argument(\n            \"--data-dir\",\n            type=str,\n            default=None,\n            help=\"Data directory for database, profiles, and generated audio\",\n        )\n        parser.add_argument(\n            \"--parent-pid\",\n            type=int,\n            default=None,\n            help=\"PID of parent process to monitor; server exits when parent dies\",\n        )\n        parser.add_argument(\n            \"--version\",\n            action=\"store_true\",\n            help=\"Print version and exit (handled above, kept for argparse help)\",\n        )\n        args = parser.parse_args()\n\n        if args.parent_pid is not None and args.parent_pid <= 0:\n            parser.error(\"--parent-pid must be a positive integer\")\n\n        # Detect backend variant from binary name\n        # voicebox-server-cuda → sets VOICEBOX_BACKEND_VARIANT=cuda\n        import os\n        binary_name = os.path.basename(sys.executable).lower()\n        if \"cuda\" in binary_name:\n            os.environ[\"VOICEBOX_BACKEND_VARIANT\"] = \"cuda\"\n            logger.info(\"Backend variant: CUDA\")\n        else:\n            os.environ[\"VOICEBOX_BACKEND_VARIANT\"] = \"cpu\"\n            logger.info(\"Backend variant: CPU\")\n\n        # Register parent watchdog to start after server is fully ready\n        if args.parent_pid is not None:\n            _parent_pid = args.parent_pid\n            _data_dir = args.data_dir\n            @app.on_event(\"startup\")\n            async def _on_startup():\n                _start_parent_watchdog(_parent_pid, _data_dir)\n\n        logger.info(f\"Parsed arguments: host={args.host}, port={args.port}, data_dir={args.data_dir}\")\n\n        # Set data directory if provided\n        if args.data_dir:\n            logger.info(f\"Setting data directory to: {args.data_dir}\")\n            config.set_data_dir(args.data_dir)\n\n        # Initialize database after data directory is set\n        logger.info(\"Initializing database...\")\n        database.init_db()\n        logger.info(\"Database initialized successfully\")\n\n        logger.info(f\"Starting uvicorn server on {args.host}:{args.port}...\")\n        uvicorn.run(\n            app,\n            host=args.host,\n            port=args.port,\n            log_level=\"info\",\n        )\n    except Exception as e:\n        logger.error(f\"Server startup failed: {e}\", exc_info=True)\n        sys.exit(1)\n"
  },
  {
    "path": "backend/services/__init__.py",
    "content": "# Services layer — generation orchestration and background task management.\n"
  },
  {
    "path": "backend/services/channels.py",
    "content": "\"\"\"\nAudio channel management module.\n\"\"\"\n\nfrom typing import List, Optional\nfrom datetime import datetime\nimport uuid\nfrom sqlalchemy.orm import Session\n\nfrom ..models import (\n    AudioChannelCreate,\n    AudioChannelUpdate,\n    AudioChannelResponse,\n    ChannelVoiceAssignment,\n    ProfileChannelAssignment,\n)\nfrom ..database import (\n    AudioChannel as DBAudioChannel,\n    ChannelDeviceMapping as DBChannelDeviceMapping,\n    ProfileChannelMapping as DBProfileChannelMapping,\n    VoiceProfile as DBVoiceProfile,\n)\n\n\nasync def list_channels(db: Session) -> List[AudioChannelResponse]:\n    \"\"\"List all audio channels.\"\"\"\n    channels = db.query(DBAudioChannel).all()\n    result = []\n    \n    for channel in channels:\n        # Get device IDs for this channel\n        device_mappings = db.query(DBChannelDeviceMapping).filter_by(\n            channel_id=channel.id\n        ).all()\n        device_ids = [m.device_id for m in device_mappings]\n        \n        result.append(AudioChannelResponse(\n            id=channel.id,\n            name=channel.name,\n            is_default=channel.is_default,\n            device_ids=device_ids,\n            created_at=channel.created_at,\n        ))\n    \n    return result\n\n\nasync def get_channel(channel_id: str, db: Session) -> Optional[AudioChannelResponse]:\n    \"\"\"Get a channel by ID.\"\"\"\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    if not channel:\n        return None\n    \n    # Get device IDs\n    device_mappings = db.query(DBChannelDeviceMapping).filter_by(\n        channel_id=channel.id\n    ).all()\n    device_ids = [m.device_id for m in device_mappings]\n    \n    return AudioChannelResponse(\n        id=channel.id,\n        name=channel.name,\n        is_default=channel.is_default,\n        device_ids=device_ids,\n        created_at=channel.created_at,\n    )\n\n\nasync def create_channel(\n    data: AudioChannelCreate,\n    db: Session,\n) -> AudioChannelResponse:\n    \"\"\"Create a new audio channel.\"\"\"\n    # Check if name already exists\n    existing = db.query(DBAudioChannel).filter_by(name=data.name).first()\n    if existing:\n        raise ValueError(f\"Channel with name '{data.name}' already exists\")\n    \n    # Create channel\n    channel = DBAudioChannel(\n        id=str(uuid.uuid4()),\n        name=data.name,\n        is_default=False,\n        created_at=datetime.utcnow(),\n    )\n    db.add(channel)\n    db.flush()\n    \n    # Add device mappings\n    for device_id in data.device_ids:\n        mapping = DBChannelDeviceMapping(\n            id=str(uuid.uuid4()),\n            channel_id=channel.id,\n            device_id=device_id,\n        )\n        db.add(mapping)\n    \n    db.commit()\n    db.refresh(channel)\n    \n    return AudioChannelResponse(\n        id=channel.id,\n        name=channel.name,\n        is_default=channel.is_default,\n        device_ids=data.device_ids,\n        created_at=channel.created_at,\n    )\n\n\nasync def update_channel(\n    channel_id: str,\n    data: AudioChannelUpdate,\n    db: Session,\n) -> Optional[AudioChannelResponse]:\n    \"\"\"Update an audio channel.\"\"\"\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    if not channel:\n        return None\n    \n    if channel.is_default:\n        raise ValueError(\"Cannot modify the default channel\")\n    \n    # Update name if provided\n    if data.name is not None:\n        # Check if name already exists (excluding current channel)\n        existing = db.query(DBAudioChannel).filter(\n            DBAudioChannel.name == data.name,\n            DBAudioChannel.id != channel_id\n        ).first()\n        if existing:\n            raise ValueError(f\"Channel with name '{data.name}' already exists\")\n        channel.name = data.name\n    \n    # Update device mappings if provided\n    if data.device_ids is not None:\n        # Delete existing mappings\n        db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete()\n        \n        # Add new mappings\n        for device_id in data.device_ids:\n            mapping = DBChannelDeviceMapping(\n                id=str(uuid.uuid4()),\n                channel_id=channel.id,\n                device_id=device_id,\n            )\n            db.add(mapping)\n    \n    db.commit()\n    db.refresh(channel)\n    \n    # Get updated device IDs\n    device_mappings = db.query(DBChannelDeviceMapping).filter_by(\n        channel_id=channel.id\n    ).all()\n    device_ids = [m.device_id for m in device_mappings]\n    \n    return AudioChannelResponse(\n        id=channel.id,\n        name=channel.name,\n        is_default=channel.is_default,\n        device_ids=device_ids,\n        created_at=channel.created_at,\n    )\n\n\nasync def delete_channel(channel_id: str, db: Session) -> bool:\n    \"\"\"Delete an audio channel.\"\"\"\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    if not channel:\n        return False\n    \n    if channel.is_default:\n        raise ValueError(\"Cannot delete the default channel\")\n    \n    # Delete device mappings\n    db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete()\n    \n    # Delete profile-channel mappings\n    db.query(DBProfileChannelMapping).filter_by(channel_id=channel_id).delete()\n    \n    # Delete channel\n    db.delete(channel)\n    db.commit()\n    \n    return True\n\n\nasync def get_channel_voices(channel_id: str, db: Session) -> List[str]:\n    \"\"\"Get list of profile IDs assigned to a channel.\"\"\"\n    mappings = db.query(DBProfileChannelMapping).filter_by(\n        channel_id=channel_id\n    ).all()\n    return [m.profile_id for m in mappings]\n\n\nasync def set_channel_voices(\n    channel_id: str,\n    data: ChannelVoiceAssignment,\n    db: Session,\n) -> None:\n    \"\"\"Set which voices are assigned to a channel.\"\"\"\n    # Verify channel exists\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    if not channel:\n        raise ValueError(f\"Channel {channel_id} not found\")\n    \n    # Verify all profiles exist\n    for profile_id in data.profile_ids:\n        profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n        if not profile:\n            raise ValueError(f\"Profile {profile_id} not found\")\n    \n    # Delete existing mappings for this channel\n    db.query(DBProfileChannelMapping).filter_by(channel_id=channel_id).delete()\n    \n    # Add new mappings\n    for profile_id in data.profile_ids:\n        mapping = DBProfileChannelMapping(\n            profile_id=profile_id,\n            channel_id=channel_id,\n        )\n        db.add(mapping)\n    \n    db.commit()\n\n\nasync def get_profile_channels(profile_id: str, db: Session) -> List[str]:\n    \"\"\"Get list of channel IDs assigned to a profile.\"\"\"\n    mappings = db.query(DBProfileChannelMapping).filter_by(\n        profile_id=profile_id\n    ).all()\n    return [m.channel_id for m in mappings]\n\n\nasync def set_profile_channels(\n    profile_id: str,\n    data: ProfileChannelAssignment,\n    db: Session,\n) -> None:\n    \"\"\"Set which channels a profile is assigned to.\"\"\"\n    # Verify profile exists\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        raise ValueError(f\"Profile {profile_id} not found\")\n    \n    # Verify all channels exist\n    for channel_id in data.channel_ids:\n        channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n        if not channel:\n            raise ValueError(f\"Channel {channel_id} not found\")\n    \n    # Delete existing mappings for this profile\n    db.query(DBProfileChannelMapping).filter_by(profile_id=profile_id).delete()\n    \n    # Add new mappings\n    for channel_id in data.channel_ids:\n        mapping = DBProfileChannelMapping(\n            profile_id=profile_id,\n            channel_id=channel_id,\n        )\n        db.add(mapping)\n    \n    db.commit()\n"
  },
  {
    "path": "backend/services/cuda.py",
    "content": "\"\"\"\nCUDA backend download, assembly, and verification.\n\nDownloads two archives from GitHub Releases:\n  1. Server core (voicebox-server-cuda.tar.gz) — the exe + non-NVIDIA deps,\n     versioned with the app.\n  2. CUDA libs (cuda-libs-{version}.tar.gz) — NVIDIA runtime libraries,\n     versioned independently (only redownloaded on CUDA toolkit bump).\n\nBoth archives are extracted into {data_dir}/backends/cuda/ which forms the\ncomplete PyInstaller --onedir directory structure that torch expects.\n\"\"\"\n\nimport hashlib\nimport json\nimport logging\nimport os\nimport sys\nimport tarfile\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom ..config import get_data_dir\nfrom ..utils.progress import get_progress_manager\nfrom .. import __version__\n\nlogger = logging.getLogger(__name__)\n\nGITHUB_RELEASES_URL = \"https://github.com/jamiepine/voicebox/releases/download\"\n\nPROGRESS_KEY = \"cuda-backend\"\n\n# The current expected CUDA libs version.  Bump this when we change the\n# CUDA toolkit version or torch's CUDA dependency changes (e.g. cu126 -> cu128).\nCUDA_LIBS_VERSION = \"cu128-v1\"\n\n\ndef get_backends_dir() -> Path:\n    \"\"\"Directory where downloaded backend binaries are stored.\"\"\"\n    d = get_data_dir() / \"backends\"\n    d.mkdir(parents=True, exist_ok=True)\n    return d\n\n\ndef get_cuda_dir() -> Path:\n    \"\"\"Directory where the CUDA backend (onedir) is extracted.\"\"\"\n    d = get_backends_dir() / \"cuda\"\n    d.mkdir(parents=True, exist_ok=True)\n    return d\n\n\ndef get_cuda_exe_name() -> str:\n    \"\"\"Platform-specific CUDA executable filename.\"\"\"\n    if sys.platform == \"win32\":\n        return \"voicebox-server-cuda.exe\"\n    return \"voicebox-server-cuda\"\n\n\ndef get_cuda_binary_path() -> Optional[Path]:\n    \"\"\"Return path to the CUDA executable if it exists inside the onedir.\"\"\"\n    p = get_cuda_dir() / get_cuda_exe_name()\n    if p.exists():\n        return p\n    return None\n\n\ndef get_cuda_libs_manifest_path() -> Path:\n    \"\"\"Path to the cuda-libs.json manifest inside the CUDA dir.\"\"\"\n    return get_cuda_dir() / \"cuda-libs.json\"\n\n\ndef get_installed_cuda_libs_version() -> Optional[str]:\n    \"\"\"Read the installed CUDA libs version from cuda-libs.json, or None.\"\"\"\n    manifest_path = get_cuda_libs_manifest_path()\n    if not manifest_path.exists():\n        return None\n    try:\n        data = json.loads(manifest_path.read_text())\n        return data.get(\"version\")\n    except Exception as e:\n        logger.warning(f\"Could not read cuda-libs.json: {e}\")\n        return None\n\n\ndef is_cuda_active() -> bool:\n    \"\"\"Check if the current process is the CUDA binary.\n\n    The CUDA binary sets this env var on startup (see server.py).\n    \"\"\"\n    return os.environ.get(\"VOICEBOX_BACKEND_VARIANT\") == \"cuda\"\n\n\ndef get_cuda_status() -> dict:\n    \"\"\"Get current CUDA backend status for the API.\"\"\"\n    progress_manager = get_progress_manager()\n    cuda_path = get_cuda_binary_path()\n    progress = progress_manager.get_progress(PROGRESS_KEY)\n    cuda_libs_version = get_installed_cuda_libs_version()\n\n    return {\n        \"available\": cuda_path is not None,\n        \"active\": is_cuda_active(),\n        \"binary_path\": str(cuda_path) if cuda_path else None,\n        \"cuda_libs_version\": cuda_libs_version,\n        \"downloading\": progress is not None and progress.get(\"status\") == \"downloading\",\n        \"download_progress\": progress,\n    }\n\n\ndef _needs_server_download(version: Optional[str] = None) -> bool:\n    \"\"\"Check if the server core archive needs to be (re)downloaded.\"\"\"\n    cuda_path = get_cuda_binary_path()\n    if not cuda_path:\n        return True\n    # Check if the binary version matches the expected app version\n    installed = get_cuda_binary_version()\n    expected = version or __version__\n    if expected.startswith(\"v\"):\n        expected = expected[1:]\n    return installed != expected\n\n\ndef _needs_cuda_libs_download() -> bool:\n    \"\"\"Check if the CUDA libs archive needs to be (re)downloaded.\"\"\"\n    installed = get_installed_cuda_libs_version()\n    if installed is None:\n        return True\n    return installed != CUDA_LIBS_VERSION\n\n\nasync def _download_and_extract_archive(\n    client,\n    url: str,\n    sha256_url: Optional[str],\n    dest_dir: Path,\n    label: str,\n    progress_offset: int,\n    total_size: int,\n):\n    \"\"\"Download a .tar.gz archive and extract it into dest_dir.\n\n    Args:\n        client: httpx.AsyncClient\n        url: URL of the .tar.gz archive\n        sha256_url: URL of the .sha256 checksum file (optional)\n        dest_dir: Directory to extract into\n        label: Human-readable label for progress updates\n        progress_offset: Byte offset for progress reporting (when downloading\n            multiple archives sequentially)\n        total_size: Total bytes across all downloads (for progress bar)\n    \"\"\"\n    progress = get_progress_manager()\n    temp_path = dest_dir / f\".download-{label.replace(' ', '-')}.tmp\"\n\n    # Clean up leftover partial download\n    if temp_path.exists():\n        temp_path.unlink()\n\n    # Fetch expected checksum (fail-fast: never extract an unverified archive)\n    expected_sha = None\n    if sha256_url:\n        try:\n            sha_resp = await client.get(sha256_url)\n            sha_resp.raise_for_status()\n            expected_sha = sha_resp.text.strip().split()[0]\n            logger.info(f\"{label}: expected SHA-256: {expected_sha[:16]}...\")\n        except Exception as e:\n            raise RuntimeError(f\"{label}: failed to fetch checksum from {sha256_url}\") from e\n\n    # Stream download, verify, and extract — always clean up temp file\n    downloaded = 0\n    try:\n        async with client.stream(\"GET\", url) as response:\n            response.raise_for_status()\n            with open(temp_path, \"wb\") as f:\n                async for chunk in response.aiter_bytes(chunk_size=1024 * 1024):\n                    f.write(chunk)\n                    downloaded += len(chunk)\n                    progress.update_progress(\n                        PROGRESS_KEY,\n                        current=progress_offset + downloaded,\n                        total=total_size,\n                        filename=f\"Downloading {label}\",\n                        status=\"downloading\",\n                    )\n\n        # Verify integrity\n        if expected_sha:\n            progress.update_progress(\n                PROGRESS_KEY,\n                current=progress_offset + downloaded,\n                total=total_size,\n                filename=f\"Verifying {label}...\",\n                status=\"downloading\",\n            )\n            sha256 = hashlib.sha256()\n            with open(temp_path, \"rb\") as f:\n                while True:\n                    data = f.read(1024 * 1024)\n                    if not data:\n                        break\n                    sha256.update(data)\n            actual = sha256.hexdigest()\n            if actual != expected_sha:\n                raise ValueError(\n                    f\"{label} integrity check failed: expected {expected_sha[:16]}..., got {actual[:16]}...\"\n                )\n            logger.info(f\"{label}: integrity verified\")\n\n        # Extract (use data filter for path traversal protection on Python 3.12+)\n        progress.update_progress(\n            PROGRESS_KEY,\n            current=progress_offset + downloaded,\n            total=total_size,\n            filename=f\"Extracting {label}...\",\n            status=\"downloading\",\n        )\n        with tarfile.open(temp_path, \"r:gz\") as tar:\n            if sys.version_info >= (3, 12):\n                tar.extractall(path=dest_dir, filter=\"data\")\n            else:\n                tar.extractall(path=dest_dir)\n\n        logger.info(f\"{label}: extracted to {dest_dir}\")\n    finally:\n        if temp_path.exists():\n            temp_path.unlink()\n    return downloaded\n\n\nasync def download_cuda_binary(version: Optional[str] = None):\n    \"\"\"Download the CUDA backend (server core + CUDA libs if needed).\n\n    Downloads both archives from GitHub Releases, extracts them into\n    {data_dir}/backends/cuda/, and writes the cuda-libs.json manifest.\n\n    Only downloads what's needed:\n    - Server core: always redownloaded (versioned with app)\n    - CUDA libs: only if missing or version mismatch\n\n    Args:\n        version: Version tag (e.g. \"v0.3.0\"). Defaults to current app version.\n    \"\"\"\n    import httpx\n\n    if version is None:\n        version = f\"v{__version__}\"\n\n    progress = get_progress_manager()\n    cuda_dir = get_cuda_dir()\n\n    need_server = _needs_server_download(version)\n    need_libs = _needs_cuda_libs_download()\n\n    if not need_server and not need_libs:\n        logger.info(\"CUDA backend is up to date, nothing to download\")\n        return\n\n    logger.info(\n        f\"Starting CUDA backend download for {version} \"\n        f\"(server={'yes' if need_server else 'cached'}, \"\n        f\"libs={'yes' if need_libs else 'cached'})\"\n    )\n    progress.update_progress(\n        PROGRESS_KEY,\n        current=0,\n        total=0,\n        filename=\"Preparing download...\",\n        status=\"downloading\",\n    )\n\n    base_url = f\"{GITHUB_RELEASES_URL}/{version}\"\n    server_archive = \"voicebox-server-cuda.tar.gz\"\n    libs_archive = f\"cuda-libs-{CUDA_LIBS_VERSION}.tar.gz\"\n\n    try:\n        async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:\n            # Estimate total download size\n            total_size = 0\n            if need_server:\n                try:\n                    head = await client.head(f\"{base_url}/{server_archive}\")\n                    total_size += int(head.headers.get(\"content-length\", 0))\n                except Exception:\n                    pass\n            if need_libs:\n                try:\n                    head = await client.head(f\"{base_url}/{libs_archive}\")\n                    total_size += int(head.headers.get(\"content-length\", 0))\n                except Exception:\n                    pass\n\n            logger.info(f\"Total download size: {total_size / 1024 / 1024:.1f} MB\")\n\n            offset = 0\n\n            # Download server core\n            if need_server:\n                server_downloaded = await _download_and_extract_archive(\n                    client,\n                    url=f\"{base_url}/{server_archive}\",\n                    sha256_url=f\"{base_url}/{server_archive}.sha256\",\n                    dest_dir=cuda_dir,\n                    label=\"CUDA server\",\n                    progress_offset=offset,\n                    total_size=total_size,\n                )\n                offset += server_downloaded\n\n                # Make executable on Unix\n                exe_path = cuda_dir / get_cuda_exe_name()\n                if sys.platform != \"win32\" and exe_path.exists():\n                    exe_path.chmod(0o755)\n\n            # Download CUDA libs\n            if need_libs:\n                await _download_and_extract_archive(\n                    client,\n                    url=f\"{base_url}/{libs_archive}\",\n                    sha256_url=f\"{base_url}/{libs_archive}.sha256\",\n                    dest_dir=cuda_dir,\n                    label=\"CUDA libraries\",\n                    progress_offset=offset,\n                    total_size=total_size,\n                )\n\n                # Write local cuda-libs.json manifest\n                manifest = {\"version\": CUDA_LIBS_VERSION}\n                get_cuda_libs_manifest_path().write_text(json.dumps(manifest, indent=2) + \"\\n\")\n\n        logger.info(f\"CUDA backend ready at {cuda_dir}\")\n        progress.mark_complete(PROGRESS_KEY)\n\n    except Exception as e:\n        logger.error(f\"CUDA backend download failed: {e}\")\n        progress.mark_error(PROGRESS_KEY, str(e))\n        raise\n\n\ndef get_cuda_binary_version() -> Optional[str]:\n    \"\"\"Get the version of the installed CUDA binary, or None if not installed.\"\"\"\n    import subprocess\n\n    cuda_path = get_cuda_binary_path()\n    if not cuda_path:\n        return None\n    try:\n        result = subprocess.run(\n            [str(cuda_path), \"--version\"],\n            capture_output=True,\n            text=True,\n            timeout=30,\n            cwd=str(cuda_path.parent),  # Run from the onedir directory\n        )\n        # Output format: \"voicebox-server 0.3.0\"\n        for line in result.stdout.strip().splitlines():\n            if \"voicebox-server\" in line:\n                return line.split()[-1]\n    except Exception as e:\n        logger.warning(f\"Could not get CUDA binary version: {e}\")\n    return None\n\n\nasync def check_and_update_cuda_binary():\n    \"\"\"Check if the CUDA binary is outdated and auto-download if so.\n\n    Called on server startup. Checks both server version and CUDA libs\n    version. Downloads only what's needed.\n    \"\"\"\n    cuda_path = get_cuda_binary_path()\n    if not cuda_path:\n        return  # No CUDA binary installed, nothing to update\n\n    need_server = _needs_server_download()\n    need_libs = _needs_cuda_libs_download()\n\n    if not need_server and not need_libs:\n        logger.info(f\"CUDA binary is up to date (server=v{__version__}, libs={get_installed_cuda_libs_version()})\")\n        return\n\n    reasons = []\n    if need_server:\n        cuda_version = get_cuda_binary_version()\n        reasons.append(f\"server v{cuda_version} != v{__version__}\")\n    if need_libs:\n        installed_libs = get_installed_cuda_libs_version()\n        reasons.append(f\"libs {installed_libs} != {CUDA_LIBS_VERSION}\")\n\n    logger.info(f\"CUDA backend needs update ({', '.join(reasons)}). Auto-downloading...\")\n\n    try:\n        await download_cuda_binary()\n    except Exception as e:\n        logger.error(f\"Auto-update of CUDA binary failed: {e}\")\n\n\nasync def delete_cuda_binary() -> bool:\n    \"\"\"Delete the downloaded CUDA backend directory. Returns True if deleted.\"\"\"\n    import shutil\n\n    cuda_dir = get_cuda_dir()\n    if cuda_dir.exists() and any(cuda_dir.iterdir()):\n        shutil.rmtree(cuda_dir)\n        logger.info(f\"Deleted CUDA backend directory: {cuda_dir}\")\n        return True\n    return False\n"
  },
  {
    "path": "backend/services/effects.py",
    "content": "\"\"\"\nEffect presets CRUD operations.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport uuid\nfrom typing import List, Optional\n\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy.exc import IntegrityError\n\nfrom ..database import EffectPreset as DBEffectPreset\nfrom ..models import EffectPresetResponse, EffectPresetCreate, EffectPresetUpdate, EffectConfig\n\n\ndef _preset_response(p: DBEffectPreset) -> EffectPresetResponse:\n    \"\"\"Convert a DB preset row to a Pydantic response.\"\"\"\n    effects_chain = [EffectConfig(**e) for e in json.loads(p.effects_chain)]\n    return EffectPresetResponse(\n        id=p.id,\n        name=p.name,\n        description=p.description,\n        effects_chain=effects_chain,\n        is_builtin=p.is_builtin or False,\n        created_at=p.created_at,\n    )\n\n\ndef list_presets(db: Session) -> List[EffectPresetResponse]:\n    \"\"\"List all effect presets (built-in + user-created).\"\"\"\n    presets = db.query(DBEffectPreset).order_by(DBEffectPreset.sort_order, DBEffectPreset.name).all()\n    return [_preset_response(p) for p in presets]\n\n\ndef get_preset(preset_id: str, db: Session) -> Optional[EffectPresetResponse]:\n    \"\"\"Get a preset by ID.\"\"\"\n    p = db.query(DBEffectPreset).filter_by(id=preset_id).first()\n    if not p:\n        return None\n    return _preset_response(p)\n\n\ndef get_preset_by_name(name: str, db: Session) -> Optional[EffectPresetResponse]:\n    \"\"\"Get a preset by name.\"\"\"\n    p = db.query(DBEffectPreset).filter_by(name=name).first()\n    if not p:\n        return None\n    return _preset_response(p)\n\n\ndef create_preset(data: EffectPresetCreate, db: Session) -> EffectPresetResponse:\n    \"\"\"Create a new user effect preset.\"\"\"\n    from .utils.effects import validate_effects_chain\n\n    chain_dicts = [e.model_dump() for e in data.effects_chain]\n    error = validate_effects_chain(chain_dicts)\n    if error:\n        raise ValueError(error)\n\n    # Check for duplicate name before insert\n    existing = db.query(DBEffectPreset).filter_by(name=data.name).first()\n    if existing:\n        raise ValueError(f\"A preset named '{data.name}' already exists\")\n\n    preset = DBEffectPreset(\n        id=str(uuid.uuid4()),\n        name=data.name,\n        description=data.description,\n        effects_chain=json.dumps(chain_dicts),\n        is_builtin=False,\n    )\n    db.add(preset)\n    try:\n        db.commit()\n    except IntegrityError:\n        db.rollback()\n        raise ValueError(f\"A preset named '{data.name}' already exists\")\n    db.refresh(preset)\n    return _preset_response(preset)\n\n\ndef update_preset(preset_id: str, data: EffectPresetUpdate, db: Session) -> Optional[EffectPresetResponse]:\n    \"\"\"Update a user effect preset. Cannot modify built-in presets.\"\"\"\n    preset = db.query(DBEffectPreset).filter_by(id=preset_id).first()\n    if not preset:\n        return None\n    if preset.is_builtin:\n        raise ValueError(\"Cannot modify built-in presets\")\n\n    if data.name is not None:\n        preset.name = data.name\n    if data.description is not None:\n        preset.description = data.description\n    if data.effects_chain is not None:\n        from .utils.effects import validate_effects_chain\n\n        chain_dicts = [e.model_dump() for e in data.effects_chain]\n        error = validate_effects_chain(chain_dicts)\n        if error:\n            raise ValueError(error)\n        preset.effects_chain = json.dumps(chain_dicts)\n\n    db.commit()\n    db.refresh(preset)\n    return _preset_response(preset)\n\n\ndef delete_preset(preset_id: str, db: Session) -> bool:\n    \"\"\"Delete a user effect preset. Cannot delete built-in presets.\"\"\"\n    preset = db.query(DBEffectPreset).filter_by(id=preset_id).first()\n    if not preset:\n        return False\n    if preset.is_builtin:\n        raise ValueError(\"Cannot delete built-in presets\")\n\n    db.delete(preset)\n    db.commit()\n    return True\n"
  },
  {
    "path": "backend/services/export_import.py",
    "content": "\"\"\"\nVoice profile export/import module.\n\nHandles exporting profiles to ZIP archives and importing them back.\nAlso handles exporting individual generations.\n\"\"\"\n\nimport json\nimport zipfile\nimport io\nfrom pathlib import Path\nfrom typing import Optional\nfrom sqlalchemy.orm import Session\n\nfrom ..models import VoiceProfileResponse\nfrom ..database import VoiceProfile as DBVoiceProfile, ProfileSample as DBProfileSample, Generation as DBGeneration, GenerationVersion as DBGenerationVersion\nfrom .profiles import create_profile, add_profile_sample\nfrom ..models import VoiceProfileCreate\nfrom .. import config\n\n\ndef _get_unique_profile_name(name: str, db: Session) -> str:\n    \"\"\"\n    Get a unique profile name by appending a number if needed.\n    \n    Args:\n        name: Original profile name\n        db: Database session\n        \n    Returns:\n        Unique profile name\n    \"\"\"\n    base_name = name\n    counter = 1\n    \n    while True:\n        existing = db.query(DBVoiceProfile).filter_by(name=name).first()\n        if not existing:\n            return name\n        \n        name = f\"{base_name} ({counter})\"\n        counter += 1\n\n\ndef export_profile_to_zip(profile_id: str, db: Session) -> bytes:\n    \"\"\"\n    Export a voice profile to a ZIP archive.\n    \n    Args:\n        profile_id: Profile ID to export\n        db: Database session\n        \n    Returns:\n        ZIP file contents as bytes\n        \n    Raises:\n        ValueError: If profile not found or has no samples\n    \"\"\"\n    # Get profile\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        raise ValueError(f\"Profile {profile_id} not found\")\n    \n    # Get all samples\n    samples = db.query(DBProfileSample).filter_by(profile_id=profile_id).all()\n    if not samples:\n        raise ValueError(f\"Profile {profile_id} has no samples\")\n    \n    # Create ZIP in memory\n    zip_buffer = io.BytesIO()\n    \n    with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:\n        # Check if profile has avatar\n        has_avatar = False\n        if profile.avatar_path:\n            avatar_path = Path(profile.avatar_path)\n            if avatar_path.exists():\n                has_avatar = True\n                # Add avatar to ZIP root with original extension\n                avatar_ext = avatar_path.suffix\n                zip_file.write(avatar_path, f\"avatar{avatar_ext}\")\n\n        # Create manifest.json\n        manifest = {\n            \"version\": \"1.0\",\n            \"profile\": {\n                \"name\": profile.name,\n                \"description\": profile.description,\n                \"language\": profile.language,\n            },\n            \"has_avatar\": has_avatar,\n        }\n        zip_file.writestr(\"manifest.json\", json.dumps(manifest, indent=2))\n\n        # Create samples.json mapping\n        samples_data = {}\n        profile_dir = config.get_profiles_dir() / profile_id\n\n        for sample in samples:\n            # Get filename from audio_path (should be {sample_id}.wav)\n            audio_path = Path(sample.audio_path)\n            filename = audio_path.name\n\n            # Read audio file\n            if not audio_path.exists():\n                raise ValueError(f\"Audio file not found: {audio_path}\")\n\n            # Add to samples directory in ZIP\n            zip_path = f\"samples/{filename}\"\n            zip_file.write(audio_path, zip_path)\n\n            # Map filename to reference text\n            samples_data[filename] = sample.reference_text\n\n        zip_file.writestr(\"samples.json\", json.dumps(samples_data, indent=2))\n    \n    zip_buffer.seek(0)\n    return zip_buffer.read()\n\n\nasync def import_profile_from_zip(file_bytes: bytes, db: Session) -> VoiceProfileResponse:\n    \"\"\"\n    Import a voice profile from a ZIP archive.\n    \n    Args:\n        file_bytes: ZIP file contents\n        db: Database session\n        \n    Returns:\n        Created profile\n        \n    Raises:\n        ValueError: If ZIP is invalid or missing required files\n    \"\"\"\n    zip_buffer = io.BytesIO(file_bytes)\n    \n    try:\n        with zipfile.ZipFile(zip_buffer, 'r') as zip_file:\n            # Validate ZIP structure\n            namelist = zip_file.namelist()\n            \n            if \"manifest.json\" not in namelist:\n                raise ValueError(\"ZIP archive missing manifest.json\")\n            \n            if \"samples.json\" not in namelist:\n                raise ValueError(\"ZIP archive missing samples.json\")\n            \n            # Read manifest\n            manifest_data = json.loads(zip_file.read(\"manifest.json\"))\n            \n            if \"version\" not in manifest_data:\n                raise ValueError(\"Invalid manifest.json: missing version\")\n            \n            if \"profile\" not in manifest_data:\n                raise ValueError(\"Invalid manifest.json: missing profile\")\n            \n            profile_data = manifest_data[\"profile\"]\n            \n            # Read samples mapping\n            samples_data = json.loads(zip_file.read(\"samples.json\"))\n            \n            if not isinstance(samples_data, dict):\n                raise ValueError(\"Invalid samples.json: must be a dictionary\")\n            \n            # Get unique profile name\n            original_name = profile_data.get(\"name\", \"Imported Profile\")\n            unique_name = _get_unique_profile_name(original_name, db)\n            \n            # Create profile\n            profile_create = VoiceProfileCreate(\n                name=unique_name,\n                description=profile_data.get(\"description\"),\n                language=profile_data.get(\"language\", \"en\"),\n            )\n            \n            profile = await create_profile(profile_create, db)\n\n            # Extract and add samples\n            profile_dir = config.get_profiles_dir() / profile.id\n            profile_dir.mkdir(parents=True, exist_ok=True)\n\n            # Handle avatar if present\n            avatar_files = [f for f in namelist if f.startswith(\"avatar.\")]\n            if avatar_files:\n                try:\n                    avatar_file = avatar_files[0]\n                    # Extract to temporary file\n                    import tempfile\n                    with tempfile.NamedTemporaryFile(suffix=Path(avatar_file).suffix, delete=False) as tmp:\n                        tmp.write(zip_file.read(avatar_file))\n                        tmp_path = tmp.name\n\n                    try:\n                        from .profiles import upload_avatar\n                        await upload_avatar(profile.id, tmp_path, db)\n                    finally:\n                        Path(tmp_path).unlink(missing_ok=True)\n                except Exception as e:\n                    # Avatar import is optional - continue even if it fails\n                    pass\n\n            for filename, reference_text in samples_data.items():\n                # Validate filename\n                if not filename.endswith('.wav'):\n                    raise ValueError(f\"Invalid sample filename: {filename} (must be .wav)\")\n                \n                # Extract audio file to temp location\n                zip_path = f\"samples/{filename}\"\n                \n                if zip_path not in namelist:\n                    raise ValueError(f\"Sample file not found in ZIP: {zip_path}\")\n                \n                # Extract to temporary file\n                import tempfile\n                with tempfile.NamedTemporaryFile(suffix=\".wav\", delete=False) as tmp:\n                    tmp.write(zip_file.read(zip_path))\n                    tmp_path = tmp.name\n                \n                try:\n                    # Add sample to profile\n                    await add_profile_sample(\n                        profile.id,\n                        tmp_path,\n                        reference_text,\n                        db,\n                    )\n                finally:\n                    # Clean up temp file\n                    Path(tmp_path).unlink(missing_ok=True)\n            \n            return profile\n            \n    except zipfile.BadZipFile:\n        raise ValueError(\"Invalid ZIP file\")\n    except json.JSONDecodeError as e:\n        raise ValueError(f\"Invalid JSON in archive: {e}\")\n    except Exception as e:\n        if isinstance(e, ValueError):\n            raise\n        raise ValueError(f\"Error importing profile: {str(e)}\")\n\n\ndef export_generation_to_zip(generation_id: str, db: Session) -> bytes:\n    \"\"\"\n    Export a generation to a ZIP archive.\n    \n    Args:\n        generation_id: Generation ID to export\n        db: Database session\n        \n    Returns:\n        ZIP file contents as bytes\n        \n    Raises:\n        ValueError: If generation not found\n    \"\"\"\n    # Get generation\n    generation = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not generation:\n        raise ValueError(f\"Generation {generation_id} not found\")\n    \n    # Get profile info\n    profile = db.query(DBVoiceProfile).filter_by(id=generation.profile_id).first()\n    if not profile:\n        raise ValueError(f\"Profile {generation.profile_id} not found\")\n    \n    # Get all versions for this generation\n    versions = (\n        db.query(DBGenerationVersion)\n        .filter_by(generation_id=generation_id)\n        .order_by(DBGenerationVersion.created_at)\n        .all()\n    )\n\n    # Create ZIP in memory\n    zip_buffer = io.BytesIO()\n    \n    with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:\n        # Build version manifest entries\n        version_entries = []\n        for v in versions:\n            v_path = Path(v.audio_path)\n            effects_chain = None\n            if v.effects_chain:\n                effects_chain = json.loads(v.effects_chain)\n            version_entries.append({\n                \"id\": v.id,\n                \"label\": v.label,\n                \"is_default\": v.is_default,\n                \"effects_chain\": effects_chain,\n                \"filename\": v_path.name,\n            })\n\n        manifest = {\n            \"version\": \"1.0\",\n            \"generation\": {\n                \"id\": generation.id,\n                \"text\": generation.text,\n                \"language\": generation.language,\n                \"duration\": generation.duration,\n                \"seed\": generation.seed,\n                \"instruct\": generation.instruct,\n                \"created_at\": generation.created_at.isoformat(),\n            },\n            \"profile\": {\n                \"id\": profile.id,\n                \"name\": profile.name,\n                \"description\": profile.description,\n                \"language\": profile.language,\n            },\n            \"versions\": version_entries,\n        }\n        zip_file.writestr(\"manifest.json\", json.dumps(manifest, indent=2))\n        \n        # Add all version audio files\n        for v in versions:\n            v_path = Path(v.audio_path)\n            if v_path.exists():\n                zip_file.write(v_path, f\"audio/{v_path.name}\")\n\n        # Fallback: if no versions exist, include the generation's main audio\n        if not versions:\n            audio_path = Path(generation.audio_path)\n            if audio_path.exists():\n                zip_file.write(audio_path, f\"audio/{audio_path.name}\")\n    \n    zip_buffer.seek(0)\n    return zip_buffer.read()\n\n\nasync def import_generation_from_zip(file_bytes: bytes, db: Session) -> dict:\n    \"\"\"\n    Import a generation from a ZIP archive.\n    \n    Args:\n        file_bytes: ZIP file contents\n        db: Database session\n        \n    Returns:\n        Dictionary with generation ID and profile info\n        \n    Raises:\n        ValueError: If ZIP is invalid or missing required files\n    \"\"\"\n    from pathlib import Path\n    import tempfile\n    import shutil\n    from datetime import datetime\n    from .. import config\n    \n    zip_buffer = io.BytesIO(file_bytes)\n    \n    try:\n        with zipfile.ZipFile(zip_buffer, 'r') as zip_file:\n            # Validate ZIP structure\n            namelist = zip_file.namelist()\n            \n            if \"manifest.json\" not in namelist:\n                raise ValueError(\"ZIP archive missing manifest.json\")\n            \n            # Read manifest\n            manifest_data = json.loads(zip_file.read(\"manifest.json\"))\n            \n            if \"version\" not in manifest_data:\n                raise ValueError(\"Invalid manifest.json: missing version\")\n            \n            if \"generation\" not in manifest_data:\n                raise ValueError(\"Invalid manifest.json: missing generation data\")\n            \n            generation_data = manifest_data[\"generation\"]\n            profile_data = manifest_data.get(\"profile\", {})\n            \n            # Validate required fields\n            required_fields = [\"text\", \"language\", \"duration\"]\n            for field in required_fields:\n                if field not in generation_data:\n                    raise ValueError(f\"Invalid manifest.json: missing generation.{field}\")\n            \n            # Find audio file in archive\n            audio_files = [f for f in namelist if f.startswith(\"audio/\") and f.endswith(\".wav\")]\n            if not audio_files:\n                raise ValueError(\"No audio file found in ZIP archive\")\n            \n            audio_file_path = audio_files[0]\n            \n            # Check if we should match an existing profile or create metadata\n            profile_id = None\n            profile_name = profile_data.get(\"name\", \"Unknown Profile\")\n            \n            # Try to find matching profile by name\n            if profile_name and profile_name != \"Unknown Profile\":\n                existing_profile = db.query(DBVoiceProfile).filter_by(name=profile_name).first()\n                if existing_profile:\n                    profile_id = existing_profile.id\n            \n            # If no matching profile, use a placeholder or the first available profile\n            if not profile_id:\n                # Get any profile, or None if no profiles exist\n                any_profile = db.query(DBVoiceProfile).first()\n                if any_profile:\n                    profile_id = any_profile.id\n                    profile_name = any_profile.name\n                else:\n                    raise ValueError(\"No voice profiles found. Please create a profile before importing generations.\")\n            \n            # Extract audio file to temporary location\n            with tempfile.NamedTemporaryFile(suffix=\".wav\", delete=False) as tmp:\n                tmp.write(zip_file.read(audio_file_path))\n                tmp_path = tmp.name\n            \n            try:\n                # Create generations directory\n                generations_dir = config.get_generations_dir()\n                generations_dir.mkdir(parents=True, exist_ok=True)\n                \n                # Generate new ID for this generation\n                new_generation_id = str(__import__('uuid').uuid4())\n                \n                # Copy audio to generations directory\n                audio_dest = generations_dir / f\"{new_generation_id}.wav\"\n                shutil.copy(tmp_path, audio_dest)\n                \n                # Create generation record\n                db_generation = DBGeneration(\n                    id=new_generation_id,\n                    profile_id=profile_id,\n                    text=generation_data[\"text\"],\n                    language=generation_data[\"language\"],\n                    audio_path=str(audio_dest),\n                    duration=generation_data[\"duration\"],\n                    seed=generation_data.get(\"seed\"),\n                    instruct=generation_data.get(\"instruct\"),\n                    created_at=datetime.utcnow(),\n                )\n                \n                db.add(db_generation)\n                db.commit()\n                db.refresh(db_generation)\n                \n                return {\n                    \"id\": db_generation.id,\n                    \"profile_id\": profile_id,\n                    \"profile_name\": profile_name,\n                    \"text\": db_generation.text,\n                    \"message\": f\"Generation imported successfully (assigned to profile: {profile_name})\"\n                }\n                \n            finally:\n                # Clean up temp file\n                Path(tmp_path).unlink(missing_ok=True)\n            \n    except zipfile.BadZipFile:\n        raise ValueError(\"Invalid ZIP file\")\n    except json.JSONDecodeError as e:\n        raise ValueError(f\"Invalid JSON in archive: {e}\")\n    except Exception as e:\n        if isinstance(e, ValueError):\n            raise\n        raise ValueError(f\"Error importing generation: {str(e)}\")\n"
  },
  {
    "path": "backend/services/generation.py",
    "content": "\"\"\"\nUnified TTS generation orchestration.\n\nReplaces the three near-identical closures (_run_generation, _run_retry,\n_run_regenerate) that lived in main.py with a single ``run_generation()``\nfunction parameterized by *mode*.\n\nMode differences:\n  - \"generate\"   : full pipeline -- save clean version, optionally apply\n                    effects and create a processed version.\n  - \"retry\"      : re-runs a failed generation with the same seed.\n                    No effects, no version creation.\n  - \"regenerate\" : re-runs with seed=None for variation.  Creates a new\n                    version with an auto-incremented \"take-N\" label.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport traceback\nfrom typing import Literal, Optional\n\nfrom .. import config\nfrom . import history, profiles\nfrom ..database import get_db\nfrom ..utils.tasks import get_task_manager\n\n\nasync def run_generation(\n    *,\n    generation_id: str,\n    profile_id: str,\n    text: str,\n    language: str,\n    engine: str,\n    model_size: str,\n    seed: Optional[int],\n    normalize: bool = False,\n    effects_chain: Optional[list] = None,\n    instruct: Optional[str] = None,\n    mode: Literal[\"generate\", \"retry\", \"regenerate\"],\n    max_chunk_chars: Optional[int] = None,\n    crossfade_ms: Optional[int] = None,\n    version_id: Optional[str] = None,\n) -> None:\n    \"\"\"Execute TTS inference and persist the result.\n\n    This is the single entry point for all background generation work.\n    It is designed to be enqueued via ``services.task_queue.enqueue_generation``.\n    \"\"\"\n    from ..backends import load_engine_model, get_tts_backend_for_engine, engine_needs_trim\n    from ..utils.chunked_tts import generate_chunked\n    from ..utils.audio import normalize_audio, save_audio, trim_tts_output\n\n    task_manager = get_task_manager()\n    bg_db = next(get_db())\n\n    try:\n        tts_model = get_tts_backend_for_engine(engine)\n\n        if not tts_model.is_loaded():\n            await history.update_generation_status(generation_id, \"loading_model\", bg_db)\n\n        await load_engine_model(engine, model_size)\n\n        voice_prompt = await profiles.create_voice_prompt_for_profile(\n            profile_id,\n            bg_db,\n            use_cache=True,\n            engine=engine,\n        )\n\n        await history.update_generation_status(generation_id, \"generating\", bg_db)\n        trim_fn = trim_tts_output if engine_needs_trim(engine) else None\n\n        gen_kwargs: dict = dict(\n            language=language,\n            seed=seed if mode != \"regenerate\" else None,\n            instruct=instruct,\n            trim_fn=trim_fn,\n        )\n        if max_chunk_chars is not None:\n            gen_kwargs[\"max_chunk_chars\"] = max_chunk_chars\n        if crossfade_ms is not None:\n            gen_kwargs[\"crossfade_ms\"] = crossfade_ms\n\n        audio, sample_rate = await generate_chunked(tts_model, text, voice_prompt, **gen_kwargs)\n\n        # --- Normalize (generate and regenerate always; retry skips) -----\n        if normalize or mode == \"regenerate\":\n            audio = normalize_audio(audio)\n\n        duration = len(audio) / sample_rate\n\n        # --- Persist audio and update status -----------------------------\n        if mode == \"generate\":\n            final_path = _save_generate(\n                generation_id=generation_id,\n                audio=audio,\n                sample_rate=sample_rate,\n                effects_chain=effects_chain,\n                save_audio=save_audio,\n                db=bg_db,\n            )\n        elif mode == \"retry\":\n            final_path = _save_retry(\n                generation_id=generation_id,\n                audio=audio,\n                sample_rate=sample_rate,\n                save_audio=save_audio,\n            )\n        elif mode == \"regenerate\":\n            final_path = _save_regenerate(\n                generation_id=generation_id,\n                version_id=version_id,\n                audio=audio,\n                sample_rate=sample_rate,\n                save_audio=save_audio,\n                db=bg_db,\n            )\n\n        await history.update_generation_status(\n            generation_id=generation_id,\n            status=\"completed\",\n            db=bg_db,\n            audio_path=final_path,\n            duration=duration,\n        )\n\n    except Exception as e:\n        traceback.print_exc()\n        await history.update_generation_status(\n            generation_id=generation_id,\n            status=\"failed\",\n            db=bg_db,\n            error=str(e),\n        )\n    finally:\n        task_manager.complete_generation(generation_id)\n        bg_db.close()\n\n\ndef _save_generate(\n    *,\n    generation_id: str,\n    audio,\n    sample_rate: int,\n    effects_chain: Optional[list],\n    save_audio,\n    db,\n) -> str:\n    \"\"\"Save clean version and optionally an effects-processed version.\n\n    Returns the final audio path (processed if effects were applied,\n    otherwise clean).\n    \"\"\"\n    from . import versions as versions_mod\n\n    clean_audio_path = config.get_generations_dir() / f\"{generation_id}.wav\"\n    save_audio(audio, str(clean_audio_path), sample_rate)\n\n    has_effects = effects_chain and any(e.get(\"enabled\", True) for e in effects_chain)\n\n    versions_mod.create_version(\n        generation_id=generation_id,\n        label=\"original\",\n        audio_path=str(clean_audio_path),\n        db=db,\n        effects_chain=None,\n        is_default=not has_effects,\n    )\n\n    final_audio_path = str(clean_audio_path)\n\n    if has_effects:\n        from ..utils.effects import apply_effects, validate_effects_chain\n\n        error_msg = validate_effects_chain(effects_chain)\n        if error_msg:\n            import logging\n            logging.getLogger(__name__).warning(\"invalid effects chain, skipping: %s\", error_msg)\n            versions_mod.set_default_version(\n                versions_mod.list_versions(generation_id, db)[0].id, db\n            )\n        else:\n            processed_audio = apply_effects(audio, sample_rate, effects_chain)\n            processed_path = config.get_generations_dir() / f\"{generation_id}_processed.wav\"\n            save_audio(processed_audio, str(processed_path), sample_rate)\n            final_audio_path = str(processed_path)\n            versions_mod.create_version(\n                generation_id=generation_id,\n                label=\"version-2\",\n                audio_path=str(processed_path),\n                db=db,\n                effects_chain=effects_chain,\n                is_default=True,\n            )\n\n    return final_audio_path\n\n\ndef _save_retry(\n    *,\n    generation_id: str,\n    audio,\n    sample_rate: int,\n    save_audio,\n) -> str:\n    \"\"\"Save retry output -- single file, no versions.\n\n    Returns the audio path.\n    \"\"\"\n    audio_path = config.get_generations_dir() / f\"{generation_id}.wav\"\n    save_audio(audio, str(audio_path), sample_rate)\n    return str(audio_path)\n\n\ndef _save_regenerate(\n    *,\n    generation_id: str,\n    version_id: Optional[str],\n    audio,\n    sample_rate: int,\n    save_audio,\n    db,\n) -> str:\n    \"\"\"Save regeneration output as a new version with auto-label.\n\n    Returns the audio path.\n    \"\"\"\n    from . import versions as versions_mod\n\n    import uuid as _uuid\n\n    suffix = _uuid.uuid4().hex[:8]\n    audio_path = config.get_generations_dir() / f\"{generation_id}_{suffix}.wav\"\n    save_audio(audio, str(audio_path), sample_rate)\n\n    # Count via DB query rather than list length to avoid TOCTOU race\n    from ..database import GenerationVersion as DBGenerationVersion\n\n    count = db.query(DBGenerationVersion).filter_by(generation_id=generation_id).count()\n    label = f\"take-{count + 1}\"\n\n    versions_mod.create_version(\n        generation_id=generation_id,\n        label=label,\n        audio_path=str(audio_path),\n        db=db,\n        effects_chain=None,\n        is_default=True,\n    )\n\n    return str(audio_path)\n"
  },
  {
    "path": "backend/services/history.py",
    "content": "\"\"\"\nGeneration history management module.\n\"\"\"\n\nfrom typing import List, Optional, Tuple\nfrom datetime import datetime\nimport uuid\nimport shutil\nfrom pathlib import Path\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import or_\n\nfrom ..models import GenerationRequest, GenerationResponse, HistoryQuery, HistoryResponse, HistoryListResponse, GenerationVersionResponse, EffectConfig\nfrom ..database import Generation as DBGeneration, GenerationVersion as DBGenerationVersion, VoiceProfile as DBVoiceProfile\nfrom .. import config\n\n\ndef _get_versions_for_generation(generation_id: str, db: Session) -> tuple:\n    \"\"\"Get versions list and active version ID for a generation.\"\"\"\n    import json\n    versions_rows = (\n        db.query(DBGenerationVersion)\n        .filter_by(generation_id=generation_id)\n        .order_by(DBGenerationVersion.created_at)\n        .all()\n    )\n    if not versions_rows:\n        return None, None\n\n    versions = []\n    active_version_id = None\n    for v in versions_rows:\n        effects_chain = None\n        if v.effects_chain:\n            try:\n                raw = json.loads(v.effects_chain)\n                effects_chain = [EffectConfig(**e) for e in raw]\n            except Exception:\n                pass\n        versions.append(GenerationVersionResponse(\n            id=v.id,\n            generation_id=v.generation_id,\n            label=v.label,\n            audio_path=v.audio_path,\n            effects_chain=effects_chain,\n            is_default=v.is_default,\n            created_at=v.created_at,\n        ))\n        if v.is_default:\n            active_version_id = v.id\n\n    return versions, active_version_id\n\n\nasync def create_generation(\n    profile_id: str,\n    text: str,\n    language: str,\n    audio_path: str,\n    duration: float,\n    seed: Optional[int],\n    db: Session,\n    instruct: Optional[str] = None,\n    generation_id: Optional[str] = None,\n    status: str = \"completed\",\n    engine: Optional[str] = \"qwen\",\n    model_size: Optional[str] = None,\n) -> GenerationResponse:\n    \"\"\"\n    Create a new generation history entry.\n\n    Args:\n        profile_id: Profile ID used for generation\n        text: Generated text\n        language: Language code\n        audio_path: Path where audio was saved\n        duration: Audio duration in seconds\n        seed: Random seed used (if any)\n        db: Database session\n        instruct: Natural language instruction used (if any)\n        generation_id: Pre-assigned ID (for async generation flow)\n        status: Generation status (generating, completed, failed)\n        engine: TTS engine used (qwen, luxtts, chatterbox, chatterbox_turbo)\n        model_size: Model size variant (1.7B, 0.6B) — only relevant for qwen\n\n    Returns:\n        Created generation entry\n    \"\"\"\n    db_generation = DBGeneration(\n        id=generation_id or str(uuid.uuid4()),\n        profile_id=profile_id,\n        text=text,\n        language=language,\n        audio_path=audio_path,\n        duration=duration,\n        seed=seed,\n        instruct=instruct,\n        engine=engine,\n        model_size=model_size,\n        status=status,\n        created_at=datetime.utcnow(),\n    )\n\n    db.add(db_generation)\n    db.commit()\n    db.refresh(db_generation)\n\n    return GenerationResponse.model_validate(db_generation)\n\n\nasync def update_generation_status(\n    generation_id: str,\n    status: str,\n    db: Session,\n    audio_path: Optional[str] = None,\n    duration: Optional[float] = None,\n    error: Optional[str] = None,\n) -> Optional[GenerationResponse]:\n    \"\"\"Update the status of a generation (used by async generation flow).\"\"\"\n    generation = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not generation:\n        return None\n\n    generation.status = status\n    if audio_path is not None:\n        generation.audio_path = audio_path\n    if duration is not None:\n        generation.duration = duration\n    if error is not None:\n        generation.error = error\n\n    db.commit()\n    db.refresh(generation)\n    return GenerationResponse.model_validate(generation)\n\n\nasync def get_generation(\n    generation_id: str,\n    db: Session,\n) -> Optional[GenerationResponse]:\n    \"\"\"\n    Get a generation by ID.\n    \n    Args:\n        generation_id: Generation ID\n        db: Database session\n        \n    Returns:\n        Generation or None if not found\n    \"\"\"\n    generation = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not generation:\n        return None\n    \n    return GenerationResponse.model_validate(generation)\n\n\nasync def list_generations(\n    query: HistoryQuery,\n    db: Session,\n) -> HistoryListResponse:\n    \"\"\"\n    List generations with optional filters.\n    \n    Args:\n        query: Query parameters (filters, pagination)\n        db: Database session\n        \n    Returns:\n        HistoryListResponse with items and total count\n    \"\"\"\n    # Build base query with join to get profile name\n    q = db.query(\n        DBGeneration,\n        DBVoiceProfile.name.label('profile_name')\n    ).join(\n        DBVoiceProfile,\n        DBGeneration.profile_id == DBVoiceProfile.id\n    )\n    \n    # Apply profile filter\n    if query.profile_id:\n        q = q.filter(DBGeneration.profile_id == query.profile_id)\n    \n    # Apply search filter (searches in text content)\n    if query.search:\n        search_pattern = f\"%{query.search}%\"\n        q = q.filter(DBGeneration.text.like(search_pattern))\n    \n    # Get total count before pagination\n    total_count = q.count()\n    \n    # Apply ordering (newest first)\n    q = q.order_by(DBGeneration.created_at.desc())\n    \n    # Apply pagination\n    q = q.offset(query.offset).limit(query.limit)\n    \n    # Execute query\n    results = q.all()\n    \n    # Convert to HistoryResponse with profile_name\n    items = []\n    for generation, profile_name in results:\n        versions, active_version_id = _get_versions_for_generation(generation.id, db)\n        items.append(HistoryResponse(\n            id=generation.id,\n            profile_id=generation.profile_id,\n            profile_name=profile_name,\n            text=generation.text,\n            language=generation.language,\n            audio_path=generation.audio_path,\n            duration=generation.duration,\n            seed=generation.seed,\n            instruct=generation.instruct,\n            engine=generation.engine or \"qwen\",\n            model_size=generation.model_size,\n            status=generation.status or \"completed\",\n            error=generation.error,\n            is_favorited=bool(generation.is_favorited),\n            created_at=generation.created_at,\n            versions=versions,\n            active_version_id=active_version_id,\n        ))\n    \n    return HistoryListResponse(\n        items=items,\n        total=total_count,\n    )\n\n\nasync def delete_generation(\n    generation_id: str,\n    db: Session,\n) -> bool:\n    \"\"\"\n    Delete a generation.\n    \n    Args:\n        generation_id: Generation ID\n        db: Database session\n        \n    Returns:\n        True if deleted, False if not found\n    \"\"\"\n    generation = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not generation:\n        return False\n\n    # Delete all version files and records\n    from . import versions as versions_mod\n    versions_mod.delete_versions_for_generation(generation_id, db)\n\n    # Delete main audio file (if not already removed by version cleanup)\n    if generation.audio_path:\n        audio_path = Path(generation.audio_path)\n        if audio_path.exists():\n            audio_path.unlink()\n\n    # Delete from database\n    db.delete(generation)\n    db.commit()\n    \n    return True\n\n\nasync def delete_generations_by_profile(\n    profile_id: str,\n    db: Session,\n) -> int:\n    \"\"\"\n    Delete all generations for a profile.\n    \n    Args:\n        profile_id: Profile ID\n        db: Database session\n        \n    Returns:\n        Number of generations deleted\n    \"\"\"\n    generations = db.query(DBGeneration).filter_by(profile_id=profile_id).all()\n    \n    count = 0\n    for generation in generations:\n        # Delete audio file\n        audio_path = Path(generation.audio_path)\n        if audio_path.exists():\n            audio_path.unlink()\n        \n        # Delete from database\n        db.delete(generation)\n        count += 1\n    \n    db.commit()\n    \n    return count\n\n\nasync def get_generation_stats(db: Session) -> dict:\n    \"\"\"\n    Get generation statistics.\n    \n    Args:\n        db: Database session\n        \n    Returns:\n        Statistics dictionary\n    \"\"\"\n    from sqlalchemy import func\n    \n    total = db.query(func.count(DBGeneration.id)).scalar()\n    \n    total_duration = db.query(func.sum(DBGeneration.duration)).scalar() or 0\n    \n    # Get generations by profile\n    by_profile = db.query(\n        DBGeneration.profile_id,\n        func.count(DBGeneration.id).label('count')\n    ).group_by(DBGeneration.profile_id).all()\n    \n    return {\n        \"total_generations\": total,\n        \"total_duration_seconds\": total_duration,\n        \"generations_by_profile\": {\n            profile_id: count for profile_id, count in by_profile\n        },\n    }\n"
  },
  {
    "path": "backend/services/profiles.py",
    "content": "\"\"\"Voice profile management module.\"\"\"\n\nimport json as _json\nimport logging\nimport shutil\nimport uuid\nfrom datetime import datetime\nfrom pathlib import Path\n\nfrom sqlalchemy import func\nfrom sqlalchemy.orm import Session\n\nfrom .. import config\nfrom ..database import Generation as DBGeneration, ProfileSample as DBProfileSample, VoiceProfile as DBVoiceProfile\nfrom ..models import (\n    EffectConfig,\n    ProfileSampleResponse,\n    VoiceProfileCreate,\n    VoiceProfileResponse,\n)\nfrom ..utils.audio import save_audio, validate_and_load_reference_audio\nfrom ..utils.cache import _get_cache_dir, clear_profile_cache\nfrom ..utils.images import process_avatar, validate_image\n\nlogger = logging.getLogger(__name__)\n\nCLONING_ENGINES = {\"qwen\", \"luxtts\", \"chatterbox\", \"chatterbox_turbo\", \"tada\"}\n\n\ndef _profile_to_response(\n    profile: DBVoiceProfile,\n    generation_count: int = 0,\n    sample_count: int = 0,\n) -> VoiceProfileResponse:\n    \"\"\"Convert a DB profile to a VoiceProfileResponse, deserializing effects_chain.\"\"\"\n    effects_chain = None\n    if profile.effects_chain:\n        try:\n            raw = _json.loads(profile.effects_chain)\n            effects_chain = [EffectConfig(**e) for e in raw]\n        except Exception as e:\n            import logging\n\n            logging.warning(f\"Failed to parse effects_chain for profile {profile.id}: {e}\")\n    return VoiceProfileResponse(\n        id=profile.id,\n        name=profile.name,\n        description=profile.description,\n        language=profile.language,\n        avatar_path=profile.avatar_path,\n        effects_chain=effects_chain,\n        voice_type=getattr(profile, \"voice_type\", None) or \"cloned\",\n        preset_engine=getattr(profile, \"preset_engine\", None),\n        preset_voice_id=getattr(profile, \"preset_voice_id\", None),\n        design_prompt=getattr(profile, \"design_prompt\", None),\n        default_engine=getattr(profile, \"default_engine\", None),\n        generation_count=generation_count,\n        sample_count=sample_count,\n        created_at=profile.created_at,\n        updated_at=profile.updated_at,\n    )\n\n\ndef _validate_profile_fields(\n    *,\n    voice_type: str,\n    preset_engine: str | None,\n    preset_voice_id: str | None,\n    design_prompt: str | None,\n    default_engine: str | None,\n) -> str | None:\n    if voice_type == \"preset\":\n        if not preset_engine or not preset_voice_id:\n            return \"Preset profiles require both preset_engine and preset_voice_id\"\n        if default_engine and default_engine != preset_engine:\n            return \"Preset profiles must use their preset_engine as default_engine\"\n        return None\n\n    if voice_type == \"designed\":\n        if not design_prompt or not design_prompt.strip():\n            return \"Designed profiles require a design_prompt\"\n        if preset_engine or preset_voice_id:\n            return \"Designed profiles cannot set preset_engine or preset_voice_id\"\n        return None\n\n    if preset_engine or preset_voice_id:\n        return \"Cloned profiles cannot set preset_engine or preset_voice_id\"\n    if design_prompt:\n        return \"Cloned profiles cannot set design_prompt\"\n    if default_engine and default_engine not in CLONING_ENGINES:\n        return f\"Cloned profiles cannot use default engine '{default_engine}'\"\n    return None\n\n\nasync def create_profile(\n    data: VoiceProfileCreate,\n    db: Session,\n) -> VoiceProfileResponse:\n    \"\"\"\n    Create a new voice profile.\n\n    Args:\n        data: Profile creation data\n        db: Database session\n\n    Returns:\n        Created profile\n\n    Raises:\n        ValueError: If a profile with the same name already exists\n    \"\"\"\n    existing_profile = db.query(DBVoiceProfile).filter_by(name=data.name).first()\n    if existing_profile:\n        raise ValueError(f\"A profile with the name '{data.name}' already exists. Please choose a different name.\")\n\n    # Auto-set default_engine for preset profiles\n    default_engine = data.default_engine\n    voice_type = data.voice_type or \"cloned\"\n    if voice_type == \"preset\" and data.preset_engine and not default_engine:\n        default_engine = data.preset_engine\n\n    validation_error = _validate_profile_fields(\n        voice_type=voice_type,\n        preset_engine=data.preset_engine,\n        preset_voice_id=data.preset_voice_id,\n        design_prompt=data.design_prompt,\n        default_engine=default_engine,\n    )\n    if validation_error:\n        raise ValueError(validation_error)\n\n    db_profile = DBVoiceProfile(\n        id=str(uuid.uuid4()),\n        name=data.name,\n        description=data.description,\n        language=data.language,\n        voice_type=voice_type,\n        preset_engine=data.preset_engine,\n        preset_voice_id=data.preset_voice_id,\n        design_prompt=data.design_prompt,\n        default_engine=default_engine,\n        created_at=datetime.utcnow(),\n        updated_at=datetime.utcnow(),\n    )\n\n    db.add(db_profile)\n    db.commit()\n    db.refresh(db_profile)\n\n    profile_dir = config.get_profiles_dir() / db_profile.id\n    profile_dir.mkdir(parents=True, exist_ok=True)\n\n    return _profile_to_response(db_profile)\n\n\nasync def add_profile_sample(\n    profile_id: str,\n    audio_path: str,\n    reference_text: str,\n    db: Session,\n) -> ProfileSampleResponse:\n    \"\"\"\n    Add a sample to a voice profile.\n\n    Args:\n        profile_id: Profile ID\n        audio_path: Path to temporary audio file\n        reference_text: Transcript of audio\n        db: Database session\n\n    Returns:\n        Created sample\n    \"\"\"\n    import asyncio\n\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        raise ValueError(f\"Profile {profile_id} not found\")\n\n    # Validate and load audio in a single pass, off the event loop\n    is_valid, error_msg, audio, sr = await asyncio.to_thread(\n        validate_and_load_reference_audio, audio_path\n    )\n    if not is_valid:\n        raise ValueError(f\"Invalid reference audio: {error_msg}\")\n\n    sample_id = str(uuid.uuid4())\n    profile_dir = config.get_profiles_dir() / profile_id\n    profile_dir.mkdir(parents=True, exist_ok=True)\n\n    dest_path = profile_dir / f\"{sample_id}.wav\"\n    await asyncio.to_thread(save_audio, audio, str(dest_path), sr)\n\n    db_sample = DBProfileSample(\n        id=sample_id,\n        profile_id=profile_id,\n        audio_path=str(dest_path),\n        reference_text=reference_text,\n    )\n\n    db.add(db_sample)\n\n    profile.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(db_sample)\n\n    # Invalidate combined audio cache for this profile\n    # Since a new sample was added, any cached combined audio is now stale\n    clear_profile_cache(profile_id)\n\n    return ProfileSampleResponse.model_validate(db_sample)\n\n\nasync def get_profile(\n    profile_id: str,\n    db: Session,\n) -> VoiceProfileResponse | None:\n    \"\"\"\n    Get a voice profile by ID.\n\n    Args:\n        profile_id: Profile ID\n        db: Database session\n\n    Returns:\n        Profile or None if not found\n    \"\"\"\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        return None\n\n    return _profile_to_response(profile)\n\n\nasync def get_profile_samples(\n    profile_id: str,\n    db: Session,\n) -> list[ProfileSampleResponse]:\n    \"\"\"\n    Get all samples for a profile.\n\n    Args:\n        profile_id: Profile ID\n        db: Database session\n\n    Returns:\n        List of samples\n    \"\"\"\n    samples = db.query(DBProfileSample).filter_by(profile_id=profile_id).all()\n    return [ProfileSampleResponse.model_validate(s) for s in samples]\n\n\nasync def list_profiles(db: Session) -> list[VoiceProfileResponse]:\n    \"\"\"\n    List all voice profiles with generation and sample counts.\n\n    Args:\n        db: Database session\n\n    Returns:\n        List of profiles\n    \"\"\"\n    profiles = db.query(DBVoiceProfile).order_by(DBVoiceProfile.created_at.desc()).all()\n\n    if not profiles:\n        return []\n\n    # Batch-fetch generation counts\n    gen_counts_rows = (\n        db.query(DBGeneration.profile_id, func.count(DBGeneration.id)).group_by(DBGeneration.profile_id).all()\n    )\n    gen_counts = {row[0]: row[1] for row in gen_counts_rows}\n\n    # Batch-fetch sample counts\n    sample_counts_rows = (\n        db.query(DBProfileSample.profile_id, func.count(DBProfileSample.id)).group_by(DBProfileSample.profile_id).all()\n    )\n    sample_counts = {row[0]: row[1] for row in sample_counts_rows}\n\n    return [\n        _profile_to_response(\n            p,\n            generation_count=gen_counts.get(p.id, 0),\n            sample_count=sample_counts.get(p.id, 0),\n        )\n        for p in profiles\n    ]\n\n\nasync def update_profile(\n    profile_id: str,\n    data: VoiceProfileCreate,\n    db: Session,\n) -> VoiceProfileResponse | None:\n    \"\"\"\n    Update a voice profile.\n\n    Args:\n        profile_id: Profile ID\n        data: Updated profile data\n        db: Database session\n\n    Returns:\n        Updated profile or None if not found\n\n    Raises:\n        ValueError: If a profile with the same name already exists (different profile)\n    \"\"\"\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        return None\n\n    if profile.name != data.name:\n        existing_profile = db.query(DBVoiceProfile).filter_by(name=data.name).first()\n        if existing_profile:\n            raise ValueError(f\"A profile with the name '{data.name}' already exists. Please choose a different name.\")\n\n    voice_type = getattr(profile, \"voice_type\", None) or \"cloned\"\n    preset_engine = getattr(profile, \"preset_engine\", None)\n    preset_voice_id = getattr(profile, \"preset_voice_id\", None)\n    design_prompt = getattr(profile, \"design_prompt\", None)\n    default_engine = data.default_engine if data.default_engine is not None else getattr(profile, \"default_engine\", None)\n\n    validation_error = _validate_profile_fields(\n        voice_type=voice_type,\n        preset_engine=preset_engine,\n        preset_voice_id=preset_voice_id,\n        design_prompt=design_prompt,\n        default_engine=default_engine,\n    )\n    if validation_error:\n        raise ValueError(validation_error)\n\n    profile.name = data.name\n    profile.description = data.description\n    profile.language = data.language\n    if data.default_engine is not None:\n        profile.default_engine = data.default_engine or None  # empty string → NULL\n    profile.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(profile)\n\n    return _profile_to_response(profile)\n\n\nasync def delete_profile(\n    profile_id: str,\n    db: Session,\n) -> bool:\n    \"\"\"\n    Delete a voice profile and all associated data.\n\n    Args:\n        profile_id: Profile ID\n        db: Database session\n\n    Returns:\n        True if deleted, False if not found\n    \"\"\"\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        return False\n\n    db.query(DBProfileSample).filter_by(profile_id=profile_id).delete()\n\n    db.delete(profile)\n    db.commit()\n\n    profile_dir = config.get_profiles_dir() / profile_id\n    if profile_dir.exists():\n        shutil.rmtree(profile_dir)\n\n    # Clean up combined audio cache files for this profile\n    clear_profile_cache(profile_id)\n\n    return True\n\n\nasync def delete_profile_sample(\n    sample_id: str,\n    db: Session,\n) -> bool:\n    \"\"\"\n    Delete a profile sample.\n\n    Args:\n        sample_id: Sample ID\n        db: Database session\n\n    Returns:\n        True if deleted, False if not found\n    \"\"\"\n    sample = db.query(DBProfileSample).filter_by(id=sample_id).first()\n    if not sample:\n        return False\n\n    # Store profile_id before deleting\n    profile_id = sample.profile_id\n\n    audio_path = Path(sample.audio_path)\n    if audio_path.exists():\n        audio_path.unlink()\n\n    db.delete(sample)\n    db.commit()\n\n    # Invalidate combined audio cache for this profile\n    # Since the sample set changed, any cached combined audio is now stale\n    clear_profile_cache(profile_id)\n\n    return True\n\n\nasync def update_profile_sample(\n    sample_id: str,\n    reference_text: str,\n    db: Session,\n) -> ProfileSampleResponse | None:\n    \"\"\"\n    Update a profile sample's reference text.\n\n    Args:\n        sample_id: Sample ID\n        reference_text: Updated reference text\n        db: Database session\n\n    Returns:\n        Updated sample or None if not found\n    \"\"\"\n    sample = db.query(DBProfileSample).filter_by(id=sample_id).first()\n    if not sample:\n        return None\n\n    # Store profile_id before updating\n    profile_id = sample.profile_id\n\n    sample.reference_text = reference_text\n    db.commit()\n    db.refresh(sample)\n\n    # Invalidate combined audio cache for this profile\n    # Since the reference text changed, cache keys and combined text are now stale\n    clear_profile_cache(profile_id)\n\n    return ProfileSampleResponse.model_validate(sample)\n\n\nasync def create_voice_prompt_for_profile(\n    profile_id: str,\n    db: Session,\n    use_cache: bool = True,\n    engine: str = \"qwen\",\n) -> dict:\n    \"\"\"\n    Create a voice prompt from a profile.\n\n    For cloned profiles: combines all audio samples into a voice prompt.\n    For preset profiles: returns the engine-specific preset voice reference.\n    For designed profiles: returns the text design prompt (future).\n\n    Args:\n        profile_id: Profile ID\n        db: Database session\n        use_cache: Whether to use cached prompts\n        engine: TTS engine to create prompt for\n\n    Returns:\n        Voice prompt dictionary\n    \"\"\"\n    from ..backends import get_tts_backend_for_engine\n\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        raise ValueError(f\"Profile not found: {profile_id}\")\n\n    voice_type = getattr(profile, \"voice_type\", None) or \"cloned\"\n\n    # ── Preset profiles: return engine-specific voice reference ──\n    if voice_type == \"preset\":\n        if not profile.preset_engine or not profile.preset_voice_id:\n            raise ValueError(f\"Preset profile {profile_id} is missing preset engine metadata\")\n        if profile.preset_engine != engine:\n            raise ValueError(\n                f\"Preset profile {profile_id} only supports engine '{profile.preset_engine}', not '{engine}'\"\n            )\n        return {\n            \"voice_type\": \"preset\",\n            \"preset_engine\": profile.preset_engine,\n            \"preset_voice_id\": profile.preset_voice_id,\n        }\n\n    # ── Designed profiles: return text description (future) ──\n    if voice_type == \"designed\":\n        if not profile.design_prompt or not profile.design_prompt.strip():\n            raise ValueError(f\"Designed profile {profile_id} is missing design_prompt\")\n        return {\n            \"voice_type\": \"designed\",\n            \"design_prompt\": profile.design_prompt,\n        }\n\n    if engine not in CLONING_ENGINES:\n        raise ValueError(f\"Engine '{engine}' does not support cloned voice profiles\")\n\n    # ── Cloned profiles: create from audio samples ──\n    samples = db.query(DBProfileSample).filter_by(profile_id=profile_id).all()\n\n    if not samples:\n        raise ValueError(f\"No samples found for profile {profile_id}\")\n\n    tts_model = get_tts_backend_for_engine(engine)\n\n    if len(samples) == 1:\n        sample = samples[0]\n        voice_prompt, _ = await tts_model.create_voice_prompt(\n            sample.audio_path,\n            sample.reference_text,\n            use_cache=use_cache,\n        )\n        return voice_prompt\n\n    audio_paths = [s.audio_path for s in samples]\n    reference_texts = [s.reference_text for s in samples]\n\n    combined_audio, combined_text = await tts_model.combine_voice_prompts(\n        audio_paths,\n        reference_texts,\n    )\n\n    # Save combined audio to cache directory (persistent)\n    # Create a hash of sample IDs to identify this specific combination\n    import hashlib\n\n    sample_ids_str = \"-\".join(sorted([s.id for s in samples]))\n    combination_hash = hashlib.md5(sample_ids_str.encode()).hexdigest()[:12]\n\n    cache_dir = _get_cache_dir()\n    cache_dir.mkdir(parents=True, exist_ok=True)\n    combined_path = cache_dir / f\"combined_{profile_id}_{combination_hash}.wav\"\n\n    save_audio(combined_audio, str(combined_path), 24000)\n\n    voice_prompt, _ = await tts_model.create_voice_prompt(\n        str(combined_path),\n        combined_text,\n        use_cache=use_cache,\n    )\n    return voice_prompt\n\n\nasync def upload_avatar(\n    profile_id: str,\n    image_path: str,\n    db: Session,\n) -> VoiceProfileResponse:\n    \"\"\"\n    Upload and process avatar image for a profile.\n\n    Args:\n        profile_id: Profile ID\n        image_path: Path to uploaded image file\n        db: Database session\n\n    Returns:\n        Updated profile\n    \"\"\"\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        raise ValueError(f\"Profile {profile_id} not found\")\n\n    is_valid, error_msg = validate_image(image_path)\n    if not is_valid:\n        raise ValueError(error_msg)\n\n    if profile.avatar_path:\n        old_avatar = Path(profile.avatar_path)\n        if old_avatar.exists():\n            old_avatar.unlink()\n\n    # Determine file extension from uploaded file\n    from PIL import Image\n\n    with Image.open(image_path) as img:\n        # Normalize JPEG variants (MPO is multi-picture format from some cameras)\n        img_format = img.format\n        if img_format in (\"MPO\", \"JPG\"):\n            img_format = \"JPEG\"\n\n        ext_map = {\"PNG\": \".png\", \"JPEG\": \".jpg\", \"WEBP\": \".webp\"}\n        ext = ext_map.get(img_format, \".png\")\n\n    profile_dir = config.get_profiles_dir() / profile_id\n    profile_dir.mkdir(parents=True, exist_ok=True)\n    output_path = profile_dir / f\"avatar{ext}\"\n\n    process_avatar(image_path, str(output_path))\n\n    profile.avatar_path = str(output_path)\n    profile.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(profile)\n\n    return _profile_to_response(profile)\n\n\nasync def delete_avatar(\n    profile_id: str,\n    db: Session,\n) -> bool:\n    \"\"\"\n    Delete avatar image for a profile.\n\n    Args:\n        profile_id: Profile ID\n        db: Database session\n\n    Returns:\n        True if deleted, False if not found or no avatar\n    \"\"\"\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile or not profile.avatar_path:\n        return False\n\n    avatar_path = Path(profile.avatar_path)\n    if avatar_path.exists():\n        avatar_path.unlink()\n\n    profile.avatar_path = None\n    profile.updated_at = datetime.utcnow()\n\n    db.commit()\n\n    return True\n"
  },
  {
    "path": "backend/services/stories.py",
    "content": "\"\"\"\nStory management module.\n\"\"\"\n\nfrom typing import List, Optional\nfrom datetime import datetime\nimport uuid\nimport tempfile\nfrom pathlib import Path\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy import func\n\nfrom ..models import (\n    StoryCreate,\n    StoryResponse,\n    StoryDetailResponse,\n    StoryItemDetail,\n    StoryItemCreate,\n    StoryItemBatchUpdate,\n    StoryItemMove,\n    StoryItemTrim,\n    StoryItemSplit,\n    StoryItemVersionUpdate,\n)\nfrom ..database import (\n    Story as DBStory,\n    StoryItem as DBStoryItem,\n    Generation as DBGeneration,\n    VoiceProfile as DBVoiceProfile,\n)\nfrom .history import _get_versions_for_generation\nfrom ..utils.audio import load_audio, save_audio\nimport numpy as np\n\n\ndef _build_item_detail(\n    item: DBStoryItem,\n    generation: DBGeneration,\n    profile_name: str,\n    db: Session,\n) -> StoryItemDetail:\n    \"\"\"Build a StoryItemDetail with version info from a story item and its generation.\"\"\"\n    versions, active_version_id = _get_versions_for_generation(generation.id, db)\n\n    # Resolve the audio path: if version_id is set, use that version's audio\n    audio_path = generation.audio_path\n    if item.version_id and versions:\n        for v in versions:\n            if v.id == item.version_id:\n                audio_path = v.audio_path\n                break\n\n    return StoryItemDetail(\n        id=item.id,\n        story_id=item.story_id,\n        generation_id=item.generation_id,\n        version_id=getattr(item, \"version_id\", None),\n        start_time_ms=item.start_time_ms,\n        track=item.track,\n        trim_start_ms=getattr(item, \"trim_start_ms\", 0),\n        trim_end_ms=getattr(item, \"trim_end_ms\", 0),\n        created_at=item.created_at,\n        profile_id=generation.profile_id,\n        profile_name=profile_name,\n        text=generation.text,\n        language=generation.language,\n        audio_path=audio_path,\n        duration=generation.duration,\n        seed=generation.seed,\n        instruct=generation.instruct,\n        generation_created_at=generation.created_at,\n        versions=versions,\n        active_version_id=active_version_id,\n    )\n\n\nasync def create_story(\n    data: StoryCreate,\n    db: Session,\n) -> StoryResponse:\n    \"\"\"\n    Create a new story.\n\n    Args:\n        data: Story creation data\n        db: Database session\n\n    Returns:\n        Created story\n    \"\"\"\n    db_story = DBStory(\n        id=str(uuid.uuid4()),\n        name=data.name,\n        description=data.description,\n        created_at=datetime.utcnow(),\n        updated_at=datetime.utcnow(),\n    )\n\n    db.add(db_story)\n    db.commit()\n    db.refresh(db_story)\n\n    item_count = db.query(func.count(DBStoryItem.id)).filter(DBStoryItem.story_id == db_story.id).scalar()\n\n    response = StoryResponse.model_validate(db_story)\n    response.item_count = item_count\n    return response\n\n\nasync def list_stories(\n    db: Session,\n) -> List[StoryResponse]:\n    \"\"\"\n    List all stories.\n\n    Args:\n        db: Database session\n\n    Returns:\n        List of stories with item counts\n    \"\"\"\n    stories = db.query(DBStory).order_by(DBStory.updated_at.desc()).all()\n\n    result = []\n    for story in stories:\n        item_count = db.query(func.count(DBStoryItem.id)).filter(DBStoryItem.story_id == story.id).scalar()\n\n        response = StoryResponse.model_validate(story)\n        response.item_count = item_count\n        result.append(response)\n\n    return result\n\n\nasync def get_story(\n    story_id: str,\n    db: Session,\n) -> Optional[StoryDetailResponse]:\n    \"\"\"\n    Get a story with all its items.\n\n    Args:\n        story_id: Story ID\n        db: Database session\n\n    Returns:\n        Story with items or None if not found\n    \"\"\"\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if not story:\n        return None\n\n    items = (\n        db.query(DBStoryItem, DBGeneration, DBVoiceProfile.name.label(\"profile_name\"))\n        .join(DBGeneration, DBStoryItem.generation_id == DBGeneration.id)\n        .join(DBVoiceProfile, DBGeneration.profile_id == DBVoiceProfile.id)\n        .filter(DBStoryItem.story_id == story_id)\n        .order_by(DBStoryItem.start_time_ms)\n        .all()\n    )\n\n    item_details = []\n    for item, generation, profile_name in items:\n        item_details.append(_build_item_detail(item, generation, profile_name, db))\n\n    response = StoryDetailResponse.model_validate(story)\n    response.items = item_details\n    return response\n\n\nasync def update_story(\n    story_id: str,\n    data: StoryCreate,\n    db: Session,\n) -> Optional[StoryResponse]:\n    \"\"\"\n    Update a story.\n\n    Args:\n        story_id: Story ID\n        data: Update data\n        db: Database session\n\n    Returns:\n        Updated story or None if not found\n    \"\"\"\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if not story:\n        return None\n\n    story.name = data.name\n    story.description = data.description\n    story.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(story)\n\n    item_count = db.query(func.count(DBStoryItem.id)).filter(DBStoryItem.story_id == story.id).scalar()\n\n    response = StoryResponse.model_validate(story)\n    response.item_count = item_count\n    return response\n\n\nasync def delete_story(\n    story_id: str,\n    db: Session,\n) -> bool:\n    \"\"\"\n    Delete a story and all its items.\n\n    Args:\n        story_id: Story ID\n        db: Database session\n\n    Returns:\n        True if deleted, False if not found\n    \"\"\"\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if not story:\n        return False\n\n    # Delete all items\n    db.query(DBStoryItem).filter_by(story_id=story_id).delete()\n\n    # Delete story\n    db.delete(story)\n    db.commit()\n\n    return True\n\n\nasync def add_item_to_story(\n    story_id: str,\n    data: StoryItemCreate,\n    db: Session,\n) -> Optional[StoryItemDetail]:\n    \"\"\"\n    Add a generation to a story.\n\n    Args:\n        story_id: Story ID\n        data: Item creation data\n        db: Database session\n\n    Returns:\n        Created item detail or None if story/generation not found\n    \"\"\"\n    # Verify story exists\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if not story:\n        return None\n\n    # Verify generation exists\n    generation = db.query(DBGeneration).filter_by(id=data.generation_id).first()\n    if not generation:\n        return None\n\n    # Check if generation is already in story\n    existing = db.query(DBStoryItem).filter_by(story_id=story_id, generation_id=data.generation_id).first()\n    if existing:\n        # Return existing item\n        profile = db.query(DBVoiceProfile).filter_by(id=generation.profile_id).first()\n        return _build_item_detail(existing, generation, profile.name if profile else \"Unknown\", db)\n\n    # Get track from data or default to 0\n    track = data.track if data.track is not None else 0\n\n    # Calculate start_time_ms if not provided\n    if data.start_time_ms is not None:\n        start_time_ms = data.start_time_ms\n    else:\n        existing_items = (\n            db.query(DBStoryItem, DBGeneration)\n            .join(DBGeneration, DBStoryItem.generation_id == DBGeneration.id)\n            .filter(\n                DBStoryItem.story_id == story_id,\n                DBStoryItem.track == track,\n            )\n            .all()\n        )\n\n        if not existing_items:\n            start_time_ms = 0\n        else:\n            max_end_time_ms = 0\n            for item, gen in existing_items:\n                item_end_ms = item.start_time_ms + int(gen.duration * 1000)\n                max_end_time_ms = max(max_end_time_ms, item_end_ms)\n\n            # Add 200ms gap after the last item\n            start_time_ms = max_end_time_ms + 200\n\n    # Create item\n    item = DBStoryItem(\n        id=str(uuid.uuid4()),\n        story_id=story_id,\n        generation_id=data.generation_id,\n        start_time_ms=start_time_ms,\n        track=track,\n        created_at=datetime.utcnow(),\n    )\n\n    db.add(item)\n\n    # Update story updated_at\n    story.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(item)\n\n    # Get profile name\n    profile = db.query(DBVoiceProfile).filter_by(id=generation.profile_id).first()\n\n    return _build_item_detail(item, generation, profile.name if profile else \"Unknown\", db)\n\n\nasync def move_story_item(\n    story_id: str,\n    item_id: str,\n    data: StoryItemMove,\n    db: Session,\n) -> Optional[StoryItemDetail]:\n    \"\"\"\n    Move a story item (update position and/or track).\n\n    Args:\n        story_id: Story ID\n        item_id: Story item ID\n        data: New position and track data\n        db: Database session\n\n    Returns:\n        Updated item detail or None if not found\n    \"\"\"\n    # Get the item\n    item = (\n        db.query(DBStoryItem)\n        .filter_by(\n            id=item_id,\n            story_id=story_id,\n        )\n        .first()\n    )\n    if not item:\n        return None\n\n    # Get the generation\n    generation = db.query(DBGeneration).filter_by(id=item.generation_id).first()\n    if not generation:\n        return None\n\n    # Update position and track\n    item.start_time_ms = data.start_time_ms\n    item.track = data.track\n\n    # Update story updated_at\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if story:\n        story.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(item)\n\n    # Get profile name\n    profile = db.query(DBVoiceProfile).filter_by(id=generation.profile_id).first()\n\n    return _build_item_detail(item, generation, profile.name if profile else \"Unknown\", db)\n\n\nasync def remove_item_from_story(\n    story_id: str,\n    item_id: str,\n    db: Session,\n) -> bool:\n    \"\"\"\n    Remove a story item from a story.\n\n    Args:\n        story_id: Story ID\n        item_id: Story item ID to remove\n        db: Database session\n\n    Returns:\n        True if removed, False if not found\n    \"\"\"\n    item = (\n        db.query(DBStoryItem)\n        .filter_by(\n            id=item_id,\n            story_id=story_id,\n        )\n        .first()\n    )\n    if not item:\n        return False\n\n    # Delete item\n    db.delete(item)\n\n    # Update story updated_at\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if story:\n        story.updated_at = datetime.utcnow()\n\n    db.commit()\n    return True\n\n\nasync def trim_story_item(\n    story_id: str,\n    item_id: str,\n    data: StoryItemTrim,\n    db: Session,\n) -> Optional[StoryItemDetail]:\n    \"\"\"\n    Trim a story item (update trim_start_ms and trim_end_ms).\n\n    Args:\n        story_id: Story ID\n        item_id: Story item ID\n        data: Trim data (trim_start_ms, trim_end_ms)\n        db: Database session\n\n    Returns:\n        Updated item detail or None if not found\n    \"\"\"\n    # Get the item\n    item = (\n        db.query(DBStoryItem)\n        .filter_by(\n            id=item_id,\n            story_id=story_id,\n        )\n        .first()\n    )\n    if not item:\n        return None\n\n    # Get the generation\n    generation = db.query(DBGeneration).filter_by(id=item.generation_id).first()\n    if not generation:\n        return None\n\n    # Validate trim values don't exceed duration\n    max_duration_ms = int(generation.duration * 1000)\n    if data.trim_start_ms + data.trim_end_ms >= max_duration_ms:\n        return None  # Invalid trim - would result in zero or negative duration\n\n    # Update trim values\n    item.trim_start_ms = data.trim_start_ms\n    item.trim_end_ms = data.trim_end_ms\n\n    # Update story updated_at\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if story:\n        story.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(item)\n\n    # Get profile name\n    profile = db.query(DBVoiceProfile).filter_by(id=generation.profile_id).first()\n\n    return _build_item_detail(item, generation, profile.name if profile else \"Unknown\", db)\n\n\nasync def split_story_item(\n    story_id: str,\n    item_id: str,\n    data: StoryItemSplit,\n    db: Session,\n) -> Optional[List[StoryItemDetail]]:\n    \"\"\"\n    Split a story item at a given time, creating two clips.\n\n    Args:\n        story_id: Story ID\n        item_id: Story item ID to split\n        data: Split data (split_time_ms - time within clip to split at)\n        db: Database session\n\n    Returns:\n        List of two updated item details (original and new) or None if not found/invalid\n    \"\"\"\n    # Get the item\n    item = (\n        db.query(DBStoryItem)\n        .filter_by(\n            id=item_id,\n            story_id=story_id,\n        )\n        .first()\n    )\n    if not item:\n        return None\n\n    # Get the generation\n    generation = db.query(DBGeneration).filter_by(id=item.generation_id).first()\n    if not generation:\n        return None\n\n    # Calculate effective duration and validate split point\n    current_trim_start = getattr(item, \"trim_start_ms\", 0)\n    current_trim_end = getattr(item, \"trim_end_ms\", 0)\n    original_duration_ms = int(generation.duration * 1000)\n    effective_duration_ms = original_duration_ms - current_trim_start - current_trim_end\n\n    # Validate split_time_ms is within the effective duration\n    if data.split_time_ms <= 0 or data.split_time_ms >= effective_duration_ms:\n        return None  # Invalid split point\n\n    # Calculate the absolute time in the original audio where we're splitting\n    absolute_split_ms = current_trim_start + data.split_time_ms\n\n    # Update original clip: trim from the end\n    item.trim_end_ms = original_duration_ms - absolute_split_ms\n\n    # Create new clip: starts after the split, trimmed from the start\n    new_item = DBStoryItem(\n        id=str(uuid.uuid4()),\n        story_id=story_id,\n        generation_id=item.generation_id,  # Same generation, different trim\n        version_id=getattr(item, \"version_id\", None),  # Preserve pinned version\n        start_time_ms=item.start_time_ms + data.split_time_ms,\n        track=item.track,\n        trim_start_ms=absolute_split_ms,\n        trim_end_ms=current_trim_end,\n        created_at=datetime.utcnow(),\n    )\n\n    db.add(new_item)\n\n    # Update story updated_at\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if story:\n        story.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(item)\n    db.refresh(new_item)\n\n    # Get profile name\n    profile = db.query(DBVoiceProfile).filter_by(id=generation.profile_id).first()\n    profile_name = profile.name if profile else \"Unknown\"\n\n    return [\n        _build_item_detail(item, generation, profile_name, db),\n        _build_item_detail(new_item, generation, profile_name, db),\n    ]\n\n\nasync def duplicate_story_item(\n    story_id: str,\n    item_id: str,\n    db: Session,\n) -> Optional[StoryItemDetail]:\n    \"\"\"\n    Duplicate a story item, creating a copy with all properties.\n\n    Args:\n        story_id: Story ID\n        item_id: Story item ID to duplicate\n        db: Database session\n\n    Returns:\n        New item detail or None if not found\n    \"\"\"\n    # Get the original item\n    original_item = (\n        db.query(DBStoryItem)\n        .filter_by(\n            id=item_id,\n            story_id=story_id,\n        )\n        .first()\n    )\n    if not original_item:\n        return None\n\n    # Get the generation\n    generation = db.query(DBGeneration).filter_by(id=original_item.generation_id).first()\n    if not generation:\n        return None\n\n    # Calculate effective duration\n    current_trim_start = getattr(original_item, \"trim_start_ms\", 0)\n    current_trim_end = getattr(original_item, \"trim_end_ms\", 0)\n    original_duration_ms = int(generation.duration * 1000)\n    effective_duration_ms = original_duration_ms - current_trim_start - current_trim_end\n\n    # Create duplicate item - place it right after the original\n    new_item = DBStoryItem(\n        id=str(uuid.uuid4()),\n        story_id=story_id,\n        generation_id=original_item.generation_id,  # Same generation as original\n        version_id=getattr(original_item, \"version_id\", None),  # Preserve pinned version\n        start_time_ms=original_item.start_time_ms + effective_duration_ms + 200,  # 200ms gap\n        track=original_item.track,\n        trim_start_ms=current_trim_start,\n        trim_end_ms=current_trim_end,\n        created_at=datetime.utcnow(),\n    )\n\n    db.add(new_item)\n\n    # Update story updated_at\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if story:\n        story.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(new_item)\n\n    # Get profile name\n    profile = db.query(DBVoiceProfile).filter_by(id=generation.profile_id).first()\n\n    return _build_item_detail(new_item, generation, profile.name if profile else \"Unknown\", db)\n\n\nasync def update_story_item_times(\n    story_id: str,\n    data: StoryItemBatchUpdate,\n    db: Session,\n) -> bool:\n    \"\"\"\n    Update story item timecodes.\n\n    Args:\n        story_id: Story ID\n        data: Batch update data with timecodes\n        db: Database session\n\n    Returns:\n        True if updated, False if story not found or invalid\n    \"\"\"\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if not story:\n        return False\n\n    # Get all items for this story\n    items = db.query(DBStoryItem).filter_by(story_id=story_id).all()\n    item_map = {item.generation_id: item for item in items}\n\n    # Verify all generation IDs belong to this story and update timecodes\n    for update in data.updates:\n        if update.generation_id not in item_map:\n            return False\n        item_map[update.generation_id].start_time_ms = update.start_time_ms\n\n    # Update story updated_at\n    story.updated_at = datetime.utcnow()\n\n    db.commit()\n    return True\n\n\nasync def reorder_story_items(\n    story_id: str,\n    generation_ids: List[str],\n    db: Session,\n    gap_ms: int = 200,\n) -> Optional[List[StoryItemDetail]]:\n    \"\"\"\n    Reorder story items and recalculate timecodes.\n\n    Args:\n        story_id: Story ID\n        generation_ids: List of generation IDs in the desired order\n        db: Database session\n        gap_ms: Gap in milliseconds between items (default 200ms)\n\n    Returns:\n        Updated list of story items with new timecodes, or None if invalid\n    \"\"\"\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if not story:\n        return None\n\n    # Get all items for this story with their generation data\n    items_with_gen = (\n        db.query(DBStoryItem, DBGeneration, DBVoiceProfile.name.label(\"profile_name\"))\n        .join(DBGeneration, DBStoryItem.generation_id == DBGeneration.id)\n        .join(DBVoiceProfile, DBGeneration.profile_id == DBVoiceProfile.id)\n        .filter(DBStoryItem.story_id == story_id)\n        .all()\n    )\n\n    # Create maps for quick lookup\n    item_map = {item.generation_id: (item, gen, profile_name) for item, gen, profile_name in items_with_gen}\n\n    # Verify all generation IDs belong to this story\n    if set(generation_ids) != set(item_map.keys()):\n        return None\n\n    # Recalculate timecodes based on new order\n    current_time_ms = 0\n    updated_items = []\n\n    for gen_id in generation_ids:\n        item, generation, profile_name = item_map[gen_id]\n\n        # Update the item's start time\n        item.start_time_ms = current_time_ms\n\n        # Calculate the duration in ms\n        duration_ms = int(generation.duration * 1000)\n\n        # Move to next position (current end + gap)\n        current_time_ms += duration_ms + gap_ms\n\n        # Build the response item\n        updated_items.append(_build_item_detail(item, generation, profile_name, db))\n\n    # Update story updated_at\n    story.updated_at = datetime.utcnow()\n\n    db.commit()\n    return updated_items\n\n\nasync def set_story_item_version(\n    story_id: str,\n    item_id: str,\n    data: StoryItemVersionUpdate,\n    db: Session,\n) -> Optional[StoryItemDetail]:\n    \"\"\"\n    Pin a story item to a specific generation version.\n\n    Args:\n        story_id: Story ID\n        item_id: Story item ID\n        data: Version update data (version_id or null for default)\n        db: Database session\n\n    Returns:\n        Updated item detail or None if not found\n    \"\"\"\n    item = (\n        db.query(DBStoryItem)\n        .filter_by(\n            id=item_id,\n            story_id=story_id,\n        )\n        .first()\n    )\n    if not item:\n        return None\n\n    generation = db.query(DBGeneration).filter_by(id=item.generation_id).first()\n    if not generation:\n        return None\n\n    # Validate version_id belongs to this generation if provided\n    if data.version_id:\n        from ..database import GenerationVersion as DBGenerationVersion\n\n        version = (\n            db.query(DBGenerationVersion)\n            .filter_by(\n                id=data.version_id,\n                generation_id=item.generation_id,\n            )\n            .first()\n        )\n        if not version:\n            return None\n\n    item.version_id = data.version_id\n\n    # Update story updated_at\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if story:\n        story.updated_at = datetime.utcnow()\n\n    db.commit()\n    db.refresh(item)\n\n    profile = db.query(DBVoiceProfile).filter_by(id=generation.profile_id).first()\n\n    return _build_item_detail(item, generation, profile.name if profile else \"Unknown\", db)\n\n\nasync def export_story_audio(\n    story_id: str,\n    db: Session,\n) -> Optional[bytes]:\n    \"\"\"\n    Export story as single mixed audio file with timecode-based mixing.\n\n    Args:\n        story_id: Story ID\n        db: Database session\n\n    Returns:\n        Audio file bytes or None if story not found\n    \"\"\"\n    story = db.query(DBStory).filter_by(id=story_id).first()\n    if not story:\n        return None\n\n    # Get all items ordered by start_time_ms\n    items = (\n        db.query(DBStoryItem, DBGeneration)\n        .join(DBGeneration, DBStoryItem.generation_id == DBGeneration.id)\n        .filter(DBStoryItem.story_id == story_id)\n        .order_by(DBStoryItem.start_time_ms)\n        .all()\n    )\n\n    if not items:\n        return None\n\n    # Load all audio files and calculate total duration\n    audio_data = []\n    sample_rate = 24000  # Default sample rate\n\n    for item, generation in items:\n        # Resolve audio path: use pinned version if set, otherwise generation default\n        resolved_audio_path = generation.audio_path\n        if getattr(item, \"version_id\", None):\n            from ..database import GenerationVersion as DBGenerationVersion\n\n            version = db.query(DBGenerationVersion).filter_by(id=item.version_id).first()\n            if version:\n                resolved_audio_path = version.audio_path\n\n        audio_path = Path(resolved_audio_path)\n        if not audio_path.exists():\n            continue\n\n        try:\n            audio, sr = load_audio(str(audio_path), sample_rate=sample_rate)\n            sample_rate = sr  # Use actual sample rate from first file\n\n            # Get trim values\n            trim_start_ms = getattr(item, \"trim_start_ms\", 0)\n            trim_end_ms = getattr(item, \"trim_end_ms\", 0)\n\n            # Calculate effective duration\n            original_duration_ms = int(generation.duration * 1000)\n            effective_duration_ms = original_duration_ms - trim_start_ms - trim_end_ms\n\n            # Slice audio based on trim values\n            trim_start_sample = int((trim_start_ms / 1000.0) * sample_rate)\n            trim_end_sample = int((trim_end_ms / 1000.0) * sample_rate)\n\n            # Extract the trimmed portion\n            if trim_end_ms > 0:\n                trimmed_audio = (\n                    audio[trim_start_sample:-trim_end_sample] if trim_end_sample > 0 else audio[trim_start_sample:]\n                )\n            else:\n                trimmed_audio = audio[trim_start_sample:]\n\n            # Store audio with its timecode info\n            start_time_ms = item.start_time_ms\n\n            audio_data.append(\n                {\n                    \"audio\": trimmed_audio,\n                    \"start_time_ms\": start_time_ms,\n                    \"duration_ms\": effective_duration_ms,\n                }\n            )\n        except Exception:\n            # Skip files that can't be loaded\n            continue\n\n    if not audio_data:\n        return None\n\n    # Calculate total duration: max(start_time_ms + duration_ms)\n    max_end_time_ms = max((data[\"start_time_ms\"] + data[\"duration_ms\"] for data in audio_data), default=0)\n\n    # Convert to samples\n    total_samples = int((max_end_time_ms / 1000.0) * sample_rate)\n\n    # Create output buffer initialized to zeros\n    final_audio = np.zeros(total_samples, dtype=np.float32)\n\n    # Mix each audio segment at its timecode position\n    for data in audio_data:\n        audio = data[\"audio\"]\n        start_time_ms = data[\"start_time_ms\"]\n\n        # Calculate start sample index\n        start_sample = int((start_time_ms / 1000.0) * sample_rate)\n\n        # Ensure we don't exceed buffer bounds\n        audio_length = len(audio)\n        end_sample = min(start_sample + audio_length, total_samples)\n\n        if start_sample < total_samples:\n            # Trim audio if it extends beyond buffer\n            audio_to_mix = audio[: end_sample - start_sample]\n\n            # Mix: add audio to existing buffer (overlapping audio will sum)\n            # Normalize to prevent clipping (simple approach: divide by max)\n            final_audio[start_sample:end_sample] += audio_to_mix\n\n    # Normalize to prevent clipping\n    max_val = np.abs(final_audio).max()\n    if max_val > 1.0:\n        final_audio = final_audio / max_val\n\n    # Save to temporary file\n    with tempfile.NamedTemporaryFile(suffix=\".wav\", delete=False) as tmp:\n        tmp_path = tmp.name\n\n    try:\n        save_audio(final_audio, tmp_path, sample_rate)\n\n        # Read file bytes\n        with open(tmp_path, \"rb\") as f:\n            audio_bytes = f.read()\n\n        return audio_bytes\n    finally:\n        # Clean up temp file\n        Path(tmp_path).unlink(missing_ok=True)\n"
  },
  {
    "path": "backend/services/task_queue.py",
    "content": "\"\"\"\nSerial generation queue — ensures only one TTS inference runs at a time\nto avoid GPU contention.\n\"\"\"\n\nimport asyncio\nimport traceback\n\n# Keep references to fire-and-forget background tasks to prevent GC\n_background_tasks: set = set()\n\n# Generation queue — serializes TTS inference to avoid GPU contention\n_generation_queue: asyncio.Queue = None  # type: ignore  # initialized at startup\n\n\ndef create_background_task(coro) -> asyncio.Task:\n    \"\"\"Create a background task and prevent it from being garbage collected.\"\"\"\n    task = asyncio.create_task(coro)\n    _background_tasks.add(task)\n    task.add_done_callback(_background_tasks.discard)\n    return task\n\n\nasync def _generation_worker():\n    \"\"\"Worker that processes generation tasks one at a time.\"\"\"\n    while True:\n        coro = await _generation_queue.get()\n        try:\n            await coro\n        except Exception:\n            traceback.print_exc()\n        finally:\n            _generation_queue.task_done()\n\n\ndef enqueue_generation(coro):\n    \"\"\"Add a generation coroutine to the serial queue.\"\"\"\n    _generation_queue.put_nowait(coro)\n\n\ndef init_queue():\n    \"\"\"Initialize the generation queue and start the worker.\n\n    Must be called once during application startup (inside a running event loop).\n    \"\"\"\n    global _generation_queue\n    _generation_queue = asyncio.Queue()\n    create_background_task(_generation_worker())\n"
  },
  {
    "path": "backend/services/transcribe.py",
    "content": "\"\"\"\nSTT (Speech-to-Text) module - delegates to backend abstraction layer.\n\"\"\"\n\nfrom typing import Optional\nfrom ..backends import get_stt_backend, STTBackend\n\n\ndef get_whisper_model() -> STTBackend:\n    \"\"\"\n    Get STT backend instance (MLX or PyTorch based on platform).\n    \n    Returns:\n        STT backend instance\n    \"\"\"\n    return get_stt_backend()\n\n\ndef unload_whisper_model():\n    \"\"\"Unload Whisper model to free memory.\"\"\"\n    backend = get_stt_backend()\n    backend.unload_model()\n"
  },
  {
    "path": "backend/services/tts.py",
    "content": "\"\"\"\nTTS inference module - delegates to backend abstraction layer.\n\"\"\"\n\nfrom typing import Optional\nimport numpy as np\nimport io\nimport soundfile as sf\n\nfrom ..backends import get_tts_backend, TTSBackend\n\n\ndef get_tts_model() -> TTSBackend:\n    \"\"\"\n    Get TTS backend instance (MLX or PyTorch based on platform).\n    \n    Returns:\n        TTS backend instance\n    \"\"\"\n    return get_tts_backend()\n\n\ndef unload_tts_model():\n    \"\"\"Unload TTS model to free memory.\"\"\"\n    backend = get_tts_backend()\n    backend.unload_model()\n\n\ndef audio_to_wav_bytes(audio: np.ndarray, sample_rate: int) -> bytes:\n    \"\"\"Convert audio array to WAV bytes.\"\"\"\n    buffer = io.BytesIO()\n    sf.write(buffer, audio, sample_rate, format=\"WAV\")\n    buffer.seek(0)\n    return buffer.read()\n"
  },
  {
    "path": "backend/services/versions.py",
    "content": "\"\"\"\nGeneration versions management module.\n\nEach generation can have multiple audio versions: a clean (unprocessed)\nversion and any number of processed versions with different effects chains.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport uuid\nfrom pathlib import Path\nfrom typing import List, Optional\n\nfrom sqlalchemy.orm import Session\n\nfrom ..database import (\n    GenerationVersion as DBGenerationVersion,\n    Generation as DBGeneration,\n)\nfrom ..models import GenerationVersionResponse, EffectConfig\nfrom .. import config\n\n\ndef _version_response(v: DBGenerationVersion) -> GenerationVersionResponse:\n    \"\"\"Convert a DB version row to a Pydantic response.\"\"\"\n    effects_chain = None\n    if v.effects_chain:\n        raw = json.loads(v.effects_chain)\n        effects_chain = [EffectConfig(**e) for e in raw]\n    return GenerationVersionResponse(\n        id=v.id,\n        generation_id=v.generation_id,\n        label=v.label,\n        audio_path=v.audio_path,\n        effects_chain=effects_chain,\n        source_version_id=v.source_version_id,\n        is_default=v.is_default,\n        created_at=v.created_at,\n    )\n\n\ndef list_versions(generation_id: str, db: Session) -> List[GenerationVersionResponse]:\n    \"\"\"List all versions for a generation.\"\"\"\n    versions = (\n        db.query(DBGenerationVersion)\n        .filter_by(generation_id=generation_id)\n        .order_by(DBGenerationVersion.created_at)\n        .all()\n    )\n    return [_version_response(v) for v in versions]\n\n\ndef get_version(version_id: str, db: Session) -> Optional[GenerationVersionResponse]:\n    \"\"\"Get a specific version by ID.\"\"\"\n    v = db.query(DBGenerationVersion).filter_by(id=version_id).first()\n    if not v:\n        return None\n    return _version_response(v)\n\n\ndef get_default_version(generation_id: str, db: Session) -> Optional[GenerationVersionResponse]:\n    \"\"\"Get the default version for a generation.\"\"\"\n    v = (\n        db.query(DBGenerationVersion)\n        .filter_by(generation_id=generation_id, is_default=True)\n        .first()\n    )\n    if not v:\n        # Fallback: return the first version\n        v = (\n            db.query(DBGenerationVersion)\n            .filter_by(generation_id=generation_id)\n            .order_by(DBGenerationVersion.created_at)\n            .first()\n        )\n    if not v:\n        return None\n    return _version_response(v)\n\n\ndef create_version(\n    generation_id: str,\n    label: str,\n    audio_path: str,\n    db: Session,\n    effects_chain: Optional[List[dict]] = None,\n    is_default: bool = False,\n    source_version_id: Optional[str] = None,\n) -> GenerationVersionResponse:\n    \"\"\"Create a new version for a generation.\n\n    If ``is_default`` is True, all other versions for this generation\n    are un-defaulted first.\n    \"\"\"\n    if is_default:\n        _clear_defaults(generation_id, db)\n\n    version = DBGenerationVersion(\n        id=str(uuid.uuid4()),\n        generation_id=generation_id,\n        label=label,\n        audio_path=audio_path,\n        effects_chain=json.dumps(effects_chain) if effects_chain else None,\n        source_version_id=source_version_id,\n        is_default=is_default,\n    )\n    db.add(version)\n    db.commit()\n    db.refresh(version)\n\n    # If this version is the default, update the generation's audio_path\n    if is_default:\n        gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n        if gen:\n            gen.audio_path = audio_path\n            db.commit()\n\n    return _version_response(version)\n\n\ndef set_default_version(version_id: str, db: Session) -> Optional[GenerationVersionResponse]:\n    \"\"\"Set a version as the default for its generation.\"\"\"\n    version = db.query(DBGenerationVersion).filter_by(id=version_id).first()\n    if not version:\n        return None\n\n    _clear_defaults(version.generation_id, db)\n    version.is_default = True\n    db.commit()\n    db.refresh(version)\n\n    # Update generation's audio_path to point to this version\n    gen = db.query(DBGeneration).filter_by(id=version.generation_id).first()\n    if gen:\n        gen.audio_path = version.audio_path\n        db.commit()\n\n    return _version_response(version)\n\n\ndef delete_version(version_id: str, db: Session) -> bool:\n    \"\"\"Delete a version. Cannot delete the last remaining version.\"\"\"\n    version = db.query(DBGenerationVersion).filter_by(id=version_id).first()\n    if not version:\n        return False\n\n    # Don't allow deleting the last version\n    count = (\n        db.query(DBGenerationVersion)\n        .filter_by(generation_id=version.generation_id)\n        .count()\n    )\n    if count <= 1:\n        return False\n\n    was_default = version.is_default\n    gen_id = version.generation_id\n\n    # Delete audio file\n    audio_path = Path(version.audio_path)\n    if audio_path.exists():\n        audio_path.unlink()\n\n    db.delete(version)\n    db.commit()\n\n    # If this was the default, promote the first remaining version\n    if was_default:\n        first = (\n            db.query(DBGenerationVersion)\n            .filter_by(generation_id=gen_id)\n            .order_by(DBGenerationVersion.created_at)\n            .first()\n        )\n        if first:\n            first.is_default = True\n            db.commit()\n            gen = db.query(DBGeneration).filter_by(id=gen_id).first()\n            if gen:\n                gen.audio_path = first.audio_path\n                db.commit()\n\n    return True\n\n\ndef delete_versions_for_generation(generation_id: str, db: Session) -> int:\n    \"\"\"Delete all versions for a generation (used when deleting a generation).\"\"\"\n    versions = (\n        db.query(DBGenerationVersion)\n        .filter_by(generation_id=generation_id)\n        .all()\n    )\n    count = 0\n    for v in versions:\n        audio_path = Path(v.audio_path)\n        if audio_path.exists():\n            audio_path.unlink()\n        db.delete(v)\n        count += 1\n    if count > 0:\n        db.commit()\n    return count\n\n\ndef _clear_defaults(generation_id: str, db: Session) -> None:\n    \"\"\"Clear the is_default flag on all versions for a generation.\"\"\"\n    db.query(DBGenerationVersion).filter_by(\n        generation_id=generation_id, is_default=True\n    ).update({\"is_default\": False})\n    db.flush()\n"
  },
  {
    "path": "backend/tests/README.md",
    "content": "# Backend Tests\n\nManual test scripts for debugging and validating backend functionality.\n\n## Test Files\n\n### `test_generation_progress.py`\nTests TTS generation with SSE progress monitoring to identify UX issues where users see download progress even when the model is already cached.\n\n**Usage:**\n```bash\ncd backend\npython tests/test_generation_progress.py\n```\n\n**Prerequisites:**\n- Server must be running (`python main.py`)\n- At least one voice profile must exist\n\n### `test_real_download.py`\nTests real model download with SSE progress monitoring.\n\n**Usage:**\n```bash\ncd backend\n# Delete cache first to force fresh download\nrm -rf ~/.cache/huggingface/hub/models--openai--whisper-base\npython tests/test_real_download.py\n```\n\n**Prerequisites:**\n- Server must be running (`python main.py`)\n\n### `test_progress.py`\nUnit tests for ProgressManager and HFProgressTracker functionality.\n\n**Usage:**\n```bash\ncd backend\npython tests/test_progress.py\n```\n\n### `test_check_progress_state.py`\nDebugging script to inspect the internal state of ProgressManager and TaskManager.\n\n**Usage:**\n```bash\ncd backend\npython tests/test_check_progress_state.py\n```\n\n## Notes\n\nThese are manual test scripts, not automated unit tests. They're designed for:\n- Debugging progress tracking issues\n- Validating SSE event streams\n- Monitoring real-time download behavior\n- Inspecting internal state during development\n"
  },
  {
    "path": "backend/tests/__init__.py",
    "content": "\"\"\"\nTest suite for Voicebox backend.\n\nThis directory contains manual test scripts for debugging and validating\nprogress tracking, model downloads, and generation functionality.\n\"\"\"\n"
  },
  {
    "path": "backend/tests/test_cors.py",
    "content": "\"\"\"\nTests for CORS origin restrictions.\n\nValidates that the CORS middleware only allows known local origins\nand respects the VOICEBOX_CORS_ORIGINS environment variable.\n\nUses a minimal FastAPI app that mirrors the exact CORS configuration\nfrom backend/main.py, so tests run without heavy ML dependencies.\n\nUsage:\n    pip install httpx pytest fastapi starlette\n    python -m pytest backend/tests/test_cors.py -v\n\"\"\"\n\nimport os\nimport pytest\nfrom unittest.mock import patch\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom starlette.testclient import TestClient\n\n\ndef _build_app(env_origins: str = \"\") -> FastAPI:\n    \"\"\"\n    Build a minimal FastAPI app with the same CORS logic as backend/main.py.\n\n    This mirrors the exact code in main.py so the test validates the real\n    configuration without needing torch/numpy/transformers installed.\n    \"\"\"\n    app = FastAPI()\n\n    _default_origins = [\n        \"http://localhost:5173\",\n        \"http://127.0.0.1:5173\",\n        \"http://localhost:17493\",\n        \"http://127.0.0.1:17493\",\n        \"tauri://localhost\",\n        \"https://tauri.localhost\",\n    ]\n    _cors_origins = _default_origins + [o.strip() for o in env_origins.split(\",\") if o.strip()]\n\n    app.add_middleware(\n        CORSMiddleware,\n        allow_origins=_cors_origins,\n        allow_credentials=True,\n        allow_methods=[\"*\"],\n        allow_headers=[\"*\"],\n    )\n\n    @app.get(\"/health\")\n    async def health():\n        return {\"status\": \"ok\"}\n\n    return app\n\n\n@pytest.fixture()\ndef client():\n    return TestClient(_build_app())\n\n\n@pytest.fixture()\ndef client_with_custom_origins():\n    return TestClient(_build_app(\"https://custom.example.com,https://other.example.com\"))\n\n\ndef _get_with_origin(client: TestClient, origin: str) -> dict:\n    \"\"\"Send a GET with Origin header, return response headers.\"\"\"\n    response = client.get(\"/health\", headers={\"Origin\": origin})\n    return dict(response.headers)\n\n\ndef _preflight(client: TestClient, origin: str) -> dict:\n    \"\"\"Send CORS preflight OPTIONS request, return response headers.\"\"\"\n    response = client.options(\n        \"/health\",\n        headers={\n            \"Origin\": origin,\n            \"Access-Control-Request-Method\": \"GET\",\n        },\n    )\n    return dict(response.headers)\n\n\nclass TestCORSDefaultOrigins:\n    \"\"\"CORS should allow known local origins and block everything else.\"\"\"\n\n    @pytest.mark.parametrize(\"origin\", [\n        \"http://localhost:5173\",\n        \"http://127.0.0.1:5173\",\n        \"http://localhost:17493\",\n        \"http://127.0.0.1:17493\",\n        \"tauri://localhost\",\n        \"https://tauri.localhost\",\n    ])\n    def test_allowed_origins(self, client, origin):\n        headers = _get_with_origin(client, origin)\n        assert headers.get(\"access-control-allow-origin\") == origin\n\n    @pytest.mark.parametrize(\"origin\", [\n        \"http://evil.com\",\n        \"http://localhost:9999\",\n        \"https://attacker.example.com\",\n        \"null\",\n    ])\n    def test_blocked_origins(self, client, origin):\n        headers = _get_with_origin(client, origin)\n        assert \"access-control-allow-origin\" not in headers\n\n    def test_preflight_allowed(self, client):\n        headers = _preflight(client, \"http://localhost:5173\")\n        assert headers.get(\"access-control-allow-origin\") == \"http://localhost:5173\"\n\n    def test_preflight_blocked(self, client):\n        headers = _preflight(client, \"http://evil.com\")\n        assert \"access-control-allow-origin\" not in headers\n\n    def test_credentials_header_present(self, client):\n        headers = _get_with_origin(client, \"http://localhost:5173\")\n        assert headers.get(\"access-control-allow-credentials\") == \"true\"\n\n\nclass TestCORSCustomOrigins:\n    \"\"\"VOICEBOX_CORS_ORIGINS env var should extend the allowlist.\"\"\"\n\n    def test_custom_origin_allowed(self, client_with_custom_origins):\n        headers = _get_with_origin(client_with_custom_origins, \"https://custom.example.com\")\n        assert headers.get(\"access-control-allow-origin\") == \"https://custom.example.com\"\n\n    def test_other_custom_origin_allowed(self, client_with_custom_origins):\n        headers = _get_with_origin(client_with_custom_origins, \"https://other.example.com\")\n        assert headers.get(\"access-control-allow-origin\") == \"https://other.example.com\"\n\n    def test_default_origins_still_work(self, client_with_custom_origins):\n        headers = _get_with_origin(client_with_custom_origins, \"http://localhost:5173\")\n        assert headers.get(\"access-control-allow-origin\") == \"http://localhost:5173\"\n\n    def test_unlisted_origin_still_blocked(self, client_with_custom_origins):\n        headers = _get_with_origin(client_with_custom_origins, \"http://evil.com\")\n        assert \"access-control-allow-origin\" not in headers\n\n\nclass TestCORSEnvVarParsing:\n    \"\"\"Edge cases for VOICEBOX_CORS_ORIGINS parsing.\"\"\"\n\n    def test_empty_env_var(self):\n        app = _build_app(\"\")\n        client = TestClient(app)\n        headers = _get_with_origin(client, \"http://evil.com\")\n        assert \"access-control-allow-origin\" not in headers\n\n    def test_whitespace_trimmed(self):\n        app = _build_app(\"  https://spaced.example.com  \")\n        client = TestClient(app)\n        headers = _get_with_origin(client, \"https://spaced.example.com\")\n        assert headers.get(\"access-control-allow-origin\") == \"https://spaced.example.com\"\n\n    def test_trailing_comma_ignored(self):\n        app = _build_app(\"https://one.example.com,\")\n        client = TestClient(app)\n        headers = _get_with_origin(client, \"https://one.example.com\")\n        assert headers.get(\"access-control-allow-origin\") == \"https://one.example.com\"\n"
  },
  {
    "path": "backend/tests/test_generation_download.py",
    "content": "\"\"\"\nTest TTS generation with SSE progress monitoring.\nThis test captures the exact SSE events triggered during generation\nto identify UX issues where users see download progress even when\nthe model is already cached.\n\"\"\"\n\nimport asyncio\nimport json\nimport httpx\nfrom typing import List, Dict, Optional\nfrom datetime import datetime\n\n\nasync def monitor_sse_stream(model_name: str, timeout: int = 120):\n    \"\"\"Monitor SSE stream for a model during generation.\"\"\"\n    events: List[Dict] = []\n    url = f\"http://localhost:8000/models/progress/{model_name}\"\n\n    print(f\"[{_timestamp()}] Connecting to SSE endpoint: {url}\")\n\n    try:\n        async with httpx.AsyncClient(timeout=timeout) as client:\n            async with client.stream(\"GET\", url) as response:\n                print(f\"[{_timestamp()}] SSE connected, status: {response.status_code}\")\n\n                if response.status_code != 200:\n                    print(f\"[{_timestamp()}] Error: SSE endpoint returned {response.status_code}\")\n                    return events\n\n                async for line in response.aiter_lines():\n                    if not line:\n                        continue\n\n                    timestamp = _timestamp()\n\n                    if line.startswith(\"data: \"):\n                        try:\n                            data = json.loads(line[6:])\n                            print(\n                                f\"[{timestamp}] → SSE Event: {data['status']:12} {data.get('progress', 0):6.1f}% {data.get('filename', '')}\"\n                            )\n                            events.append({**data, \"_timestamp\": timestamp})\n\n                            # Stop if complete or error\n                            if data.get(\"status\") in (\"complete\", \"error\"):\n                                print(f\"[{timestamp}] → Model {data['status']}!\")\n                                break\n\n                        except json.JSONDecodeError as e:\n                            print(f\"[{timestamp}] Error parsing JSON: {e}\")\n                            print(f\"  Line was: {line}\")\n\n                    elif line.startswith(\": heartbeat\"):\n                        print(f\"[{timestamp}] ♥ heartbeat\")\n\n    except asyncio.TimeoutError:\n        print(f\"[{_timestamp()}] SSE monitoring timed out\")\n    except Exception as e:\n        print(f\"[{_timestamp()}] SSE error: {e}\")\n\n    return events\n\n\nasync def trigger_generation(profile_id: str, text: str, model_size: str = \"1.7B\"):\n    \"\"\"Trigger TTS generation via the API.\"\"\"\n    url = \"http://localhost:8000/generate\"\n\n    print(f\"\\n[{_timestamp()}] Triggering generation...\")\n    print(f\"  Profile: {profile_id}\")\n    print(f\"  Text: {text[:50]}...\")\n    print(f\"  Model: {model_size}\")\n\n    try:\n        async with httpx.AsyncClient(timeout=120) as client:\n            response = await client.post(\n                url,\n                json={\n                    \"profile_id\": profile_id,\n                    \"text\": text,\n                    \"language\": \"en\",\n                    \"model_size\": model_size,\n                },\n            )\n\n            print(f\"[{_timestamp()}] Response: {response.status_code}\")\n\n            if response.status_code == 200:\n                result = response.json()\n                print(f\"[{_timestamp()}] ✓ Generation successful!\")\n                print(f\"  Generation ID: {result.get('id')}\")\n                print(f\"  Duration: {result.get('duration', 0):.2f}s\")\n                return True, result\n            elif response.status_code == 202:\n                # Model is being downloaded\n                result = response.json()\n                print(f\"[{_timestamp()}] → Model download in progress\")\n                print(f\"  Detail: {result}\")\n                return False, result\n            else:\n                print(f\"[{_timestamp()}] ✗ Error: {response.text}\")\n                return False, None\n\n    except Exception as e:\n        print(f\"[{_timestamp()}] ✗ Exception: {e}\")\n        return False, None\n\n\nasync def get_first_profile():\n    \"\"\"Get the first available voice profile.\"\"\"\n    url = \"http://localhost:8000/profiles\"\n\n    try:\n        async with httpx.AsyncClient(timeout=10) as client:\n            response = await client.get(url)\n            if response.status_code == 200:\n                profiles = response.json()\n                if profiles:\n                    return profiles[0][\"id\"]\n    except Exception as e:\n        print(f\"Error getting profiles: {e}\")\n\n    return None\n\n\nasync def check_server():\n    \"\"\"Check if the server is running.\"\"\"\n    try:\n        async with httpx.AsyncClient(timeout=5) as client:\n            response = await client.get(\"http://localhost:8000/health\")\n            return response.status_code == 200\n    except Exception as e:\n        print(f\"Server not running: {e}\")\n        return False\n\n\ndef _timestamp():\n    \"\"\"Get current timestamp for logging.\"\"\"\n    return datetime.now().strftime(\"%H:%M:%S.%f\")[:-3]\n\n\nasync def test_generation_with_cached_model():\n    \"\"\"\n    Test Case 1: Generation when model is already cached.\n\n    This should NOT show any download progress events.\n    If it does, that's the UX bug we're trying to fix.\n    \"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"TEST CASE 1: Generation with Cached Model\")\n    print(\"=\" * 80)\n    print(\"Expected: No download progress events (or minimal/instant completion)\")\n    print(\"Actual UX Issue: Users see 'started' and 'finished' events even for cached models\")\n    print(\"=\" * 80)\n\n    model_size = \"1.7B\"\n    model_name = f\"qwen-tts-{model_size}\"\n\n    # Get a profile\n    profile_id = await get_first_profile()\n    if not profile_id:\n        print(\"✗ No voice profiles found. Please create a profile first.\")\n        return False\n\n    print(f\"\\nUsing profile: {profile_id}\")\n\n    # Start SSE monitor BEFORE triggering generation\n    monitor_task = asyncio.create_task(monitor_sse_stream(model_name, timeout=30))\n\n    # Wait for SSE to connect\n    await asyncio.sleep(1)\n\n    # Trigger generation\n    test_text = \"Hello, this is a test of the voice generation system.\"\n    success, result = await trigger_generation(profile_id, test_text, model_size)\n\n    if not success and result and result.get(\"downloading\"):\n        print(\"\\n⚠ Model is being downloaded. Waiting for download to complete...\")\n        # Wait for SSE monitor to capture download events\n        events = await monitor_task\n        return events\n\n    # Wait a bit more to catch any progress events\n    await asyncio.sleep(3)\n\n    # Cancel SSE monitor\n    monitor_task.cancel()\n    try:\n        events = await monitor_task\n    except asyncio.CancelledError:\n        events = []\n\n    return events\n\n\nasync def test_generation_with_fresh_download():\n    \"\"\"\n    Test Case 2: Generation when model needs to be downloaded.\n\n    This SHOULD show download progress events.\n    \"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"TEST CASE 2: Generation with Model Download\")\n    print(\"=\" * 80)\n    print(\"Expected: Download progress events from 0% to 100%\")\n    print(\"=\" * 80)\n\n    # Use a different model size to force download\n    model_size = \"0.6B\"  # Smaller model for faster testing\n    model_name = f\"qwen-tts-{model_size}\"\n\n    # Get a profile\n    profile_id = await get_first_profile()\n    if not profile_id:\n        print(\"✗ No voice profiles found. Please create a profile first.\")\n        return False\n\n    print(f\"\\nUsing profile: {profile_id}\")\n    print(\"Note: This will download the model if not cached\")\n\n    # Start SSE monitor BEFORE triggering generation\n    monitor_task = asyncio.create_task(monitor_sse_stream(model_name, timeout=300))\n\n    # Wait for SSE to connect\n    await asyncio.sleep(1)\n\n    # Trigger generation\n    test_text = \"This should trigger a model download if the model is not cached.\"\n    success, result = await trigger_generation(profile_id, test_text, model_size)\n\n    if not success and result and result.get(\"downloading\"):\n        print(\"\\n→ Model download initiated. Monitoring progress...\")\n        # Wait for download to complete\n        events = await monitor_task\n\n        # Try generation again\n        print(f\"\\n[{_timestamp()}] Retrying generation after download...\")\n        await asyncio.sleep(2)\n        success, result = await trigger_generation(profile_id, test_text, model_size)\n\n        if success:\n            print(\"✓ Generation successful after download\")\n\n        return events\n\n    # If model was already cached\n    await asyncio.sleep(3)\n    monitor_task.cancel()\n    try:\n        events = await monitor_task\n    except asyncio.CancelledError:\n        events = []\n\n    return events\n\n\nasync def main():\n    print(\"=\" * 80)\n    print(\"TTS Generation Progress Test\")\n    print(\"=\" * 80)\n    print(\"Purpose: Capture exact SSE events during generation to identify UX issues\")\n    print(\"=\" * 80)\n\n    # Check if server is running\n    print(f\"\\n[{_timestamp()}] Checking if server is running...\")\n    if not await check_server():\n        print(\"✗ Server is not running on http://localhost:8000\")\n        print(\"\\nPlease start the server first:\")\n        print(\"  cd backend && python main.py\")\n        return False\n\n    print(\"✓ Server is running\")\n\n    # Test Case 1: Cached model\n    print(\"\\n\" + \"🧪 \" * 20)\n    events_cached = await test_generation_with_cached_model()\n\n    # Results for Test Case 1\n    print(\"\\n\" + \"=\" * 80)\n    print(\"TEST CASE 1 RESULTS: Generation with Cached Model\")\n    print(\"=\" * 80)\n\n    if not events_cached:\n        print(\"✓ GOOD: No SSE progress events received\")\n        print(\"  This is the expected behavior for a cached model.\")\n    else:\n        print(f\"⚠ ISSUE FOUND: Received {len(events_cached)} SSE events:\")\n        print(\"\\nEvent Timeline:\")\n        for i, event in enumerate(events_cached, 1):\n            timestamp = event.pop(\"_timestamp\", \"??:??:??.???\")\n            print(f\"  {i}. [{timestamp}] {event}\")\n\n        print(\"\\n⚠ This explains the UX issue!\")\n        print(\"  Users see progress events even when the model is already cached,\")\n        print(\"  making them think the model is downloading again.\")\n\n    print(\"\\n\" + \"=\" * 80)\n    print(\"Test Complete!\")\n    print(\"=\" * 80)\n\n    return True\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "backend/tests/test_profile_duplicate_names.py",
    "content": "\"\"\"\nTests for profile duplicate name validation.\n\nThis test suite verifies that the application correctly handles\nduplicate profile names and provides user-friendly error messages.\n\"\"\"\n\nimport pytest\nimport tempfile\nimport shutil\nfrom pathlib import Path\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\n\n# Add parent directory to path to import backend modules\nimport sys\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\nfrom database import Base, VoiceProfile as DBVoiceProfile\nfrom models import VoiceProfileCreate\nfrom profiles import create_profile, update_profile\n\n\n@pytest.fixture\ndef test_db():\n    \"\"\"Create a temporary test database.\"\"\"\n    # Create temporary directory for test database\n    temp_dir = tempfile.mkdtemp()\n    db_path = Path(temp_dir) / \"test.db\"\n\n    # Create engine and session\n    engine = create_engine(f\"sqlite:///{db_path}\")\n    Base.metadata.create_all(bind=engine)\n    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\n\n    db = SessionLocal()\n\n    yield db\n\n    # Cleanup\n    db.close()\n    shutil.rmtree(temp_dir)\n\n\n@pytest.fixture\ndef mock_profiles_dir(monkeypatch, tmp_path):\n    \"\"\"Mock the profiles directory to use a temporary path.\"\"\"\n    from backend import config\n    monkeypatch.setattr(config, 'get_profiles_dir', lambda: tmp_path)\n    return tmp_path\n\n\n@pytest.mark.asyncio\nasync def test_create_profile_duplicate_name_raises_error(test_db, mock_profiles_dir):\n    \"\"\"Test that creating a profile with a duplicate name raises a ValueError.\"\"\"\n    # Create first profile\n    profile_data_1 = VoiceProfileCreate(\n        name=\"Test Profile\",\n        description=\"First profile\",\n        language=\"en\"\n    )\n\n    profile_1 = await create_profile(profile_data_1, test_db)\n    assert profile_1.name == \"Test Profile\"\n\n    # Try to create second profile with same name\n    profile_data_2 = VoiceProfileCreate(\n        name=\"Test Profile\",\n        description=\"Second profile\",\n        language=\"en\"\n    )\n\n    with pytest.raises(ValueError) as exc_info:\n        await create_profile(profile_data_2, test_db)\n\n    # Verify error message is user-friendly\n    assert \"already exists\" in str(exc_info.value)\n    assert \"Test Profile\" in str(exc_info.value)\n    assert \"choose a different name\" in str(exc_info.value).lower()\n\n\n@pytest.mark.asyncio\nasync def test_create_profile_different_names_succeeds(test_db, mock_profiles_dir):\n    \"\"\"Test that creating profiles with different names succeeds.\"\"\"\n    # Create first profile\n    profile_data_1 = VoiceProfileCreate(\n        name=\"Profile One\",\n        description=\"First profile\",\n        language=\"en\"\n    )\n\n    profile_1 = await create_profile(profile_data_1, test_db)\n    assert profile_1.name == \"Profile One\"\n\n    # Create second profile with different name\n    profile_data_2 = VoiceProfileCreate(\n        name=\"Profile Two\",\n        description=\"Second profile\",\n        language=\"en\"\n    )\n\n    profile_2 = await create_profile(profile_data_2, test_db)\n    assert profile_2.name == \"Profile Two\"\n\n    # Verify both profiles exist\n    assert profile_1.id != profile_2.id\n\n\n@pytest.mark.asyncio\nasync def test_update_profile_to_duplicate_name_raises_error(test_db, mock_profiles_dir):\n    \"\"\"Test that updating a profile to a duplicate name raises a ValueError.\"\"\"\n    # Create two profiles with different names\n    profile_data_1 = VoiceProfileCreate(\n        name=\"Profile A\",\n        description=\"First profile\",\n        language=\"en\"\n    )\n    profile_1 = await create_profile(profile_data_1, test_db)\n\n    profile_data_2 = VoiceProfileCreate(\n        name=\"Profile B\",\n        description=\"Second profile\",\n        language=\"en\"\n    )\n    profile_2 = await create_profile(profile_data_2, test_db)\n\n    # Try to update profile_2 to use profile_1's name\n    update_data = VoiceProfileCreate(\n        name=\"Profile A\",  # Duplicate name\n        description=\"Updated description\",\n        language=\"en\"\n    )\n\n    with pytest.raises(ValueError) as exc_info:\n        await update_profile(profile_2.id, update_data, test_db)\n\n    # Verify error message is user-friendly\n    assert \"already exists\" in str(exc_info.value)\n    assert \"Profile A\" in str(exc_info.value)\n\n\n@pytest.mark.asyncio\nasync def test_update_profile_keep_same_name_succeeds(test_db, mock_profiles_dir):\n    \"\"\"Test that updating a profile while keeping the same name succeeds.\"\"\"\n    # Create profile\n    profile_data = VoiceProfileCreate(\n        name=\"My Profile\",\n        description=\"Original description\",\n        language=\"en\"\n    )\n    profile = await create_profile(profile_data, test_db)\n\n    # Update profile with same name but different description\n    update_data = VoiceProfileCreate(\n        name=\"My Profile\",  # Same name\n        description=\"Updated description\",\n        language=\"en\"\n    )\n\n    updated_profile = await update_profile(profile.id, update_data, test_db)\n\n    # Verify update succeeded\n    assert updated_profile is not None\n    assert updated_profile.id == profile.id\n    assert updated_profile.name == \"My Profile\"\n    assert updated_profile.description == \"Updated description\"\n\n\n@pytest.mark.asyncio\nasync def test_update_profile_to_new_unique_name_succeeds(test_db, mock_profiles_dir):\n    \"\"\"Test that updating a profile to a new unique name succeeds.\"\"\"\n    # Create profile\n    profile_data = VoiceProfileCreate(\n        name=\"Original Name\",\n        description=\"Profile description\",\n        language=\"en\"\n    )\n    profile = await create_profile(profile_data, test_db)\n\n    # Update profile with new unique name\n    update_data = VoiceProfileCreate(\n        name=\"New Unique Name\",\n        description=\"Updated description\",\n        language=\"en\"\n    )\n\n    updated_profile = await update_profile(profile.id, update_data, test_db)\n\n    # Verify update succeeded\n    assert updated_profile is not None\n    assert updated_profile.id == profile.id\n    assert updated_profile.name == \"New Unique Name\"\n\n\n@pytest.mark.asyncio\nasync def test_case_sensitive_names_allowed(test_db, mock_profiles_dir):\n    \"\"\"Test that profile names are case-sensitive (e.g., 'Test' and 'test' are different).\"\"\"\n    # Create profile with lowercase name\n    profile_data_1 = VoiceProfileCreate(\n        name=\"test profile\",\n        description=\"Lowercase\",\n        language=\"en\"\n    )\n    profile_1 = await create_profile(profile_data_1, test_db)\n\n    # Create profile with different case\n    profile_data_2 = VoiceProfileCreate(\n        name=\"Test Profile\",\n        description=\"Title case\",\n        language=\"en\"\n    )\n    profile_2 = await create_profile(profile_data_2, test_db)\n\n    # Both should succeed since SQLite unique constraint is case-sensitive by default\n    assert profile_1.name == \"test profile\"\n    assert profile_2.name == \"Test Profile\"\n    assert profile_1.id != profile_2.id\n"
  },
  {
    "path": "backend/tests/test_progress.py",
    "content": "\"\"\"\nTest script to debug model download progress tracking.\n\"\"\"\n\nimport asyncio\nimport json\nimport time\nfrom typing import List, Dict\nimport logging\n\n# Set up logging to see what's happening\nlogging.basicConfig(\n    level=logging.DEBUG,\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'\n)\n\nfrom utils.progress import ProgressManager, get_progress_manager\nfrom utils.hf_progress import HFProgressTracker, create_hf_progress_callback\n\n\ndef test_progress_manager_basic():\n    \"\"\"Test 1: Basic ProgressManager functionality.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 1: ProgressManager Basic Operations\")\n    print(\"=\" * 60)\n\n    pm = ProgressManager()\n\n    # Test update_progress\n    pm.update_progress(\n        model_name=\"test-model\",\n        current=50,\n        total=100,\n        filename=\"test.bin\",\n        status=\"downloading\"\n    )\n\n    # Test get_progress\n    progress = pm.get_progress(\"test-model\")\n    print(f\"✓ Progress stored: {progress}\")\n    assert progress is not None\n    assert progress[\"progress\"] == 50.0\n    assert progress[\"filename\"] == \"test.bin\"\n    assert progress[\"status\"] == \"downloading\"\n\n    # Test mark_complete\n    pm.mark_complete(\"test-model\")\n    progress = pm.get_progress(\"test-model\")\n    print(f\"✓ Marked complete: {progress}\")\n    assert progress[\"status\"] == \"complete\"\n    assert progress[\"progress\"] == 100.0\n\n    print(\"✓ Test 1 PASSED\\n\")\n    return True\n\n\nasync def test_progress_manager_sse():\n    \"\"\"Test 2: ProgressManager SSE streaming.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 2: ProgressManager SSE Streaming\")\n    print(\"=\" * 60)\n\n    pm = ProgressManager()\n    collected_events: List[Dict] = []\n\n    # Simulate SSE client\n    async def sse_client():\n        \"\"\"Simulates a frontend SSE connection.\"\"\"\n        print(\"  SSE client: Subscribing to test-model-sse...\")\n        async for event in pm.subscribe(\"test-model-sse\"):\n            # Parse SSE event\n            if event.startswith(\"data: \"):\n                data = json.loads(event[6:])\n                print(f\"  SSE client: Received event: {data['status']} - {data.get('progress', 0):.1f}%\")\n                collected_events.append(data)\n\n                # Stop when complete\n                if data.get(\"status\") in (\"complete\", \"error\"):\n                    break\n            elif event.startswith(\": heartbeat\"):\n                print(\"  SSE client: Received heartbeat\")\n\n    # Simulate download progress updates (from backend thread)\n    async def simulate_download():\n        \"\"\"Simulates backend sending progress updates.\"\"\"\n        print(\"  Backend: Starting simulated download...\")\n        await asyncio.sleep(0.2)  # Let SSE client subscribe first\n\n        # Send progress updates\n        for i in range(0, 101, 20):\n            print(f\"  Backend: Updating progress to {i}%\")\n            pm.update_progress(\n                model_name=\"test-model-sse\",\n                current=i,\n                total=100,\n                filename=f\"file_{i}.bin\",\n                status=\"downloading\" if i < 100 else \"downloading\"\n            )\n            await asyncio.sleep(0.1)\n\n        # Mark complete\n        print(\"  Backend: Marking download complete\")\n        pm.mark_complete(\"test-model-sse\")\n\n    # Run SSE client and download simulation concurrently\n    await asyncio.gather(\n        sse_client(),\n        simulate_download()\n    )\n\n    # Verify we got events\n    print(f\"\\n  Collected {len(collected_events)} events\")\n    assert len(collected_events) > 0, \"Should have received at least one event\"\n    assert collected_events[-1][\"status\"] == \"complete\", \"Last event should be 'complete'\"\n\n    print(\"✓ Test 2 PASSED\\n\")\n    return True\n\n\ndef test_hf_progress_tracker():\n    \"\"\"Test 3: HFProgressTracker tqdm patching.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 3: HFProgressTracker tqdm Patching\")\n    print(\"=\" * 60)\n\n    captured_progress: List[tuple] = []\n\n    def progress_callback(downloaded: int, total: int, filename: str):\n        \"\"\"Capture progress updates.\"\"\"\n        captured_progress.append((downloaded, total, filename))\n        print(f\"  Progress callback: {downloaded}/{total} bytes ({filename})\")\n\n    tracker = HFProgressTracker(progress_callback)\n\n    # Simulate a download with tqdm\n    with tracker.patch_download():\n        try:\n            from tqdm import tqdm\n\n            # Simulate downloading a file\n            print(\"  Simulating download with tqdm...\")\n            total_size = 1000\n            with tqdm(total=total_size, desc=\"model.bin\", unit=\"B\", unit_scale=True) as pbar:\n                for chunk in range(0, total_size, 100):\n                    pbar.update(100)\n                    time.sleep(0.01)\n\n            print(f\"  Captured {len(captured_progress)} progress updates\")\n            assert len(captured_progress) > 0, \"Should have captured progress updates\"\n\n            # Verify progress increases\n            last_downloaded = 0\n            for downloaded, total, filename in captured_progress:\n                assert downloaded >= last_downloaded, \"Downloaded bytes should increase\"\n                assert total == total_size, \"Total should be consistent\"\n                last_downloaded = downloaded\n\n            print(\"✓ Test 3 PASSED\\n\")\n            return True\n\n        except ImportError:\n            print(\"✗ tqdm not available, skipping test\\n\")\n            return None\n\n\nasync def test_full_integration():\n    \"\"\"Test 4: Full integration test.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test 4: Full Integration (ProgressManager + HFProgressTracker)\")\n    print(\"=\" * 60)\n\n    pm = get_progress_manager()\n    collected_events: List[Dict] = []\n\n    # SSE client\n    async def sse_client():\n        print(\"  SSE client: Subscribing...\")\n        async for event in pm.subscribe(\"integration-test\"):\n            if event.startswith(\"data: \"):\n                data = json.loads(event[6:])\n                print(f\"  SSE client: {data['status']} - {data.get('progress', 0):.1f}% - {data.get('filename', '')}\")\n                collected_events.append(data)\n                if data.get(\"status\") in (\"complete\", \"error\"):\n                    break\n\n    # Simulate backend download with HFProgressTracker\n    async def simulate_real_download():\n        await asyncio.sleep(0.2)  # Let SSE subscribe\n\n        print(\"  Backend: Starting download with HFProgressTracker...\")\n\n        # Set up tracking (like the real backend does)\n        progress_callback = create_hf_progress_callback(\"integration-test\", pm)\n        tracker = HFProgressTracker(progress_callback)\n\n        # Initialize progress\n        pm.update_progress(\n            model_name=\"integration-test\",\n            current=0,\n            total=1,\n            filename=\"\",\n            status=\"downloading\"\n        )\n\n        # Simulate download with tqdm patching\n        with tracker.patch_download():\n            try:\n                from tqdm import tqdm\n\n                # Simulate multi-file download (like HuggingFace does)\n                files = [\n                    (\"model.safetensors\", 5000),\n                    (\"config.json\", 1000),\n                    (\"tokenizer.json\", 500),\n                ]\n\n                for filename, size in files:\n                    print(f\"  Backend: Downloading {filename}...\")\n                    with tqdm(total=size, desc=filename, unit=\"B\") as pbar:\n                        for chunk in range(0, size, 500):\n                            chunk_size = min(500, size - chunk)\n                            pbar.update(chunk_size)\n                            await asyncio.sleep(0.05)\n\n                # Mark complete\n                print(\"  Backend: Download complete\")\n                pm.mark_complete(\"integration-test\")\n\n            except ImportError:\n                print(\"  ✗ tqdm not available\")\n                pm.mark_error(\"integration-test\", \"tqdm not available\")\n\n    # Run both\n    await asyncio.gather(\n        sse_client(),\n        simulate_real_download()\n    )\n\n    # Verify\n    print(f\"\\n  Collected {len(collected_events)} events\")\n    if len(collected_events) > 0:\n        print(f\"  First event: {collected_events[0]}\")\n        print(f\"  Last event: {collected_events[-1]}\")\n        assert collected_events[-1][\"status\"] == \"complete\", \"Should end with 'complete'\"\n        print(\"✓ Test 4 PASSED\\n\")\n        return True\n    else:\n        print(\"✗ Test 4 FAILED - No events received\\n\")\n        return False\n\n\nasync def main():\n    \"\"\"Run all tests.\"\"\"\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Voicebox Progress Tracking Test Suite\")\n    print(\"=\" * 60)\n\n    results = []\n\n    # Test 1: Basic operations\n    try:\n        results.append((\"Basic Operations\", test_progress_manager_basic()))\n    except Exception as e:\n        print(f\"✗ Test 1 FAILED: {e}\\n\")\n        results.append((\"Basic Operations\", False))\n\n    # Test 2: SSE streaming\n    try:\n        results.append((\"SSE Streaming\", await test_progress_manager_sse()))\n    except Exception as e:\n        print(f\"✗ Test 2 FAILED: {e}\\n\")\n        results.append((\"SSE Streaming\", False))\n\n    # Test 3: tqdm patching\n    try:\n        results.append((\"tqdm Patching\", test_hf_progress_tracker()))\n    except Exception as e:\n        print(f\"✗ Test 3 FAILED: {e}\\n\")\n        results.append((\"tqdm Patching\", False))\n\n    # Test 4: Full integration\n    try:\n        results.append((\"Full Integration\", await test_full_integration()))\n    except Exception as e:\n        print(f\"✗ Test 4 FAILED: {e}\\n\")\n        results.append((\"Full Integration\", False))\n\n    # Summary\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test Results Summary\")\n    print(\"=\" * 60)\n\n    for name, result in results:\n        status = \"✓ PASS\" if result else (\"⊘ SKIP\" if result is None else \"✗ FAIL\")\n        print(f\"  {status:8} {name}\")\n\n    passed = sum(1 for _, r in results if r is True)\n    failed = sum(1 for _, r in results if r is False)\n    skipped = sum(1 for _, r in results if r is None)\n\n    print()\n    print(f\"  Total: {len(results)} tests\")\n    print(f\"  Passed: {passed}\")\n    print(f\"  Failed: {failed}\")\n    print(f\"  Skipped: {skipped}\")\n    print(\"=\" * 60 + \"\\n\")\n\n    return failed == 0\n\n\nif __name__ == \"__main__\":\n    success = asyncio.run(main())\n    exit(0 if success else 1)\n"
  },
  {
    "path": "backend/tests/test_qwen_download.py",
    "content": "\"\"\"\nTest Qwen TTS model download with SSE progress monitoring.\n\nThis specifically tests the MLX TTS backend download progress tracking,\nwhich requires tqdm to be patched BEFORE mlx_audio is imported.\n\nUsage:\n    cd backend && python -m tests.test_qwen_download\n\nPrerequisites:\n    - Server must be running: cd backend && python main.py\n    - Delete model first for fresh download test:\n      curl -X DELETE http://localhost:8000/models/qwen-tts-0.6B\n\"\"\"\n\nimport asyncio\nimport json\nimport httpx\nimport time\nfrom typing import List, Dict, Optional\n\n\nasync def monitor_sse_stream(model_name: str, timeout: int = 600) -> List[Dict]:\n    \"\"\"\n    Monitor SSE stream for a model download.\n    \n    Args:\n        model_name: Name of the model to monitor\n        timeout: Maximum time to wait for download (seconds)\n        \n    Returns:\n        List of SSE events received\n    \"\"\"\n    events: List[Dict] = []\n    url = f\"http://localhost:8000/models/progress/{model_name}\"\n    last_progress = -1\n    \n    print(f\"\\n📡 Connecting to SSE endpoint: {url}\")\n\n    try:\n        async with httpx.AsyncClient(timeout=timeout) as client:\n            async with client.stream(\"GET\", url) as response:\n                print(f\"   SSE connected, status: {response.status_code}\")\n\n                if response.status_code != 200:\n                    print(f\"   ❌ Error: SSE endpoint returned {response.status_code}\")\n                    return events\n\n                async for line in response.aiter_lines():\n                    if not line:\n                        continue\n\n                    if line.startswith(\"data: \"):\n                        try:\n                            data = json.loads(line[6:])\n                            events.append(data)\n                            \n                            # Print progress (only when it changes significantly)\n                            progress = data.get('progress', 0)\n                            status = data.get('status', 'unknown')\n                            filename = data.get('filename', '')\n                            current = data.get('current', 0)\n                            total = data.get('total', 0)\n                            \n                            # Print every 5% change or status change\n                            if abs(progress - last_progress) >= 5 or status in ('complete', 'error'):\n                                current_mb = current / (1024 * 1024)\n                                total_mb = total / (1024 * 1024)\n                                print(f\"   📊 {status:12} {progress:6.1f}% ({current_mb:.1f}MB / {total_mb:.1f}MB) {filename[:50]}\")\n                                last_progress = progress\n\n                            # Stop if complete or error\n                            if status in (\"complete\", \"error\"):\n                                if status == \"complete\":\n                                    print(f\"   ✅ Download complete!\")\n                                else:\n                                    print(f\"   ❌ Download error: {data.get('error', 'unknown')}\")\n                                break\n\n                        except json.JSONDecodeError as e:\n                            print(f\"   ⚠️  Error parsing JSON: {e}\")\n\n                    elif line.startswith(\": heartbeat\"):\n                        # Heartbeat every 1 second, don't spam\n                        pass\n\n    except asyncio.CancelledError:\n        print(\"   ⏹️  SSE monitor cancelled\")\n    except Exception as e:\n        print(f\"   ❌ SSE error: {e}\")\n\n    return events\n\n\nasync def trigger_download(model_name: str) -> bool:\n    \"\"\"Trigger a model download via the API.\"\"\"\n    url = \"http://localhost:8000/models/download\"\n\n    print(f\"\\n🚀 Triggering download for: {model_name}\")\n\n    try:\n        async with httpx.AsyncClient(timeout=30) as client:\n            response = await client.post(url, json={\"model_name\": model_name})\n            result = response.json()\n            print(f\"   Response: {response.status_code} - {result}\")\n            return response.status_code == 200\n    except Exception as e:\n        print(f\"   ❌ Error triggering download: {e}\")\n        return False\n\n\nasync def delete_model(model_name: str) -> bool:\n    \"\"\"Delete a model from cache.\"\"\"\n    url = f\"http://localhost:8000/models/{model_name}\"\n\n    print(f\"\\n🗑️  Deleting model: {model_name}\")\n\n    try:\n        async with httpx.AsyncClient(timeout=30) as client:\n            response = await client.delete(url)\n            if response.status_code == 200:\n                print(f\"   ✅ Model deleted\")\n                return True\n            elif response.status_code == 404:\n                print(f\"   ℹ️  Model not found (already deleted)\")\n                return True\n            else:\n                print(f\"   ⚠️  Delete response: {response.status_code} - {response.text}\")\n                return False\n    except Exception as e:\n        print(f\"   ❌ Error deleting model: {e}\")\n        return False\n\n\nasync def check_model_status(model_name: str) -> Optional[Dict]:\n    \"\"\"Check the status of a model.\"\"\"\n    try:\n        async with httpx.AsyncClient(timeout=10) as client:\n            response = await client.get(\"http://localhost:8000/models/status\")\n            if response.status_code == 200:\n                data = response.json()\n                for model in data.get(\"models\", []):\n                    if model[\"model_name\"] == model_name:\n                        return model\n    except Exception as e:\n        print(f\"   ⚠️  Error checking model status: {e}\")\n    return None\n\n\nasync def check_server() -> bool:\n    \"\"\"Check if the server is running.\"\"\"\n    try:\n        async with httpx.AsyncClient(timeout=5) as client:\n            response = await client.get(\"http://localhost:8000/health\")\n            return response.status_code == 200\n    except Exception:\n        return False\n\n\nasync def main():\n    print(\"=\" * 70)\n    print(\"🧪 Qwen TTS Model Download Progress Test\")\n    print(\"=\" * 70)\n    print(\"\\nThis test verifies that MLX TTS download progress tracking works.\")\n    print(\"It specifically tests the tqdm patching for mlx_audio.tts imports.\")\n\n    # Check if server is running\n    print(\"\\n📡 Checking if server is running...\")\n    if not await check_server():\n        print(\"   ❌ Server is not running on http://localhost:8000\")\n        print(\"\\n   Please start the server first:\")\n        print(\"   cd backend && python main.py\")\n        return False\n\n    print(\"   ✅ Server is running\")\n\n    # Test model\n    model_name = \"qwen-tts-0.6B\"  # Note: 0.6B currently maps to 1.7B on MLX\n    \n    # Check current status\n    print(f\"\\n📊 Checking status of {model_name}...\")\n    status = await check_model_status(model_name)\n    if status:\n        print(f\"   Downloaded: {status.get('downloaded', False)}\")\n        print(f\"   Downloading: {status.get('downloading', False)}\")\n        print(f\"   Loaded: {status.get('loaded', False)}\")\n        if status.get('size_mb'):\n            print(f\"   Size: {status['size_mb']:.1f} MB\")\n    else:\n        print(\"   ⚠️  Could not get model status\")\n\n    # Ask if user wants to delete first\n    print(\"\\n\" + \"-\" * 70)\n    if status and status.get('downloaded'):\n        print(\"⚠️  Model is already downloaded. Delete it for a fresh download test?\")\n        print(\"   [y] Yes, delete and download fresh\")\n        print(\"   [n] No, just test SSE connection\")\n        print(\"   [q] Quit\")\n        \n        choice = input(\"\\nChoice [y/n/q]: \").strip().lower()\n        \n        if choice == 'q':\n            print(\"Exiting...\")\n            return True\n        \n        if choice == 'y':\n            if not await delete_model(model_name):\n                print(\"Failed to delete model. Continue anyway? [y/n]\")\n                if input().strip().lower() != 'y':\n                    return False\n    else:\n        print(\"Model not downloaded. Will perform fresh download test.\")\n        input(\"Press Enter to continue...\")\n\n    # Run the test\n    print(\"\\n\" + \"=\" * 70)\n    print(\"🏃 Starting Download Test\")\n    print(\"=\" * 70)\n\n    async def run_test():\n        # Start SSE monitor in background FIRST\n        monitor_task = asyncio.create_task(monitor_sse_stream(model_name, timeout=600))\n\n        # Wait for SSE to connect\n        await asyncio.sleep(1)\n\n        # Trigger download\n        success = await trigger_download(model_name)\n\n        if not success:\n            print(\"   ❌ Failed to trigger download\")\n            monitor_task.cancel()\n            try:\n                await monitor_task\n            except asyncio.CancelledError:\n                pass\n            return []\n\n        # Wait for SSE monitor to complete\n        print(\"\\n⏳ Waiting for download to complete (this may take several minutes)...\")\n        events = await monitor_task\n\n        return events\n\n    start_time = time.time()\n    events = await run_test()\n    elapsed = time.time() - start_time\n\n    # Results\n    print(\"\\n\" + \"=\" * 70)\n    print(\"📋 Test Results\")\n    print(\"=\" * 70)\n\n    print(f\"\\n⏱️  Elapsed time: {elapsed:.1f} seconds\")\n    print(f\"📨 Total SSE events received: {len(events)}\")\n\n    if not events:\n        print(\"\\n❌ FAILED - No SSE events received!\")\n        print(\"\\nPossible causes:\")\n        print(\"  1. SSE endpoint not working\")\n        print(\"  2. tqdm not patched before mlx_audio import\")\n        print(\"  3. Progress callbacks not firing\")\n        print(\"  4. Model already fully downloaded\")\n        print(\"\\nDebug steps:\")\n        print(\"  1. Check server logs for [DEBUG] messages\")\n        print(\"  2. Look for 'tqdm patched' before 'mlx_audio.tts import'\")\n        print(f\"  3. Delete model: curl -X DELETE http://localhost:8000/models/{model_name}\")\n        return False\n\n    # Analyze events\n    first_event = events[0]\n    last_event = events[-1]\n    \n    print(f\"\\n📊 First event:\")\n    print(f\"   Status: {first_event.get('status')}\")\n    print(f\"   Progress: {first_event.get('progress', 0):.1f}%\")\n    \n    print(f\"\\n📊 Last event:\")\n    print(f\"   Status: {last_event.get('status')}\")\n    print(f\"   Progress: {last_event.get('progress', 0):.1f}%\")\n\n    # Check for expected behaviors\n    has_progress_updates = len(events) > 2\n    has_increasing_progress = False\n    has_complete = any(e.get('status') == 'complete' for e in events)\n    has_100_percent = any(e.get('progress', 0) >= 100 for e in events)\n    \n    # Check if progress increased over time\n    if len(events) >= 2:\n        progress_values = [e.get('progress', 0) for e in events]\n        has_increasing_progress = progress_values[-1] > progress_values[0]\n\n    print(\"\\n📋 Checks:\")\n    print(f\"   {'✅' if has_progress_updates else '❌'} Multiple progress updates received ({len(events)} events)\")\n    print(f\"   {'✅' if has_increasing_progress else '❌'} Progress increased over time\")\n    print(f\"   {'✅' if has_100_percent else '❌'} Reached 100% progress\")\n    print(f\"   {'✅' if has_complete else '❌'} Received 'complete' status\")\n\n    # Overall result\n    success = has_progress_updates and has_complete\n    \n    if success:\n        print(\"\\n\" + \"=\" * 70)\n        print(\"✅ TEST PASSED - Qwen TTS download progress tracking works!\")\n        print(\"=\" * 70)\n    else:\n        print(\"\\n\" + \"=\" * 70)\n        print(\"❌ TEST FAILED - Progress tracking has issues\")\n        print(\"=\" * 70)\n        print(\"\\nCheck the server logs for debug output.\")\n\n    return success\n\n\nif __name__ == \"__main__\":\n    result = asyncio.run(main())\n    exit(0 if result else 1)\n"
  },
  {
    "path": "backend/tests/test_whisper_download.py",
    "content": "\"\"\"\nTest real model download with SSE progress monitoring.\n\"\"\"\n\nimport asyncio\nimport json\nimport httpx\nimport time\nfrom typing import List, Dict\n\nasync def monitor_sse_stream(model_name: str, timeout: int = 300):\n    \"\"\"Monitor SSE stream for a model download.\"\"\"\n    events: List[Dict] = []\n    url = f\"http://localhost:8000/models/progress/{model_name}\"\n\n    print(f\"Connecting to SSE endpoint: {url}\")\n\n    async with httpx.AsyncClient(timeout=timeout) as client:\n        async with client.stream(\"GET\", url) as response:\n            print(f\"SSE connected, status: {response.status_code}\")\n\n            if response.status_code != 200:\n                print(f\"Error: SSE endpoint returned {response.status_code}\")\n                return events\n\n            async for line in response.aiter_lines():\n                if not line:\n                    continue\n\n                print(f\"  Raw SSE: {line[:100]}...\")  # Print first 100 chars\n\n                if line.startswith(\"data: \"):\n                    try:\n                        data = json.loads(line[6:])\n                        print(f\"  → {data['status']:12} {data.get('progress', 0):6.1f}% {data.get('filename', '')}\")\n                        events.append(data)\n\n                        # Stop if complete or error\n                        if data.get(\"status\") in (\"complete\", \"error\"):\n                            print(f\"  Download {data['status']}!\")\n                            break\n\n                    except json.JSONDecodeError as e:\n                        print(f\"  Error parsing JSON: {e}\")\n                        print(f\"  Line was: {line}\")\n\n                elif line.startswith(\": heartbeat\"):\n                    print(\"  ♥ heartbeat\")\n\n    return events\n\n\nasync def trigger_download(model_name: str):\n    \"\"\"Trigger a model download via the API.\"\"\"\n    url = \"http://localhost:8000/models/download\"\n\n    print(f\"\\nTriggering download for: {model_name}\")\n\n    async with httpx.AsyncClient(timeout=300) as client:\n        response = await client.post(url, json={\"model_name\": model_name})\n        print(f\"Response: {response.status_code} - {response.json()}\")\n        return response.status_code == 200\n\n\nasync def check_server():\n    \"\"\"Check if the server is running.\"\"\"\n    try:\n        async with httpx.AsyncClient(timeout=5) as client:\n            response = await client.get(\"http://localhost:8000/health\")\n            return response.status_code == 200\n    except Exception as e:\n        print(f\"Server not running: {e}\")\n        return False\n\n\nasync def main():\n    print(\"=\" * 60)\n    print(\"Real Model Download Progress Test\")\n    print(\"=\" * 60)\n\n    # Check if server is running\n    print(\"\\nChecking if server is running...\")\n    if not await check_server():\n        print(\"✗ Server is not running on http://localhost:8000\")\n        print(\"\\nPlease start the server first:\")\n        print(\"  cd backend && python main.py\")\n        return False\n\n    print(\"✓ Server is running\")\n\n    # Choose a small model for testing\n    model_name = \"whisper-base\"  # ~150MB, faster to download\n    print(f\"\\nUsing model: {model_name}\")\n\n    # Option to delete model first if it exists\n    print(\"\\nDo you want to delete the model first to force a fresh download? (y/n)\")\n    # For automated testing, skip deletion prompt\n    # delete_first = input().strip().lower() == 'y'\n    delete_first = False\n\n    if delete_first:\n        print(f\"Deleting {model_name}...\")\n        async with httpx.AsyncClient(timeout=30) as client:\n            response = await client.delete(f\"http://localhost:8000/models/{model_name}\")\n            print(f\"Delete response: {response.status_code}\")\n\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Starting Test\")\n    print(\"=\" * 60)\n\n    # Start monitoring SSE stream BEFORE triggering download\n    async def run_test():\n        # Start SSE monitor in background\n        monitor_task = asyncio.create_task(monitor_sse_stream(model_name))\n\n        # Wait a bit to ensure SSE is connected\n        await asyncio.sleep(1)\n\n        # Trigger download\n        success = await trigger_download(model_name)\n\n        if not success:\n            print(\"✗ Failed to trigger download\")\n            monitor_task.cancel()\n            return False\n\n        # Wait for SSE monitor to complete\n        events = await monitor_task\n\n        return events\n\n    events = await run_test()\n\n    # Results\n    print(\"\\n\" + \"=\" * 60)\n    print(\"Test Results\")\n    print(\"=\" * 60)\n\n    if not events:\n        print(\"✗ FAILED - No SSE events received!\")\n        print(\"\\nPossible causes:\")\n        print(\"  1. SSE endpoint not working\")\n        print(\"  2. Progress updates not being sent\")\n        print(\"  3. Model already downloaded (no progress to report)\")\n        print(\"\\nTry deleting the model first to force a fresh download:\")\n        print(f\"  curl -X DELETE http://localhost:8000/models/{model_name}\")\n        return False\n\n    print(f\"✓ Received {len(events)} SSE events\")\n    print(f\"\\nFirst event: {events[0]}\")\n    print(f\"Last event: {events[-1]}\")\n\n    # Check if we got meaningful progress\n    has_progress = any(e.get('progress', 0) > 0 for e in events)\n    has_complete = any(e.get('status') == 'complete' for e in events)\n\n    if has_progress:\n        print(\"✓ Progress updates received\")\n    else:\n        print(\"✗ No progress updates (might be already downloaded)\")\n\n    if has_complete:\n        print(\"✓ Download completed successfully\")\n    else:\n        print(\"✗ Download did not complete\")\n\n    success = has_progress and has_complete\n\n    if success:\n        print(\"\\n✓ TEST PASSED - Progress tracking works!\")\n    else:\n        print(\"\\n⊘ TEST INCONCLUSIVE - Try with a fresh download\")\n\n    return success\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "backend/utils/__init__.py",
    "content": "# Utils package\n"
  },
  {
    "path": "backend/utils/audio.py",
    "content": "\"\"\"\nAudio processing utilities.\n\"\"\"\n\nimport numpy as np\nimport soundfile as sf\nimport librosa\nfrom typing import Tuple, Optional\n\n\ndef normalize_audio(\n    audio: np.ndarray,\n    target_db: float = -20.0,\n    peak_limit: float = 0.85,\n) -> np.ndarray:\n    \"\"\"\n    Normalize audio to target loudness with peak limiting.\n    \n    Args:\n        audio: Input audio array\n        target_db: Target RMS level in dB\n        peak_limit: Peak limit (0.0-1.0)\n        \n    Returns:\n        Normalized audio array\n    \"\"\"\n    # Convert to float32\n    audio = audio.astype(np.float32)\n    \n    # Calculate current RMS\n    rms = np.sqrt(np.mean(audio**2))\n    \n    # Calculate target RMS\n    target_rms = 10**(target_db / 20)\n    \n    # Apply gain\n    if rms > 0:\n        gain = target_rms / rms\n        audio = audio * gain\n    \n    # Peak limiting\n    audio = np.clip(audio, -peak_limit, peak_limit)\n    \n    return audio\n\n\ndef load_audio(\n    path: str,\n    sample_rate: int = 24000,\n    mono: bool = True,\n) -> Tuple[np.ndarray, int]:\n    \"\"\"\n    Load audio file with normalization.\n    \n    Args:\n        path: Path to audio file\n        sample_rate: Target sample rate\n        mono: Convert to mono\n        \n    Returns:\n        Tuple of (audio_array, sample_rate)\n    \"\"\"\n    audio, sr = librosa.load(path, sr=sample_rate, mono=mono)\n    return audio, sr\n\n\ndef save_audio(\n    audio: np.ndarray,\n    path: str,\n    sample_rate: int = 24000,\n) -> None:\n    \"\"\"\n    Save audio file with atomic write and error handling.\n\n    Writes to a temporary file first, then atomically renames to the\n    target path.  This prevents corrupted/partial WAV files if the\n    process is interrupted mid-write.\n\n    Args:\n        audio: Audio array\n        path: Output path\n        sample_rate: Sample rate\n\n    Raises:\n        OSError: If file cannot be written\n    \"\"\"\n    from pathlib import Path\n    import os\n\n    temp_path = f\"{path}.tmp\"\n    try:\n        # Ensure parent directory exists\n        Path(path).parent.mkdir(parents=True, exist_ok=True)\n\n        # Write to temporary file first (explicit format since .tmp\n        # extension is not recognised by soundfile)\n        sf.write(temp_path, audio, sample_rate, format='WAV')\n\n        # Atomic rename to final path\n        os.replace(temp_path, path)\n\n    except Exception as e:\n        # Clean up temp file on failure\n        try:\n            if Path(temp_path).exists():\n                Path(temp_path).unlink()\n        except Exception:\n            pass  # Best effort cleanup\n\n        raise OSError(f\"Failed to save audio to {path}: {e}\") from e\n\n\ndef trim_tts_output(\n    audio: np.ndarray,\n    sample_rate: int = 24000,\n    frame_ms: int = 20,\n    silence_threshold_db: float = -40.0,\n    min_silence_ms: int = 200,\n    max_internal_silence_ms: int = 1000,\n    fade_ms: int = 30,\n) -> np.ndarray:\n    \"\"\"\n    Trim trailing silence and post-silence hallucination from TTS output.\n\n    Chatterbox sometimes produces ``[speech][silence][hallucinated noise]``.\n    This detects internal silence gaps longer than *max_internal_silence_ms*\n    and cuts the audio at that boundary, then trims trailing silence and\n    applies a short cosine fade-out.\n\n    Args:\n        audio: Input audio array (mono float32)\n        sample_rate: Sample rate in Hz\n        frame_ms: Frame size for RMS energy calculation\n        silence_threshold_db: dB threshold below which a frame is silence\n        min_silence_ms: Minimum trailing silence to keep\n        max_internal_silence_ms: Cut after any silence gap longer than this\n        fade_ms: Cosine fade-out duration in ms\n\n    Returns:\n        Trimmed audio array\n    \"\"\"\n    frame_len = int(sample_rate * frame_ms / 1000)\n    if frame_len == 0 or len(audio) < frame_len:\n        return audio\n\n    n_frames = len(audio) // frame_len\n    threshold_linear = 10 ** (silence_threshold_db / 20)\n\n    # Compute per-frame RMS\n    rms = np.array(\n        [\n            np.sqrt(np.mean(audio[i * frame_len : (i + 1) * frame_len] ** 2))\n            for i in range(n_frames)\n        ]\n    )\n    is_speech = rms >= threshold_linear\n\n    # Find first speech frame\n    first_speech = 0\n    for i, s in enumerate(is_speech):\n        if s:\n            first_speech = max(0, i - 1)  # keep 1 frame padding\n            break\n\n    # Walk forward from first speech; cut at long internal silence gaps\n    max_silence_frames = int(max_internal_silence_ms / frame_ms)\n    consecutive_silence = 0\n    cut_frame = n_frames\n\n    for i in range(first_speech, n_frames):\n        if is_speech[i]:\n            consecutive_silence = 0\n        else:\n            consecutive_silence += 1\n            if consecutive_silence >= max_silence_frames:\n                cut_frame = i - consecutive_silence + 1\n                break\n\n    # Trim trailing silence from the cut point\n    min_silence_frames = int(min_silence_ms / frame_ms)\n    end_frame = cut_frame\n    while end_frame > first_speech and not is_speech[end_frame - 1]:\n        end_frame -= 1\n    # Keep a short tail\n    end_frame = min(end_frame + min_silence_frames, cut_frame)\n\n    # Convert frames back to samples\n    start_sample = first_speech * frame_len\n    end_sample = min(end_frame * frame_len, len(audio))\n\n    trimmed = audio[start_sample:end_sample].copy()\n\n    # Cosine fade-out\n    fade_samples = int(sample_rate * fade_ms / 1000)\n    if fade_samples > 0 and len(trimmed) > fade_samples:\n        fade = np.cos(np.linspace(0, np.pi / 2, fade_samples)) ** 2\n        trimmed[-fade_samples:] *= fade\n\n    return trimmed\n\n\ndef validate_reference_audio(\n    audio_path: str,\n    min_duration: float = 2.0,\n    max_duration: float = 30.0,\n    min_rms: float = 0.01,\n) -> Tuple[bool, Optional[str]]:\n    \"\"\"\n    Validate reference audio for voice cloning.\n    \n    Args:\n        audio_path: Path to audio file\n        min_duration: Minimum duration in seconds\n        max_duration: Maximum duration in seconds\n        min_rms: Minimum RMS level\n        \n    Returns:\n        Tuple of (is_valid, error_message)\n    \"\"\"\n    result = validate_and_load_reference_audio(\n        audio_path, min_duration, max_duration, min_rms\n    )\n    return (result[0], result[1])\n\n\ndef validate_and_load_reference_audio(\n    audio_path: str,\n    min_duration: float = 2.0,\n    max_duration: float = 30.0,\n    min_rms: float = 0.01,\n) -> Tuple[bool, Optional[str], Optional[np.ndarray], Optional[int]]:\n    \"\"\"\n    Validate and load reference audio in a single pass.\n    \n    Returns:\n        Tuple of (is_valid, error_message, audio_array, sample_rate)\n    \"\"\"\n    try:\n        audio, sr = load_audio(audio_path)\n        duration = len(audio) / sr\n        \n        if duration < min_duration:\n            return False, f\"Audio too short (minimum {min_duration} seconds)\", None, None\n        if duration > max_duration:\n            return False, f\"Audio too long (maximum {max_duration} seconds)\", None, None\n        \n        rms = np.sqrt(np.mean(audio**2))\n        if rms < min_rms:\n            return False, \"Audio is too quiet or silent\", None, None\n        \n        if np.abs(audio).max() > 0.99:\n            return False, \"Audio is clipping (reduce input gain)\", None, None\n        \n        return True, None, audio, sr\n    except Exception as e:\n        return False, f\"Error validating audio: {str(e)}\", None, None\n"
  },
  {
    "path": "backend/utils/cache.py",
    "content": "\"\"\"\nVoice prompt caching utilities.\n\"\"\"\n\nimport hashlib\nimport logging\nimport torch\nfrom pathlib import Path\nfrom typing import Optional, Union, Dict, Any\n\nfrom .. import config\n\nlogger = logging.getLogger(__name__)\n\n\ndef _get_cache_dir() -> Path:\n    \"\"\"Get cache directory from config.\"\"\"\n    return config.get_cache_dir()\n\n\n# In-memory cache - can store dict (voice prompt) or tensor (legacy)\n_memory_cache: dict[str, Union[torch.Tensor, Dict[str, Any]]] = {}\n\n\ndef get_cache_key(audio_path: str, reference_text: str) -> str:\n    \"\"\"\n    Generate cache key from audio file and reference text.\n\n    Args:\n        audio_path: Path to audio file\n        reference_text: Reference text\n\n    Returns:\n        Cache key (MD5 hash)\n    \"\"\"\n    # Read audio file\n    with open(audio_path, \"rb\") as f:\n        audio_bytes = f.read()\n\n    # Combine audio bytes and text\n    combined = audio_bytes + reference_text.encode(\"utf-8\")\n\n    # Generate hash\n    return hashlib.md5(combined).hexdigest()\n\n\ndef get_cached_voice_prompt(\n    cache_key: str,\n) -> Optional[Union[torch.Tensor, Dict[str, Any]]]:\n    \"\"\"\n    Get cached voice prompt if available.\n\n    Args:\n        cache_key: Cache key\n\n    Returns:\n        Cached voice prompt (dict or tensor) or None\n    \"\"\"\n    # Check in-memory cache\n    if cache_key in _memory_cache:\n        return _memory_cache[cache_key]\n\n    # Check disk cache\n    cache_file = _get_cache_dir() / f\"{cache_key}.prompt\"\n    if cache_file.exists():\n        try:\n            prompt = torch.load(cache_file)\n            _memory_cache[cache_key] = prompt\n            return prompt\n        except Exception:\n            # Cache file corrupted, delete it\n            cache_file.unlink()\n\n    return None\n\n\ndef cache_voice_prompt(\n    cache_key: str,\n    voice_prompt: Union[torch.Tensor, Dict[str, Any]],\n) -> None:\n    \"\"\"\n    Cache voice prompt to memory and disk.\n\n    Args:\n        cache_key: Cache key\n        voice_prompt: Voice prompt (dict or tensor)\n    \"\"\"\n    # Store in memory\n    _memory_cache[cache_key] = voice_prompt\n\n    # Store on disk (torch.save can handle both dicts and tensors)\n    cache_file = _get_cache_dir() / f\"{cache_key}.prompt\"\n    torch.save(voice_prompt, cache_file)\n\n\ndef clear_voice_prompt_cache() -> int:\n    \"\"\"\n    Clear all voice prompt caches (memory and disk).\n\n    Returns:\n        Number of cache files deleted\n    \"\"\"\n    # Clear memory cache\n    _memory_cache.clear()\n\n    # Clear disk cache\n    cache_dir = _get_cache_dir()\n    deleted_count = 0\n\n    if cache_dir.exists():\n        # Delete prompt cache files\n        for cache_file in cache_dir.glob(\"*.prompt\"):\n            try:\n                cache_file.unlink()\n                deleted_count += 1\n            except Exception as e:\n                logger.warning(\"Failed to delete cache file %s: %s\", cache_file, e)\n\n        # Delete combined audio files\n        for audio_file in cache_dir.glob(\"combined_*.wav\"):\n            try:\n                audio_file.unlink()\n                deleted_count += 1\n            except Exception as e:\n                logger.warning(\"Failed to delete combined audio file %s: %s\", audio_file, e)\n\n    return deleted_count\n\n\ndef clear_profile_cache(profile_id: str) -> int:\n    \"\"\"\n    Clear cache files for a specific profile.\n\n    Args:\n        profile_id: Profile ID\n\n    Returns:\n        Number of cache files deleted\n    \"\"\"\n    cache_dir = _get_cache_dir()\n    deleted_count = 0\n\n    if cache_dir.exists():\n        # Delete combined audio files for this profile\n        pattern = f\"combined_{profile_id}_*.wav\"\n        for audio_file in cache_dir.glob(pattern):\n            try:\n                audio_file.unlink()\n                deleted_count += 1\n            except Exception as e:\n                logger.warning(\"Failed to delete combined audio file %s: %s\", audio_file, e)\n\n    return deleted_count\n"
  },
  {
    "path": "backend/utils/chunked_tts.py",
    "content": "\"\"\"\nChunked TTS generation utilities.\n\nSplits long text into sentence-boundary chunks, generates audio per-chunk\nvia any TTSBackend, and concatenates with crossfade.  All logic is\nengine-agnostic — it wraps the standard ``TTSBackend.generate()`` interface.\n\nShort text (≤ max_chunk_chars) uses the single-shot fast path with zero\noverhead.\n\"\"\"\n\nimport logging\nimport re\nfrom typing import List, Tuple\n\nimport numpy as np\n\nlogger = logging.getLogger(\"voicebox.chunked-tts\")\n\n# Default chunk size in characters.  Can be overridden per-request via\n# the ``max_chunk_chars`` field on GenerationRequest.\nDEFAULT_MAX_CHUNK_CHARS = 800\n\n# Common abbreviations that should NOT be treated as sentence endings.\n# Lowercase for case-insensitive matching.\n_ABBREVIATIONS = frozenset(\n    {\n        \"mr\",\n        \"mrs\",\n        \"ms\",\n        \"dr\",\n        \"prof\",\n        \"sr\",\n        \"jr\",\n        \"st\",\n        \"ave\",\n        \"blvd\",\n        \"inc\",\n        \"ltd\",\n        \"corp\",\n        \"dept\",\n        \"est\",\n        \"approx\",\n        \"vs\",\n        \"etc\",\n        \"e.g\",\n        \"i.e\",\n        \"a.m\",\n        \"p.m\",\n        \"u.s\",\n        \"u.s.a\",\n        \"u.k\",\n    }\n)\n\n# Paralinguistic tags used by Chatterbox Turbo.  The splitter must never\n# cut inside one of these.\n_PARA_TAG_RE = re.compile(r\"\\[[^\\]]*\\]\")\n\n\ndef split_text_into_chunks(text: str, max_chars: int = DEFAULT_MAX_CHUNK_CHARS) -> List[str]:\n    \"\"\"Split *text* at natural boundaries into chunks of at most *max_chars*.\n\n    Priority: sentence-end (``.!?`` not preceded by an abbreviation and not\n    inside brackets) → clause boundary (``;:,—``) → whitespace → hard cut.\n\n    Paralinguistic tags like ``[laugh]`` are treated as atomic and will not\n    be split across chunks.\n    \"\"\"\n    text = text.strip()\n    if not text:\n        return []\n    if len(text) <= max_chars:\n        return [text]\n\n    chunks: List[str] = []\n    remaining = text\n\n    while remaining:\n        remaining = remaining.lstrip()\n        if not remaining:\n            break\n        if len(remaining) <= max_chars:\n            chunks.append(remaining)\n            break\n\n        segment = remaining[:max_chars]\n\n        # Try to split at the last real sentence ending\n        split_pos = _find_last_sentence_end(segment)\n        if split_pos == -1:\n            split_pos = _find_last_clause_boundary(segment)\n        if split_pos == -1:\n            split_pos = segment.rfind(\" \")\n        if split_pos == -1:\n            # Absolute fallback: hard cut but avoid splitting inside a tag\n            split_pos = _safe_hard_cut(segment, max_chars)\n\n        chunk = remaining[: split_pos + 1].strip()\n        if chunk:\n            chunks.append(chunk)\n        remaining = remaining[split_pos + 1 :]\n\n    return chunks\n\n\ndef _find_last_sentence_end(text: str) -> int:\n    \"\"\"Return the index of the last sentence-ending punctuation in *text*.\n\n    Skips periods that follow common abbreviations (``Dr.``, ``Mr.``, etc.)\n    and periods inside bracket tags (``[laugh]``).  Also handles CJK\n    sentence-ending punctuation (``。！？``).\n    \"\"\"\n    best = -1\n    # ASCII sentence ends\n    for m in re.finditer(r\"[.!?](?:\\s|$)\", text):\n        pos = m.start()\n        char = text[pos]\n        # Skip periods after abbreviations\n        if char == \".\":\n            # Walk backwards to find the preceding word\n            word_start = pos - 1\n            while word_start >= 0 and text[word_start].isalpha():\n                word_start -= 1\n            word = text[word_start + 1 : pos].lower()\n            if word in _ABBREVIATIONS:\n                continue\n            # Skip decimal numbers (digit immediately before the period)\n            if word_start >= 0 and text[word_start].isdigit():\n                continue\n        # Skip if we're inside a bracket tag\n        if _inside_bracket_tag(text, pos):\n            continue\n        best = pos\n    # CJK sentence-ending punctuation\n    for m in re.finditer(r\"[\\u3002\\uff01\\uff1f]\", text):\n        if m.start() > best:\n            best = m.start()\n    return best\n\n\ndef _find_last_clause_boundary(text: str) -> int:\n    \"\"\"Return the index of the last clause-boundary punctuation.\"\"\"\n    best = -1\n    for m in re.finditer(r\"[;:,\\u2014](?:\\s|$)\", text):\n        pos = m.start()\n        # Skip if inside a bracket tag\n        if _inside_bracket_tag(text, pos):\n            continue\n        best = pos\n    return best\n\n\ndef _inside_bracket_tag(text: str, pos: int) -> bool:\n    \"\"\"Return True if *pos* falls inside a ``[...]`` tag.\"\"\"\n    for m in _PARA_TAG_RE.finditer(text):\n        if m.start() < pos < m.end():\n            return True\n    return False\n\n\ndef _safe_hard_cut(segment: str, max_chars: int) -> int:\n    \"\"\"Find a hard-cut position that doesn't split a ``[tag]``.\"\"\"\n    cut = max_chars - 1\n    # Check if the cut falls inside a bracket tag; if so, move before it\n    for m in _PARA_TAG_RE.finditer(segment):\n        if m.start() < cut < m.end():\n            return m.start() - 1 if m.start() > 0 else cut\n    return cut\n\n\ndef concatenate_audio_chunks(\n    chunks: List[np.ndarray],\n    sample_rate: int,\n    crossfade_ms: int = 50,\n) -> np.ndarray:\n    \"\"\"Concatenate audio arrays with a short crossfade to eliminate clicks.\n\n    Each chunk is expected to be a 1-D float32 ndarray at *sample_rate* Hz.\n    \"\"\"\n    if not chunks:\n        return np.array([], dtype=np.float32)\n    if len(chunks) == 1:\n        return chunks[0]\n\n    crossfade_samples = int(sample_rate * crossfade_ms / 1000)\n    result = np.array(chunks[0], dtype=np.float32, copy=True)\n\n    for chunk in chunks[1:]:\n        if len(chunk) == 0:\n            continue\n        overlap = min(crossfade_samples, len(result), len(chunk))\n        if overlap > 0:\n            fade_out = np.linspace(1.0, 0.0, overlap, dtype=np.float32)\n            fade_in = np.linspace(0.0, 1.0, overlap, dtype=np.float32)\n            result[-overlap:] = result[-overlap:] * fade_out + chunk[:overlap] * fade_in\n            result = np.concatenate([result, chunk[overlap:]])\n        else:\n            result = np.concatenate([result, chunk])\n\n    return result\n\n\nasync def generate_chunked(\n    backend,\n    text: str,\n    voice_prompt: dict,\n    language: str = \"en\",\n    seed: int | None = None,\n    instruct: str | None = None,\n    max_chunk_chars: int = DEFAULT_MAX_CHUNK_CHARS,\n    crossfade_ms: int = 50,\n    trim_fn=None,\n) -> Tuple[np.ndarray, int]:\n    \"\"\"Generate audio with automatic chunking for long text.\n\n    For text shorter than *max_chunk_chars* this is a thin wrapper around\n    ``backend.generate()`` with zero overhead.\n\n    For longer text the input is split at natural sentence boundaries,\n    each chunk is generated independently, optionally trimmed (useful for\n    Chatterbox engines that hallucinate trailing noise), and the results\n    are concatenated with a crossfade (or hard cut if *crossfade_ms* is 0).\n\n    Parameters\n    ----------\n    backend : TTSBackend\n        Any backend implementing the ``generate()`` protocol.\n    text : str\n        Input text (may be arbitrarily long).\n    voice_prompt, language, seed, instruct\n        Forwarded to ``backend.generate()`` verbatim.\n    max_chunk_chars : int\n        Maximum characters per chunk (default 800).\n    crossfade_ms : int\n        Crossfade duration in milliseconds between chunks.  0 for a hard\n        cut with no overlap (default 50).\n    trim_fn : callable | None\n        Optional ``(audio, sample_rate) -> audio`` post-processing\n        function applied to each chunk before concatenation (e.g.\n        ``trim_tts_output`` for Chatterbox engines).\n\n    Returns\n    -------\n    (audio, sample_rate) : Tuple[np.ndarray, int]\n    \"\"\"\n    chunks = split_text_into_chunks(text, max_chunk_chars)\n\n    if len(chunks) <= 1:\n        # Short text — single-shot fast path\n        audio, sample_rate = await backend.generate(\n            text,\n            voice_prompt,\n            language,\n            seed,\n            instruct,\n        )\n        if trim_fn is not None:\n            audio = trim_fn(audio, sample_rate)\n        return audio, sample_rate\n\n    # Long text — chunked generation\n    logger.info(\n        \"Splitting %d chars into %d chunks (max %d chars each)\",\n        len(text),\n        len(chunks),\n        max_chunk_chars,\n    )\n    audio_chunks: List[np.ndarray] = []\n    sample_rate: int | None = None\n\n    for i, chunk_text in enumerate(chunks):\n        logger.info(\n            \"Generating chunk %d/%d (%d chars)\",\n            i + 1,\n            len(chunks),\n            len(chunk_text),\n        )\n        # Vary the seed per chunk to avoid correlated RNG artefacts,\n        # but keep it deterministic so the same (text, seed) pair\n        # always produces the same output.\n        chunk_seed = (seed + i) if seed is not None else None\n\n        chunk_audio, chunk_sr = await backend.generate(\n            chunk_text,\n            voice_prompt,\n            language,\n            chunk_seed,\n            instruct,\n        )\n        if trim_fn is not None:\n            chunk_audio = trim_fn(chunk_audio, chunk_sr)\n\n        audio_chunks.append(np.asarray(chunk_audio, dtype=np.float32))\n        if sample_rate is None:\n            sample_rate = chunk_sr\n\n    audio = concatenate_audio_chunks(audio_chunks, sample_rate, crossfade_ms=crossfade_ms)\n    return audio, sample_rate\n"
  },
  {
    "path": "backend/utils/dac_shim.py",
    "content": "\"\"\"\nMinimal shim for descript-audio-codec (DAC).\n\nTADA only imports Snake1d from dac.nn.layers and dac.model.dac.\nThe real DAC package pulls in descript-audiotools which depends on\nonnx, tensorboard, protobuf, matplotlib, pystoi, etc. — none of\nwhich are needed for TADA's runtime use of Snake1d.\n\nThis shim provides the exact Snake1d implementation (MIT-licensed,\nfrom https://github.com/descriptinc/descript-audio-codec) so we can\navoid the entire audiotools dependency chain.\n\nIf the real DAC package is installed, this module is never used —\nPython's import system will find the site-packages version first.\nInstall this shim only when descript-audio-codec is NOT installed.\n\"\"\"\n\nimport sys\nimport types\n\nimport torch\nimport torch.nn as nn\n\n\n# ── Snake activation (from dac/nn/layers.py) ────────────────────────\n\n# NOTE: The original DAC code uses @torch.jit.script here for a 1.4x\n# speedup.  We omit it because TorchScript calls inspect.getsource()\n# which fails inside a PyInstaller frozen binary (no .py source files).\ndef snake(x: torch.Tensor, alpha: torch.Tensor) -> torch.Tensor:\n    shape = x.shape\n    x = x.reshape(shape[0], shape[1], -1)\n    x = x + (alpha + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2)\n    x = x.reshape(shape)\n    return x\n\n\nclass Snake1d(nn.Module):\n    def __init__(self, channels: int):\n        super().__init__()\n        self.alpha = nn.Parameter(torch.ones(1, channels, 1))\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        return snake(x, self.alpha)\n\n\n# ── Register as dac.nn.layers and dac.model.dac ─────────────────────\n\ndef install_dac_shim() -> None:\n    \"\"\"Register fake dac package modules in sys.modules.\n\n    Only installs the shim if 'dac' is not already importable\n    (i.e. the real descript-audio-codec is not installed).\n    \"\"\"\n    try:\n        import dac  # noqa: F401  — real package exists, do nothing\n        return\n    except ImportError:\n        pass\n\n    # Create the module tree: dac -> dac.nn -> dac.nn.layers\n    #                              -> dac.model -> dac.model.dac\n    dac_pkg = types.ModuleType(\"dac\")\n    dac_pkg.__path__ = []  # make it a package\n    dac_pkg.__package__ = \"dac\"\n\n    dac_nn = types.ModuleType(\"dac.nn\")\n    dac_nn.__path__ = []\n    dac_nn.__package__ = \"dac.nn\"\n\n    dac_nn_layers = types.ModuleType(\"dac.nn.layers\")\n    dac_nn_layers.__package__ = \"dac.nn\"\n    dac_nn_layers.Snake1d = Snake1d\n    dac_nn_layers.snake = snake\n\n    dac_model = types.ModuleType(\"dac.model\")\n    dac_model.__path__ = []\n    dac_model.__package__ = \"dac.model\"\n\n    dac_model_dac = types.ModuleType(\"dac.model.dac\")\n    dac_model_dac.__package__ = \"dac.model\"\n    dac_model_dac.Snake1d = Snake1d\n\n    # Wire up submodules\n    dac_pkg.nn = dac_nn\n    dac_pkg.model = dac_model\n    dac_nn.layers = dac_nn_layers\n    dac_model.dac = dac_model_dac\n\n    # Register in sys.modules\n    sys.modules[\"dac\"] = dac_pkg\n    sys.modules[\"dac.nn\"] = dac_nn\n    sys.modules[\"dac.nn.layers\"] = dac_nn_layers\n    sys.modules[\"dac.model\"] = dac_model\n    sys.modules[\"dac.model.dac\"] = dac_model_dac\n"
  },
  {
    "path": "backend/utils/effects.py",
    "content": "\"\"\"\nAudio post-processing effects engine.\n\nUses Spotify's pedalboard library to apply professional-grade DSP effects\nto generated audio. Effects are described as a JSON-serializable chain\n(list of effect dicts) so they can be stored in the database and sent\nover the API.\n\nSupported effect types:\n  - chorus      (flanger-style with short delays)\n  - reverb      (room reverb)\n  - delay       (echo / delay line)\n  - compressor  (dynamic range compression)\n  - gain        (volume adjustment in dB)\n  - highpass     (high-pass filter)\n  - lowpass      (low-pass filter)\n  - pitch_shift (semitone pitch shifting)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport numpy as np\nfrom typing import Any, Dict, List, Optional\n\nfrom pedalboard import (\n    Pedalboard,\n    Chorus,\n    Reverb,\n    Compressor,\n    Gain,\n    HighpassFilter,\n    LowpassFilter,\n    Delay,\n    PitchShift,\n)\n\n\n# Each param definition: (default, min, max, description)\nEFFECT_REGISTRY: Dict[str, Dict[str, Any]] = {\n    \"chorus\": {\n        \"cls\": Chorus,\n        \"label\": \"Chorus / Flanger\",\n        \"description\": \"Modulated delay for flanging or chorus effects. Short centre_delay_ms (<10) gives flanger; longer gives chorus.\",\n        \"params\": {\n            \"rate_hz\": {\"default\": 1.0, \"min\": 0.01, \"max\": 20.0, \"step\": 0.01, \"description\": \"LFO speed (Hz)\"},\n            \"depth\": {\"default\": 0.5, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01, \"description\": \"Modulation depth\"},\n            \"feedback\": {\"default\": 0.0, \"min\": 0.0, \"max\": 0.95, \"step\": 0.01, \"description\": \"Feedback amount\"},\n            \"centre_delay_ms\": {\n                \"default\": 7.0,\n                \"min\": 0.5,\n                \"max\": 50.0,\n                \"step\": 0.1,\n                \"description\": \"Centre delay (ms)\",\n            },\n            \"mix\": {\"default\": 0.5, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01, \"description\": \"Wet/dry mix\"},\n        },\n    },\n    \"reverb\": {\n        \"cls\": Reverb,\n        \"label\": \"Reverb\",\n        \"description\": \"Room reverb effect.\",\n        \"params\": {\n            \"room_size\": {\"default\": 0.5, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01, \"description\": \"Room size\"},\n            \"damping\": {\"default\": 0.5, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01, \"description\": \"High frequency damping\"},\n            \"wet_level\": {\"default\": 0.33, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01, \"description\": \"Wet level\"},\n            \"dry_level\": {\"default\": 0.4, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01, \"description\": \"Dry level\"},\n            \"width\": {\"default\": 1.0, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01, \"description\": \"Stereo width\"},\n        },\n    },\n    \"delay\": {\n        \"cls\": Delay,\n        \"label\": \"Delay\",\n        \"description\": \"Echo / delay line.\",\n        \"params\": {\n            \"delay_seconds\": {\n                \"default\": 0.3,\n                \"min\": 0.01,\n                \"max\": 2.0,\n                \"step\": 0.01,\n                \"description\": \"Delay time (seconds)\",\n            },\n            \"feedback\": {\"default\": 0.3, \"min\": 0.0, \"max\": 0.95, \"step\": 0.01, \"description\": \"Feedback amount\"},\n            \"mix\": {\"default\": 0.3, \"min\": 0.0, \"max\": 1.0, \"step\": 0.01, \"description\": \"Wet/dry mix\"},\n        },\n    },\n    \"compressor\": {\n        \"cls\": Compressor,\n        \"label\": \"Compressor\",\n        \"description\": \"Dynamic range compression for consistent loudness.\",\n        \"params\": {\n            \"threshold_db\": {\"default\": -20.0, \"min\": -60.0, \"max\": 0.0, \"step\": 0.5, \"description\": \"Threshold (dB)\"},\n            \"ratio\": {\"default\": 4.0, \"min\": 1.0, \"max\": 20.0, \"step\": 0.1, \"description\": \"Compression ratio\"},\n            \"attack_ms\": {\"default\": 10.0, \"min\": 0.1, \"max\": 100.0, \"step\": 0.1, \"description\": \"Attack time (ms)\"},\n            \"release_ms\": {\n                \"default\": 100.0,\n                \"min\": 10.0,\n                \"max\": 1000.0,\n                \"step\": 1.0,\n                \"description\": \"Release time (ms)\",\n            },\n        },\n    },\n    \"gain\": {\n        \"cls\": Gain,\n        \"label\": \"Gain\",\n        \"description\": \"Volume adjustment in decibels.\",\n        \"params\": {\n            \"gain_db\": {\"default\": 0.0, \"min\": -40.0, \"max\": 40.0, \"step\": 0.5, \"description\": \"Gain (dB)\"},\n        },\n    },\n    \"highpass\": {\n        \"cls\": HighpassFilter,\n        \"label\": \"High-Pass Filter\",\n        \"description\": \"Removes frequencies below the cutoff.\",\n        \"params\": {\n            \"cutoff_frequency_hz\": {\n                \"default\": 80.0,\n                \"min\": 20.0,\n                \"max\": 8000.0,\n                \"step\": 1.0,\n                \"description\": \"Cutoff frequency (Hz)\",\n            },\n        },\n    },\n    \"lowpass\": {\n        \"cls\": LowpassFilter,\n        \"label\": \"Low-Pass Filter\",\n        \"description\": \"Removes frequencies above the cutoff.\",\n        \"params\": {\n            \"cutoff_frequency_hz\": {\n                \"default\": 8000.0,\n                \"min\": 200.0,\n                \"max\": 20000.0,\n                \"step\": 1.0,\n                \"description\": \"Cutoff frequency (Hz)\",\n            },\n        },\n    },\n    \"pitch_shift\": {\n        \"cls\": PitchShift,\n        \"label\": \"Pitch Shift\",\n        \"description\": \"Shift pitch up or down by semitones.\",\n        \"params\": {\n            \"semitones\": {\"default\": 0.0, \"min\": -12.0, \"max\": 12.0, \"step\": 0.5, \"description\": \"Semitones to shift\"},\n        },\n    },\n}\n\n\nBUILTIN_PRESETS: Dict[str, Dict[str, Any]] = {\n    \"robotic\": {\n        \"name\": \"Robotic\",\n        \"sort_order\": 0,\n        \"description\": \"Metallic robotic voice (flanger with slow LFO and high feedback)\",\n        \"effects_chain\": [\n            {\n                \"type\": \"chorus\",\n                \"enabled\": True,\n                \"params\": {\n                    \"rate_hz\": 0.2,\n                    \"depth\": 1.0,\n                    \"feedback\": 0.35,\n                    \"centre_delay_ms\": 7.0,\n                    \"mix\": 0.5,\n                },\n            },\n        ],\n    },\n    \"radio\": {\n        \"name\": \"Radio\",\n        \"sort_order\": 1,\n        \"description\": \"Thin AM-radio voice with band-pass filtering and light compression\",\n        \"effects_chain\": [\n            {\n                \"type\": \"highpass\",\n                \"enabled\": True,\n                \"params\": {\"cutoff_frequency_hz\": 300.0},\n            },\n            {\n                \"type\": \"lowpass\",\n                \"enabled\": True,\n                \"params\": {\"cutoff_frequency_hz\": 3500.0},\n            },\n            {\n                \"type\": \"compressor\",\n                \"enabled\": True,\n                \"params\": {\n                    \"threshold_db\": -15.0,\n                    \"ratio\": 6.0,\n                    \"attack_ms\": 5.0,\n                    \"release_ms\": 50.0,\n                },\n            },\n            {\n                \"type\": \"gain\",\n                \"enabled\": True,\n                \"params\": {\"gain_db\": 6.0},\n            },\n        ],\n    },\n    \"echo_chamber\": {\n        \"name\": \"Echo Chamber\",\n        \"sort_order\": 2,\n        \"description\": \"Spacious reverb with trailing echo\",\n        \"effects_chain\": [\n            {\n                \"type\": \"reverb\",\n                \"enabled\": True,\n                \"params\": {\n                    \"room_size\": 0.85,\n                    \"damping\": 0.3,\n                    \"wet_level\": 0.45,\n                    \"dry_level\": 0.55,\n                    \"width\": 1.0,\n                },\n            },\n            {\n                \"type\": \"delay\",\n                \"enabled\": True,\n                \"params\": {\n                    \"delay_seconds\": 0.25,\n                    \"feedback\": 0.3,\n                    \"mix\": 0.2,\n                },\n            },\n        ],\n    },\n    \"deep_voice\": {\n        \"name\": \"Deep Voice\",\n        \"sort_order\": 99,\n        \"description\": \"Lower pitch with added warmth\",\n        \"effects_chain\": [\n            {\n                \"type\": \"pitch_shift\",\n                \"enabled\": True,\n                \"params\": {\"semitones\": -3.0},\n            },\n            {\n                \"type\": \"lowpass\",\n                \"enabled\": True,\n                \"params\": {\"cutoff_frequency_hz\": 6000.0},\n            },\n            {\n                \"type\": \"compressor\",\n                \"enabled\": True,\n                \"params\": {\n                    \"threshold_db\": -18.0,\n                    \"ratio\": 3.0,\n                    \"attack_ms\": 10.0,\n                    \"release_ms\": 150.0,\n                },\n            },\n        ],\n    },\n}\n\n\ndef get_available_effects() -> List[Dict[str, Any]]:\n    \"\"\"Return the list of available effect types with their parameter definitions.\n\n    Used by the frontend to build the effects chain editor UI.\n    \"\"\"\n    result = []\n    for effect_type, info in EFFECT_REGISTRY.items():\n        result.append(\n            {\n                \"type\": effect_type,\n                \"label\": info[\"label\"],\n                \"description\": info[\"description\"],\n                \"params\": {name: {k: v for k, v in pdef.items()} for name, pdef in info[\"params\"].items()},\n            }\n        )\n    return result\n\n\ndef get_builtin_presets() -> Dict[str, Dict[str, Any]]:\n    \"\"\"Return all built-in effect presets.\"\"\"\n    return BUILTIN_PRESETS\n\n\ndef validate_effects_chain(effects_chain: List[Dict[str, Any]]) -> Optional[str]:\n    \"\"\"Validate an effects chain configuration.\n\n    Returns None if valid, or an error message string.\n    \"\"\"\n    if not isinstance(effects_chain, list):\n        return \"effects_chain must be a list\"\n\n    for i, effect in enumerate(effects_chain):\n        if not isinstance(effect, dict):\n            return f\"Effect at index {i} must be a dict\"\n\n        effect_type = effect.get(\"type\")\n        if effect_type not in EFFECT_REGISTRY:\n            return f\"Unknown effect type '{effect_type}' at index {i}. Available: {list(EFFECT_REGISTRY.keys())}\"\n\n        params = effect.get(\"params\", {})\n        if not isinstance(params, dict):\n            return f\"Effect '{effect_type}' at index {i}: params must be a dict\"\n\n        registry = EFFECT_REGISTRY[effect_type]\n        for param_name, value in params.items():\n            if param_name not in registry[\"params\"]:\n                return f\"Effect '{effect_type}' at index {i}: unknown param '{param_name}'\"\n\n            pdef = registry[\"params\"][param_name]\n            if not isinstance(value, (int, float)):\n                return f\"Effect '{effect_type}' at index {i}: param '{param_name}' must be a number\"\n            if value < pdef[\"min\"] or value > pdef[\"max\"]:\n                return (\n                    f\"Effect '{effect_type}' at index {i}: param '{param_name}' \"\n                    f\"must be between {pdef['min']} and {pdef['max']} (got {value})\"\n                )\n\n    return None\n\n\ndef build_pedalboard(effects_chain: List[Dict[str, Any]]) -> Pedalboard:\n    \"\"\"Build a Pedalboard instance from an effects chain config.\n\n    Skips effects where ``enabled`` is ``False``.\n    \"\"\"\n    plugins = []\n    for effect in effects_chain:\n        if not effect.get(\"enabled\", True):\n            continue\n\n        effect_type = effect[\"type\"]\n        registry = EFFECT_REGISTRY[effect_type]\n        cls = registry[\"cls\"]\n\n        # Merge defaults with provided params\n        params = {}\n        for pname, pdef in registry[\"params\"].items():\n            params[pname] = effect.get(\"params\", {}).get(pname, pdef[\"default\"])\n\n        plugins.append(cls(**params))\n\n    return Pedalboard(plugins)\n\n\ndef apply_effects(\n    audio: np.ndarray,\n    sample_rate: int,\n    effects_chain: List[Dict[str, Any]],\n) -> np.ndarray:\n    \"\"\"Apply an effects chain to audio data.\n\n    Args:\n        audio: Input audio array (1-D mono float32).\n        sample_rate: Sample rate in Hz.\n        effects_chain: List of effect configuration dicts.\n\n    Returns:\n        Processed audio array.\n    \"\"\"\n    if not effects_chain:\n        return audio\n\n    board = build_pedalboard(effects_chain)\n\n    # pedalboard expects shape (channels, samples)\n    if audio.ndim == 1:\n        audio_2d = audio[np.newaxis, :]\n    else:\n        audio_2d = audio\n\n    processed = board(audio_2d.astype(np.float32), sample_rate)\n\n    # Return same dimensionality as input\n    if audio.ndim == 1:\n        return processed[0]\n    return processed\n"
  },
  {
    "path": "backend/utils/hf_offline_patch.py",
    "content": "\"\"\"Monkey-patch huggingface_hub to force offline mode with cached models.\n\nPrevents mlx_audio from making network requests when models are already\ndownloaded. Must be imported BEFORE mlx_audio.\n\"\"\"\n\nimport logging\nimport os\nfrom pathlib import Path\nfrom typing import Optional, Union\n\nlogger = logging.getLogger(__name__)\n\n\ndef patch_huggingface_hub_offline():\n    \"\"\"Monkey-patch huggingface_hub to force offline mode.\"\"\"\n    try:\n        import huggingface_hub  # noqa: F401 -- need the package loaded\n        from huggingface_hub import constants as hf_constants\n        from huggingface_hub.file_download import _try_to_load_from_cache\n\n        original_try_load = _try_to_load_from_cache\n\n        def _patched_try_to_load_from_cache(\n            repo_id: str,\n            filename: str,\n            cache_dir: Union[str, Path, None] = None,\n            revision: Optional[str] = None,\n            repo_type: Optional[str] = None,\n        ):\n            result = original_try_load(\n                repo_id=repo_id,\n                filename=filename,\n                cache_dir=cache_dir,\n                revision=revision,\n                repo_type=repo_type,\n            )\n\n            if result is None:\n                cache_path = Path(hf_constants.HF_HUB_CACHE) / f\"models--{repo_id.replace('/', '--')}\"\n                logger.debug(\"file not cached: %s/%s (expected at %s)\", repo_id, filename, cache_path)\n            else:\n                logger.debug(\"cache hit: %s/%s\", repo_id, filename)\n\n            return result\n\n        import huggingface_hub.file_download as fd\n\n        fd._try_to_load_from_cache = _patched_try_to_load_from_cache\n        logger.debug(\"huggingface_hub patched for offline mode\")\n\n    except ImportError:\n        logger.debug(\"huggingface_hub not available, skipping offline patch\")\n    except Exception:\n        logger.exception(\"failed to patch huggingface_hub for offline mode\")\n\n\ndef ensure_original_qwen_config_cached():\n    \"\"\"Symlink the original Qwen repo cache to the MLX community version.\n\n    mlx_audio may try to fetch config from the original Qwen repo. If only\n    the MLX community variant is cached, create a symlink so the cache lookup\n    succeeds without a network request.\n    \"\"\"\n    try:\n        from huggingface_hub import constants as hf_constants\n    except ImportError:\n        return\n\n    original_repo = \"Qwen/Qwen3-TTS-12Hz-1.7B-Base\"\n    mlx_repo = \"mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16\"\n\n    cache_dir = Path(hf_constants.HF_HUB_CACHE)\n    original_path = cache_dir / f\"models--{original_repo.replace('/', '--')}\"\n    mlx_path = cache_dir / f\"models--{mlx_repo.replace('/', '--')}\"\n\n    if not original_path.exists() and mlx_path.exists():\n        try:\n            original_path.parent.mkdir(parents=True, exist_ok=True)\n            original_path.symlink_to(mlx_path, target_is_directory=True)\n            logger.info(\"created cache symlink: %s -> %s\", original_repo, mlx_repo)\n        except Exception:\n            logger.warning(\"could not create cache symlink for %s\", original_repo, exc_info=True)\n\n\nif os.environ.get(\"VOICEBOX_OFFLINE_PATCH\", \"1\") != \"0\":\n    patch_huggingface_hub_offline()\n    ensure_original_qwen_config_cached()\n"
  },
  {
    "path": "backend/utils/hf_progress.py",
    "content": "\"\"\"\nHuggingFace Hub download progress tracking.\n\"\"\"\n\nfrom typing import Optional, Callable\nfrom contextlib import contextmanager\nimport logging\nimport threading\nimport sys\n\nlogger = logging.getLogger(__name__)\n\n\nclass HFProgressTracker:\n    \"\"\"Tracks HuggingFace Hub download progress by intercepting tqdm.\"\"\"\n\n    def __init__(self, progress_callback: Optional[Callable] = None, filter_non_downloads: bool = False):\n        self.progress_callback = progress_callback\n        self.filter_non_downloads = filter_non_downloads  # Only filter if True\n        self._original_tqdm_class = None\n        self._lock = threading.Lock()\n        self._total_downloaded = 0\n        self._total_size = 0\n        self._file_sizes = {}  # Track sizes of individual files\n        self._file_downloaded = {}  # Track downloaded bytes per file\n        self._current_filename = \"\"\n        self._active_tqdms = {}  # Track active tqdm instances\n        self._hf_tqdm_original_update = None  # For monkey-patching hf's tqdm\n\n    def _create_tracked_tqdm_class(self):\n        \"\"\"Create a tqdm subclass that tracks progress.\"\"\"\n        tracker = self\n        original_tqdm = self._original_tqdm_class\n\n        class TrackedTqdm(original_tqdm):\n            \"\"\"A tqdm subclass that reports progress to our tracker.\"\"\"\n\n            def __init__(self, *args, **kwargs):\n                # Extract filename from desc before passing to parent\n                desc = kwargs.get(\"desc\", \"\")\n                if not desc and args:\n                    first_arg = args[0]\n                    if isinstance(first_arg, str):\n                        desc = first_arg\n\n                filename = \"\"\n                if desc:\n                    # Try to extract filename from description\n                    # HuggingFace Hub uses format like \"model.safetensors: 0%|...\"\n                    if \":\" in desc:\n                        filename = desc.split(\":\")[0].strip()\n                    else:\n                        filename = desc.strip()\n\n                # Filter out non-standard kwargs that huggingface_hub might pass\n                # These are custom kwargs that tqdm doesn't understand\n                filtered_kwargs = {}\n                # Known tqdm kwargs - pass these through\n                tqdm_kwargs = {\n                    \"iterable\",\n                    \"desc\",\n                    \"total\",\n                    \"leave\",\n                    \"file\",\n                    \"ncols\",\n                    \"mininterval\",\n                    \"maxinterval\",\n                    \"miniters\",\n                    \"ascii\",\n                    \"disable\",\n                    \"unit\",\n                    \"unit_scale\",\n                    \"dynamic_ncols\",\n                    \"smoothing\",\n                    \"bar_format\",\n                    \"initial\",\n                    \"position\",\n                    \"postfix\",\n                    \"unit_divisor\",\n                    \"write_bytes\",\n                    \"lock_args\",\n                    \"nrows\",\n                    \"colour\",\n                    \"color\",\n                    \"delay\",\n                    \"gui\",\n                    \"disable_default\",\n                    \"pos\",\n                }\n                for key, value in kwargs.items():\n                    if key in tqdm_kwargs:\n                        filtered_kwargs[key] = value\n\n                # Force-enable the progress bar — we're tracking progress ourselves,\n                # we don't need tqdm to render to a terminal, but we DO need\n                # self.n to be updated when update() is called.\n                filtered_kwargs[\"disable\"] = False\n\n                # Try to initialize with filtered kwargs, fall back to all kwargs if that fails\n                try:\n                    super().__init__(*args, **filtered_kwargs)\n                except TypeError:\n                    # If filtering failed, try with all kwargs (maybe tqdm version accepts them)\n                    kwargs[\"disable\"] = False\n                    super().__init__(*args, **kwargs)\n\n                self._tracker_filename = filename or \"unknown\"\n\n                with tracker._lock:\n                    if filename:\n                        tracker._current_filename = filename\n                    tracker._active_tqdms[id(self)] = {\n                        \"filename\": self._tracker_filename,\n                    }\n\n            def update(self, n=1):\n                result = super().update(n)\n\n                # Report progress\n                with tracker._lock:\n                    if id(self) in tracker._active_tqdms:\n                        filename = tracker._active_tqdms[id(self)][\"filename\"]\n                        current = getattr(self, \"n\", 0)\n                        total = getattr(self, \"total\", 0)\n\n                        if total and total > 0:\n                            # Always filter out non-byte progress bars (e.g., \"Fetching 12 files\")\n                            # These cause crazy percentages because they're counting files, not bytes\n                            if self._is_non_byte_progress(filename):\n                                return result\n\n                            # When model is cached, also filter out generation-related progress\n                            if tracker.filter_non_downloads:\n                                if not self._is_download_progress(filename):\n                                    return result\n\n                            # Update per-file tracking\n                            tracker._file_sizes[filename] = total\n                            tracker._file_downloaded[filename] = current\n\n                            # Calculate totals across all files\n                            tracker._total_size = sum(tracker._file_sizes.values())\n                            tracker._total_downloaded = sum(tracker._file_downloaded.values())\n\n                            # Only report progress once we have a meaningful total (at least 1MB)\n                            # This avoids the \"100% at 0MB\" issue when small config\n                            # files are counted before the real model files\n                            MIN_TOTAL_BYTES = 1_000_000  # 1MB\n                            if tracker._total_size < MIN_TOTAL_BYTES:\n                                return result\n\n                            # Call progress callback\n                            if tracker.progress_callback:\n                                tracker.progress_callback(tracker._total_downloaded, tracker._total_size, filename)\n\n                return result\n\n            def _is_non_byte_progress(self, filename: str) -> bool:\n                \"\"\"Check if this progress bar should be SKIPPED (returns True to skip).\n\n                We want to track byte-based progress bars. This method identifies\n                progress bars that count files/items instead of bytes, which would\n                cause crazy percentages if mixed with our byte counting.\n\n                Returns:\n                    True = SKIP this bar (it's not byte-based)\n                    False = TRACK this bar (it counts bytes)\n                \"\"\"\n                if not filename:\n                    return False\n\n                filename_lower = filename.lower()\n\n                # Skip \"Fetching X files\" - it counts files (total=12), not bytes\n                # Don't skip \"Downloading (incomplete total...)\" - that IS byte-based\n                skip_patterns = [\n                    \"fetching\",  # \"Fetching 12 files\" has total=12 files, not bytes\n                ]\n                return any(pattern in filename_lower for pattern in skip_patterns)\n\n            def _is_download_progress(self, filename: str) -> bool:\n                \"\"\"Check if this is a real file download progress bar vs internal processing.\"\"\"\n                if not filename or filename == \"unknown\":\n                    return False\n\n                # Real downloads have file extensions\n                download_extensions = [\n                    \".safetensors\",\n                    \".bin\",\n                    \".pt\",\n                    \".pth\",  # Model weights\n                    \".json\",\n                    \".txt\",\n                    \".py\",  # Config files\n                    \".msgpack\",\n                    \".h5\",  # Other formats\n                ]\n\n                filename_lower = filename.lower()\n                has_extension = any(filename_lower.endswith(ext) for ext in download_extensions)\n\n                # Skip generation-related progress indicators\n                skip_patterns = [\"segment\", \"processing\", \"generating\", \"loading\"]\n                has_skip_pattern = any(pattern in filename_lower for pattern in skip_patterns)\n\n                return has_extension and not has_skip_pattern\n\n            def close(self):\n                with tracker._lock:\n                    if id(self) in tracker._active_tqdms:\n                        del tracker._active_tqdms[id(self)]\n                return super().close()\n\n        return TrackedTqdm\n\n    @contextmanager\n    def patch_download(self):\n        \"\"\"Context manager to patch tqdm for progress tracking.\"\"\"\n        try:\n            import tqdm as tqdm_module\n\n            # Store original tqdm class\n            self._original_tqdm_class = tqdm_module.tqdm\n\n            # Reset totals\n            with self._lock:\n                self._total_downloaded = 0\n                self._total_size = 0\n                self._file_sizes = {}\n                self._file_downloaded = {}\n                self._current_filename = \"\"\n                self._active_tqdms = {}\n\n            # Create our tracked tqdm class\n            tracked_tqdm = self._create_tracked_tqdm_class()\n\n            # Patch tqdm.tqdm\n            tqdm_module.tqdm = tracked_tqdm\n\n            # Also patch tqdm.auto.tqdm if it exists (used by huggingface_hub)\n            self._original_tqdm_auto = None\n            if hasattr(tqdm_module, \"auto\") and hasattr(tqdm_module.auto, \"tqdm\"):\n                self._original_tqdm_auto = tqdm_module.auto.tqdm\n                tqdm_module.auto.tqdm = tracked_tqdm\n\n            # Patch in sys.modules to catch already-imported references\n            # huggingface_hub uses: from tqdm.auto import tqdm as base_tqdm\n            # So we need to patch both 'tqdm' and 'base_tqdm' attributes\n            self._patched_modules = {}\n            tqdm_attr_names = [\"tqdm\", \"base_tqdm\", \"old_tqdm\"]  # Various names used\n\n            patched_count = 0\n            for module_name in list(sys.modules.keys()):\n                if \"huggingface\" in module_name or module_name.startswith(\"tqdm\"):\n                    try:\n                        module = sys.modules[module_name]\n                        for attr_name in tqdm_attr_names:\n                            if hasattr(module, attr_name):\n                                attr = getattr(module, attr_name)\n                                # Only patch if it's a tqdm class (not already patched)\n                                is_tqdm_class = (\n                                    attr is self._original_tqdm_class\n                                    or (self._original_tqdm_auto and attr is self._original_tqdm_auto)\n                                    or (\n                                        hasattr(attr, \"__name__\")\n                                        and attr.__name__ == \"tqdm\"\n                                        and hasattr(attr, \"update\")\n                                    )  # tqdm classes have update method\n                                )\n                                if is_tqdm_class:\n                                    key = f\"{module_name}.{attr_name}\"\n                                    self._patched_modules[key] = (module, attr_name, attr)\n                                    setattr(module, attr_name, tracked_tqdm)\n                                    patched_count += 1\n                    except (AttributeError, TypeError):\n                        pass\n\n            # ALSO monkey-patch the update method on huggingface_hub's tqdm class\n            # This is needed because the class was already defined at import time\n            self._hf_tqdm_original_update = None\n            try:\n                from huggingface_hub.utils import tqdm as hf_tqdm_module\n\n                if hasattr(hf_tqdm_module, \"tqdm\"):\n                    hf_tqdm_class = hf_tqdm_module.tqdm\n                    self._hf_tqdm_original_update = hf_tqdm_class.update\n\n                    # Create a wrapper that calls our tracking\n                    tracker = self  # Reference to HFProgressTracker instance\n\n                    def patched_update(tqdm_self, n=1):\n                        result = tracker._hf_tqdm_original_update(tqdm_self, n)\n\n                        # Track this progress\n                        with tracker._lock:\n                            desc = getattr(tqdm_self, \"desc\", \"\") or \"\"\n                            current = getattr(tqdm_self, \"n\", 0)\n                            total = getattr(tqdm_self, \"total\", 0) or 0\n\n                            # Skip non-byte progress bars\n                            if \"fetching\" in desc.lower():\n                                return result\n\n                            # Skip until we have a meaningful total (at least 1MB)\n                            # This avoids the \"100% at 0MB\" issue when small config\n                            # files are counted before the real model files\n                            MIN_TOTAL_BYTES = 1_000_000  # 1MB\n                            if total >= MIN_TOTAL_BYTES:\n                                tracker._total_downloaded = current\n                                tracker._total_size = total\n\n                                if tracker.progress_callback:\n                                    tracker.progress_callback(current, total, desc)\n\n                        return result\n\n                    hf_tqdm_class.update = patched_update\n                    patched_count += 1\n                    logger.debug(\"Monkey-patched huggingface_hub.utils.tqdm.tqdm.update\")\n            except (ImportError, AttributeError) as e:\n                logger.warning(\"Could not monkey-patch hf_tqdm: %s\", e)\n\n            logger.debug(\"Patched %d tqdm references\", patched_count)\n\n            yield\n\n        except ImportError:\n            # If tqdm not available, just yield without patching\n            yield\n        finally:\n            # Restore original tqdm\n            if self._original_tqdm_class:\n                try:\n                    import tqdm as tqdm_module\n\n                    tqdm_module.tqdm = self._original_tqdm_class\n\n                    if self._original_tqdm_auto:\n                        tqdm_module.auto.tqdm = self._original_tqdm_auto\n\n                    # Restore patched modules\n                    for key, (module, attr_name, original) in self._patched_modules.items():\n                        try:\n                            if module and original:\n                                setattr(module, attr_name, original)\n                        except (AttributeError, TypeError):\n                            pass\n                    self._patched_modules = {}\n\n                    # Restore hf_tqdm's original update method\n                    if self._hf_tqdm_original_update:\n                        try:\n                            from huggingface_hub.utils import tqdm as hf_tqdm_module\n\n                            if hasattr(hf_tqdm_module, \"tqdm\"):\n                                hf_tqdm_module.tqdm.update = self._hf_tqdm_original_update\n                        except (ImportError, AttributeError):\n                            pass\n                        self._hf_tqdm_original_update = None\n\n                except (ImportError, AttributeError):\n                    pass\n\n\ndef create_hf_progress_callback(model_name: str, progress_manager):\n    \"\"\"Create a progress callback for HuggingFace downloads.\"\"\"\n\n    def callback(downloaded: int, total: int, filename: str = \"\"):\n        \"\"\"Progress callback.\n\n        Note: We send updates even when total=0 (unknown) to provide feedback\n        during the \"incomplete total\" phase of huggingface_hub downloads.\n        The frontend handles total=0 gracefully.\n        \"\"\"\n        progress_manager.update_progress(\n            model_name=model_name,\n            current=downloaded,\n            total=total,\n            filename=filename or \"\",\n            status=\"downloading\",\n        )\n\n    return callback\n"
  },
  {
    "path": "backend/utils/images.py",
    "content": "\"\"\"Image processing utilities for avatar uploads.\"\"\"\n\nfrom pathlib import Path\nfrom typing import Optional, Tuple\nfrom PIL import Image\n\n# JPEG can be reported as 'JPEG' or 'MPO' (for multi-picture format from some cameras)\nALLOWED_FORMATS = {'PNG', 'JPEG', 'WEBP', 'MPO', 'JPG'}\nMAX_SIZE = 512\nMAX_FILE_SIZE = 5 * 1024 * 1024  # 5MB\n\n\ndef validate_image(file_path: str) -> Tuple[bool, Optional[str]]:\n    \"\"\"\n    Validate image format and file size.\n    \n    Args:\n        file_path: Path to image file\n        \n    Returns:\n        Tuple of (is_valid, error_message)\n    \"\"\"\n    path = Path(file_path)\n    \n    # Check file size\n    if path.stat().st_size > MAX_FILE_SIZE:\n        return False, f\"File size exceeds maximum of {MAX_FILE_SIZE // (1024 * 1024)}MB\"\n    \n    try:\n        with Image.open(file_path) as img:\n            # Verify the image can be loaded\n            img.load()\n            \n            # Check format (normalize JPEG variants)\n            img_format = img.format\n            if img_format in ('MPO', 'JPG'):\n                img_format = 'JPEG'\n            \n            if img_format not in {'PNG', 'JPEG', 'WEBP'}:\n                return False, f\"Invalid format '{img_format}'. Allowed formats: PNG, JPEG, WEBP\"\n            \n            return True, None\n    except Exception as e:\n        return False, f\"Invalid image file: {str(e)}\"\n\n\ndef process_avatar(input_path: str, output_path: str, max_size: int = MAX_SIZE) -> None:\n    \"\"\"\n    Process avatar image: resize and optimize.\n    \n    Resizes image to fit within max_size x max_size while maintaining aspect ratio.\n    \n    Args:\n        input_path: Path to input image\n        output_path: Path to save processed image\n        max_size: Maximum width or height in pixels\n    \"\"\"\n    with Image.open(input_path) as img:\n        # Handle EXIF orientation for JPEG images\n        try:\n            from PIL import ExifTags\n            for orientation in ExifTags.TAGS.keys():\n                if ExifTags.TAGS[orientation] == 'Orientation':\n                    break\n            exif = img._getexif()\n            if exif is not None:\n                orientation_value = exif.get(orientation)\n                if orientation_value == 3:\n                    img = img.rotate(180, expand=True)\n                elif orientation_value == 6:\n                    img = img.rotate(270, expand=True)\n                elif orientation_value == 8:\n                    img = img.rotate(90, expand=True)\n        except (AttributeError, KeyError, IndexError, TypeError):\n            # No EXIF data or orientation tag\n            pass\n        \n        # Convert to RGB if necessary (handles RGBA, P, CMYK, etc.)\n        if img.mode not in ('RGB', 'L'):\n            if img.mode == 'RGBA':\n                # Create white background for RGBA images\n                background = Image.new('RGB', img.size, (255, 255, 255))\n                background.paste(img, mask=img.split()[3])  # Use alpha channel as mask\n                img = background\n            elif img.mode == 'CMYK':\n                # Convert CMYK to RGB\n                img = img.convert('RGB')\n            elif img.mode == 'P':\n                # Convert palette mode to RGB\n                img = img.convert('RGB')\n            else:\n                img = img.convert('RGB')\n        \n        # Calculate new size maintaining aspect ratio\n        img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)\n        \n        # Determine output format from extension\n        output_ext = Path(output_path).suffix.lower()\n        \n        format_map = {\n            '.png': 'PNG',\n            '.jpeg': 'JPEG',\n            '.jpg': 'JPEG',\n            '.webp': 'WEBP'\n        }\n        \n        output_format = format_map.get(output_ext, 'PNG')\n        \n        # Save with optimization\n        save_kwargs = {'optimize': True}\n        if output_format == 'JPEG':\n            save_kwargs['quality'] = 90\n        \n        img.save(output_path, format=output_format, **save_kwargs)\n"
  },
  {
    "path": "backend/utils/platform_detect.py",
    "content": "\"\"\"\nPlatform detection for backend selection.\n\"\"\"\n\nimport platform\nfrom typing import Literal\n\n\ndef is_apple_silicon() -> bool:\n    \"\"\"\n    Check if running on Apple Silicon (arm64 macOS).\n    \n    Returns:\n        True if on Apple Silicon, False otherwise\n    \"\"\"\n    return platform.system() == \"Darwin\" and platform.machine() == \"arm64\"\n\n\ndef get_backend_type() -> Literal[\"mlx\", \"pytorch\"]:\n    \"\"\"\n    Detect the best backend for the current platform.\n\n    Returns:\n        \"mlx\" on Apple Silicon (if MLX is available and functional), \"pytorch\" otherwise\n    \"\"\"\n    if is_apple_silicon():\n        try:\n            import mlx.core  # noqa: F401 — triggers native lib loading\n            return \"mlx\"\n        except (ImportError, OSError, RuntimeError):\n            # MLX not installed, or native libraries failed to load inside a\n            # PyInstaller bundle (OSError on missing .dylib / .metallib).\n            # Fall through to PyTorch.\n            return \"pytorch\"\n    return \"pytorch\"\n"
  },
  {
    "path": "backend/utils/progress.py",
    "content": "\"\"\"\nProgress tracking for model downloads using Server-Sent Events.\n\"\"\"\n\nfrom typing import Optional, Callable, Dict, List\nfrom fastapi.responses import StreamingResponse\nimport asyncio\nimport json\nimport threading\nfrom datetime import datetime\n\n\nclass ProgressManager:\n    \"\"\"Manages download progress for multiple models.\n    \n    Thread-safe: can be called from background threads (e.g., via asyncio.to_thread).\n    \"\"\"\n    \n    # Throttle settings to prevent overwhelming SSE clients\n    THROTTLE_INTERVAL_SECONDS = 0.5  # Minimum time between updates\n    THROTTLE_PROGRESS_DELTA = 1.0    # Minimum progress change (%) to force update\n    \n    def __init__(self):\n        self._progress: Dict[str, Dict] = {}\n        self._listeners: Dict[str, list] = {}\n        self._lock = threading.Lock()  # Thread-safe lock for progress dict\n        self._main_loop: Optional[asyncio.AbstractEventLoop] = None\n        self._last_notify_time: Dict[str, float] = {}  # Last notification time per model\n        self._last_notify_progress: Dict[str, float] = {}  # Last notified progress per model\n    \n    def _set_main_loop(self, loop: asyncio.AbstractEventLoop):\n        \"\"\"Set the main event loop for thread-safe operations.\"\"\"\n        self._main_loop = loop\n    \n    def _notify_listeners_threadsafe(self, model_name: str, progress_data: Dict):\n        \"\"\"Notify listeners in a thread-safe manner.\"\"\"\n        import logging\n        logger = logging.getLogger(__name__)\n        \n        if model_name not in self._listeners:\n            return\n            \n        for queue in self._listeners[model_name]:\n            try:\n                # Check if we're in the main event loop thread\n                try:\n                    running_loop = asyncio.get_running_loop()\n                    # We're in an async context, can use put_nowait directly\n                    queue.put_nowait(progress_data.copy())\n                except RuntimeError:\n                    # Not in async context (running in background thread)\n                    # Use call_soon_threadsafe to safely put on queue\n                    if self._main_loop and self._main_loop.is_running():\n                        self._main_loop.call_soon_threadsafe(\n                            lambda q=queue, d=progress_data.copy(): q.put_nowait(d) if not q.full() else None\n                        )\n                    else:\n                        logger.debug(f\"No main loop available for {model_name}, skipping notification\")\n            except asyncio.QueueFull:\n                logger.warning(f\"Queue full for {model_name}, dropping update\")\n            except Exception as e:\n                logger.warning(f\"Error notifying listener for {model_name}: {e}\")\n\n    def update_progress(\n        self,\n        model_name: str,\n        current: int,\n        total: int,\n        filename: Optional[str] = None,\n        status: str = \"downloading\",\n    ):\n        \"\"\"\n        Update progress for a model download.\n\n        Thread-safe: can be called from background threads.\n        \n        Progress updates are throttled to prevent overwhelming SSE clients.\n        Updates are sent at most every THROTTLE_INTERVAL_SECONDS, or when\n        progress changes by at least THROTTLE_PROGRESS_DELTA percent.\n\n        Args:\n            model_name: Name of the model (e.g., \"qwen-tts-1.7B\", \"whisper-base\")\n            current: Current bytes downloaded\n            total: Total bytes to download\n            filename: Current file being downloaded\n            status: Status string (downloading, extracting, complete, error)\n        \"\"\"\n        import logging\n        import time\n        logger = logging.getLogger(__name__)\n\n        # Calculate progress percentage, clamped to 0-100 range\n        # This prevents crazy percentages from edge cases like:\n        # - current > total temporarily during aggregation\n        # - mixing file-count progress with byte-count progress\n        if total > 0:\n            progress_pct = min(100.0, max(0.0, (current / total * 100)))\n        else:\n            progress_pct = 0\n\n        progress_data = {\n            \"model_name\": model_name,\n            \"current\": current,\n            \"total\": total,\n            \"progress\": progress_pct,\n            \"filename\": filename,\n            \"status\": status,\n            \"timestamp\": datetime.now().isoformat(),\n        }\n\n        # Thread-safe update of progress dict (always update internal state)\n        with self._lock:\n            self._progress[model_name] = progress_data\n\n        # Check if we should notify listeners (throttling)\n        current_time = time.time()\n        last_time = self._last_notify_time.get(model_name, 0)\n        last_progress = self._last_notify_progress.get(model_name, -100)\n        \n        time_delta = current_time - last_time\n        progress_delta = abs(progress_pct - last_progress)\n        \n        # Always notify for complete/error status, or if throttle conditions are met\n        should_notify = (\n            status in (\"complete\", \"error\") or\n            time_delta >= self.THROTTLE_INTERVAL_SECONDS or\n            progress_delta >= self.THROTTLE_PROGRESS_DELTA\n        )\n        \n        if not should_notify:\n            return  # Skip this update (throttled)\n        \n        # Update throttle tracking\n        self._last_notify_time[model_name] = current_time\n        self._last_notify_progress[model_name] = progress_pct\n\n        # Notify all listeners (thread-safe)\n        listener_count = len(self._listeners.get(model_name, []))\n\n        if listener_count > 0:\n            logger.debug(f\"Notifying {listener_count} listeners for {model_name}: {progress_pct:.1f}% ({filename})\")\n            self._notify_listeners_threadsafe(model_name, progress_data)\n        else:\n            logger.debug(f\"No listeners for {model_name}, progress update stored: {progress_pct:.1f}%\")\n    \n    def get_progress(self, model_name: str) -> Optional[Dict]:\n        \"\"\"Get current progress for a model. Thread-safe.\"\"\"\n        with self._lock:\n            progress = self._progress.get(model_name)\n            return progress.copy() if progress else None\n    \n    def get_all_active(self) -> List[Dict]:\n        \"\"\"Get all active downloads (status is 'downloading' or 'extracting'). Thread-safe.\"\"\"\n        active = []\n        with self._lock:\n            for model_name, progress in self._progress.items():\n                status = progress.get(\"status\", \"\")\n                if status in (\"downloading\", \"extracting\"):\n                    active.append(progress.copy())\n        return active\n    \n    def create_progress_callback(self, model_name: str, filename: Optional[str] = None):\n        \"\"\"\n        Create a progress callback function for HuggingFace downloads.\n        \n        Args:\n            model_name: Name of the model\n            filename: Optional filename filter\n            \n        Returns:\n            Callback function\n        \"\"\"\n        def callback(progress: Dict):\n            \"\"\"HuggingFace Hub progress callback.\"\"\"\n            if \"total\" in progress and \"current\" in progress:\n                current = progress.get(\"current\", 0)\n                total = progress.get(\"total\", 0)\n                file_name = progress.get(\"filename\", filename)\n                \n                self.update_progress(\n                    model_name=model_name,\n                    current=current,\n                    total=total,\n                    filename=file_name,\n                    status=\"downloading\",\n                )\n        \n        return callback\n    \n    async def subscribe(self, model_name: str):\n        \"\"\"\n        Subscribe to progress updates for a model.\n\n        Yields progress updates as Server-Sent Events.\n        \"\"\"\n        import logging\n        logger = logging.getLogger(__name__)\n        \n        # Store the main event loop for thread-safe operations\n        try:\n            self._main_loop = asyncio.get_running_loop()\n        except RuntimeError:\n            pass\n\n        queue = asyncio.Queue(maxsize=10)\n\n        # Add to listeners\n        if model_name not in self._listeners:\n            self._listeners[model_name] = []\n        self._listeners[model_name].append(queue)\n\n        logger.info(f\"SSE client subscribed to {model_name}, total listeners: {len(self._listeners[model_name])}\")\n\n        try:\n            # Send initial progress if available and still in progress (thread-safe read)\n            with self._lock:\n                initial_progress = self._progress.get(model_name)\n                if initial_progress:\n                    initial_progress = initial_progress.copy()\n            \n            if initial_progress:\n                status = initial_progress.get('status')\n                # Only send initial progress if download is actually in progress\n                # Don't send old 'complete' or 'error' status from previous downloads\n                if status in ('downloading', 'extracting'):\n                    logger.info(f\"Sending initial progress for {model_name}: {status}\")\n                    yield f\"data: {json.dumps(initial_progress)}\\n\\n\"\n                else:\n                    logger.info(f\"Skipping initial progress for {model_name} (status: {status})\")\n            else:\n                logger.info(f\"No initial progress available for {model_name}\")\n\n            # Stream updates\n            while True:\n                try:\n                    # Wait for update with timeout\n                    progress = await asyncio.wait_for(queue.get(), timeout=1.0)\n                    logger.debug(f\"Sending progress update for {model_name}: {progress.get('status')} - {progress.get('progress', 0):.1f}%\")\n                    yield f\"data: {json.dumps(progress)}\\n\\n\"\n\n                    # Stop if complete or error\n                    if progress.get(\"status\") in (\"complete\", \"error\"):\n                        logger.info(f\"Download {progress.get('status')} for {model_name}, closing SSE connection\")\n                        break\n                except asyncio.TimeoutError:\n                    # Send heartbeat\n                    yield \": heartbeat\\n\\n\"\n                    continue\n        except (BrokenPipeError, ConnectionResetError, asyncio.CancelledError):\n            logger.debug(f\"SSE client disconnected from {model_name}\")\n        finally:\n            # Remove from listeners\n            if model_name in self._listeners:\n                self._listeners[model_name].remove(queue)\n                if not self._listeners[model_name]:\n                    del self._listeners[model_name]\n                logger.info(f\"SSE client unsubscribed from {model_name}, remaining listeners: {len(self._listeners.get(model_name, []))}\")\n    \n    def mark_complete(self, model_name: str):\n        \"\"\"Mark a model download as complete. Thread-safe.\"\"\"\n        import logging\n        logger = logging.getLogger(__name__)\n\n        with self._lock:\n            if model_name in self._progress:\n                self._progress[model_name][\"status\"] = \"complete\"\n                self._progress[model_name][\"progress\"] = 100.0\n                progress_data = self._progress[model_name].copy()\n            else:\n                logger.warning(f\"Cannot mark {model_name} as complete: not found in progress\")\n                return\n        \n        logger.info(f\"Marked {model_name} as complete\")\n        # Notify listeners (thread-safe)\n        self._notify_listeners_threadsafe(model_name, progress_data)\n    \n    def mark_error(self, model_name: str, error: str):\n        \"\"\"Mark a model download as failed. Thread-safe.\"\"\"\n        import logging\n        logger = logging.getLogger(__name__)\n\n        with self._lock:\n            if model_name in self._progress:\n                self._progress[model_name][\"status\"] = \"error\"\n                self._progress[model_name][\"error\"] = error\n                progress_data = self._progress[model_name].copy()\n            else:\n                # Create new progress entry for error\n                progress_data = {\n                    \"model_name\": model_name,\n                    \"current\": 0,\n                    \"total\": 0,\n                    \"progress\": 0,\n                    \"filename\": None,\n                    \"status\": \"error\",\n                    \"error\": error,\n                    \"timestamp\": datetime.now().isoformat(),\n                }\n                self._progress[model_name] = progress_data\n        \n        logger.error(f\"Marked {model_name} as error: {error}\")\n        # Notify listeners (thread-safe)\n        self._notify_listeners_threadsafe(model_name, progress_data)\n\n\n# Global progress manager instance\n_progress_manager: Optional[ProgressManager] = None\n\n\ndef get_progress_manager() -> ProgressManager:\n    \"\"\"Get or create the global progress manager.\"\"\"\n    global _progress_manager\n    if _progress_manager is None:\n        _progress_manager = ProgressManager()\n    return _progress_manager\n"
  },
  {
    "path": "backend/utils/tasks.py",
    "content": "\"\"\"\nTask tracking for active downloads and generations.\n\"\"\"\n\nfrom typing import Optional, Dict, List\nfrom datetime import datetime\nfrom dataclasses import dataclass, field\n\n\n@dataclass\nclass DownloadTask:\n    \"\"\"Represents an active download task.\"\"\"\n    model_name: str\n    status: str = \"downloading\"  # downloading, extracting, complete, error\n    started_at: datetime = field(default_factory=datetime.utcnow)\n    error: Optional[str] = None\n\n\n@dataclass\nclass GenerationTask:\n    \"\"\"Represents an active generation task.\"\"\"\n    task_id: str\n    profile_id: str\n    text_preview: str  # First 50 chars of text\n    started_at: datetime = field(default_factory=datetime.utcnow)\n\n\nclass TaskManager:\n    \"\"\"Manages active downloads and generations.\"\"\"\n    \n    def __init__(self):\n        self._active_downloads: Dict[str, DownloadTask] = {}\n        self._active_generations: Dict[str, GenerationTask] = {}\n    \n    def start_download(self, model_name: str) -> None:\n        \"\"\"Mark a download as started.\"\"\"\n        self._active_downloads[model_name] = DownloadTask(\n            model_name=model_name,\n            status=\"downloading\",\n        )\n    \n    def complete_download(self, model_name: str) -> None:\n        \"\"\"Mark a download as complete.\"\"\"\n        if model_name in self._active_downloads:\n            del self._active_downloads[model_name]\n    \n    def error_download(self, model_name: str, error: str) -> None:\n        \"\"\"Mark a download as failed.\"\"\"\n        if model_name in self._active_downloads:\n            self._active_downloads[model_name].status = \"error\"\n            self._active_downloads[model_name].error = error\n    \n    def start_generation(self, task_id: str, profile_id: str, text: str) -> None:\n        \"\"\"Mark a generation as started.\"\"\"\n        text_preview = text[:50] + \"...\" if len(text) > 50 else text\n        self._active_generations[task_id] = GenerationTask(\n            task_id=task_id,\n            profile_id=profile_id,\n            text_preview=text_preview,\n        )\n    \n    def complete_generation(self, task_id: str) -> None:\n        \"\"\"Mark a generation as complete.\"\"\"\n        if task_id in self._active_generations:\n            del self._active_generations[task_id]\n    \n    def get_active_downloads(self) -> List[DownloadTask]:\n        \"\"\"Get all active downloads.\"\"\"\n        return list(self._active_downloads.values())\n    \n    def get_active_generations(self) -> List[GenerationTask]:\n        \"\"\"Get all active generations.\"\"\"\n        return list(self._active_generations.values())\n    \n    def cancel_download(self, model_name: str) -> bool:\n        \"\"\"Cancel/dismiss a download task (removes it from active list).\"\"\"\n        return self._active_downloads.pop(model_name, None) is not None\n\n    def clear_all(self) -> None:\n        \"\"\"Clear all download and generation tasks.\"\"\"\n        self._active_downloads.clear()\n        self._active_generations.clear()\n\n    def is_download_active(self, model_name: str) -> bool:\n        \"\"\"Check if a download is active.\"\"\"\n        return model_name in self._active_downloads\n    \n    def is_generation_active(self, task_id: str) -> bool:\n        \"\"\"Check if a generation is active.\"\"\"\n        return task_id in self._active_generations\n\n\n# Global task manager instance\n_task_manager: Optional[TaskManager] = None\n\n\ndef get_task_manager() -> TaskManager:\n    \"\"\"Get or create the global task manager.\"\"\"\n    global _task_manager\n    if _task_manager is None:\n        _task_manager = TaskManager()\n    return _task_manager\n"
  },
  {
    "path": "biome.json",
    "content": "{\n  \"$schema\": \"https://biomejs.dev/schemas/2.3.12/schema.json\",\n  \"vcs\": {\n    \"enabled\": true,\n    \"clientKind\": \"git\",\n    \"useIgnoreFile\": true\n  },\n  \"formatter\": {\n    \"enabled\": true,\n    \"indentStyle\": \"space\",\n    \"indentWidth\": 2,\n    \"lineWidth\": 100\n  },\n  \"linter\": {\n    \"enabled\": true,\n    \"rules\": {\n      \"recommended\": true,\n      \"a11y\": {\n        \"useButtonType\": \"warn\"\n      },\n      \"correctness\": {\n        \"noUnusedVariables\": \"warn\",\n        \"noUnusedImports\": \"error\",\n        \"useExhaustiveDependencies\": \"warn\",\n        \"useHookAtTopLevel\": \"error\"\n      },\n      \"suspicious\": {\n        \"noDoubleEquals\": \"error\",\n        \"noExplicitAny\": \"warn\",\n        \"noUnknownAtRules\": \"off\"\n      },\n      \"style\": {\n        \"useFilenamingConvention\": \"off\",\n        \"noNonNullAssertion\": \"off\"\n      }\n    }\n  },\n  \"javascript\": {\n    \"formatter\": {\n      \"quoteStyle\": \"single\",\n      \"jsxQuoteStyle\": \"double\",\n      \"trailingCommas\": \"all\",\n      \"semicolons\": \"always\",\n      \"arrowParentheses\": \"always\"\n    }\n  },\n  \"json\": {\n    \"formatter\": {\n      \"trailingCommas\": \"none\"\n    }\n  },\n  \"css\": {\n    \"parser\": {\n      \"cssModules\": false,\n      \"allowWrongLineComments\": false,\n      \"tailwindDirectives\": true\n    },\n    \"linter\": {\n      \"enabled\": true\n    }\n  }\n}\n"
  },
  {
    "path": "data/.gitkeep",
    "content": "# User data directory\n# This directory contains:\n# - profiles/ - Voice profile audio files\n# - generations/ - Generated audio files\n# - projects/ - Audio studio project files\n# - voicebox.db - SQLite database\n# - cache/ - Voice prompt cache files\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  voicebox:\n    build: .\n    container_name: voicebox\n    restart: unless-stopped\n\n    ports:\n      # Bind to localhost only for security\n      - \"127.0.0.1:17493:17493\"\n\n    volumes:\n      # Bind-mount for generated audio (customize the host path as needed)\n      # Host side:      ./output/\n      # Container side: /app/data/generations/\n      - ./output:/app/data/generations\n\n      # Named volume for profiles, DB, cache (persists across container restarts)\n      - voicebox-data:/app/data\n\n      # HuggingFace model cache (so models aren't re-downloaded on rebuild)\n      - huggingface-cache:/home/voicebox/.cache/huggingface\n\n    environment:\n      - LOG_LEVEL=info\n\n    networks:\n      - voicebox-net\n\n    deploy:\n      resources:\n        limits:\n          cpus: '4'\n          memory: 8G\n\nnetworks:\n  voicebox-net:\n    driver: bridge\n\nvolumes:\n  voicebox-data:\n  huggingface-cache:\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# deps\n/node_modules\n\n# generated content\n.source\n\n# test & build\n/coverage\n/.next/\n/out/\n/build\n*.tsbuildinfo\n\n# misc\n.DS_Store\n*.pem\n/.pnp\n.pnp.js\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# others\n.env*.local\n.vercel\nnext-env.d.ts"
  },
  {
    "path": "docs/README.md",
    "content": "# fumadocs-ui-template\n\nThis is a Next.js application generated with\n[Create Fumadocs](https://github.com/fuma-nama/fumadocs).\n\nRun development server:\n\n```bash\nbun run dev\n```\n\nOpen http://localhost:3000 with your browser to see the result.\n\n## Explore\n\nIn the project, you can see:\n\n- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.\n- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.\n\n| Route                     | Description                                            |\n| ------------------------- | ------------------------------------------------------ |\n| `app/(home)`              | The route group for your landing page and other pages. |\n| `app/docs`                | The documentation layout and pages.                    |\n| `app/api/search/route.ts` | The Route Handler for search.                          |\n\n### Fumadocs MDX\n\nA `source.config.ts` config file has been included, you can customise different options like frontmatter schema.\n\nRead the [Introduction](https://fumadocs.dev/docs/mdx) for further details.\n\n## Learn More\n\nTo learn more about Next.js and Fumadocs, take a look at the following\nresources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js\n  features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs\n"
  },
  {
    "path": "docs/app/[[...slug]]/layout.tsx",
    "content": "import { DocsLayout } from 'fumadocs-ui/layouts/docs';\nimport { baseOptions } from '@/lib/layout.shared';\nimport { source } from '@/lib/source';\n\nexport default function Layout({ children }: LayoutProps<'/[[...slug]]'>) {\n  return (\n    <DocsLayout tree={source.pageTree} {...baseOptions()}>\n      {children}\n    </DocsLayout>\n  );\n}\n"
  },
  {
    "path": "docs/app/[[...slug]]/page.tsx",
    "content": "import { createRelativeLink } from 'fumadocs-ui/mdx';\nimport { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';\nimport type { Metadata } from 'next';\nimport { notFound } from 'next/navigation';\nimport { MarkdownCopyButton, ViewOptionsPopover } from '@/components/ai/page-actions';\nimport { APIPage } from '@/components/api-page';\nimport { getPageImage, source } from '@/lib/source';\nimport { getMDXComponents } from '@/mdx-components';\n\nexport default async function Page(props: PageProps<'/[[...slug]]'>) {\n  const params = await props.params;\n  const page = source.getPage(params.slug);\n  if (!page) notFound();\n\n  const MDX = page.data.body;\n  const markdownUrl = `${page.url}.mdx`;\n  const githubUrl = `https://github.com/jamiepine/voicebox/blob/main/docs/content/docs/${page.path}`;\n\n  return (\n    <DocsPage\n      toc={page.data.toc}\n      full={page.data.full}\n      editOnGithub={{\n        owner: 'jamiepine',\n        repo: 'voicebox',\n        sha: 'main',\n        path: `docs/content/docs/${page.path}`,\n      }}\n      lastUpdate={page.data.lastModified}\n    >\n      <DocsTitle>{page.data.title}</DocsTitle>\n      <DocsDescription className=\"mb-0\">{page.data.description}</DocsDescription>\n      <div className=\"flex flex-row gap-2 items-center\">\n        <MarkdownCopyButton markdownUrl={markdownUrl} />\n        <ViewOptionsPopover markdownUrl={markdownUrl} githubUrl={githubUrl} />\n      </div>\n      <div\n        role=\"separator\"\n        style={{\n          height: '1px',\n          background: 'currentColor',\n          opacity: 0.15,\n          marginTop: '8px',\n          marginBottom: '24px',\n        }}\n      />\n      <DocsBody>\n        <MDX\n          components={getMDXComponents({\n            a: createRelativeLink(source, page),\n          })}\n        />\n      </DocsBody>\n    </DocsPage>\n  );\n}\n\nexport async function generateStaticParams() {\n  return source.generateParams();\n}\n\nexport async function generateMetadata(props: PageProps<'/[[...slug]]'>): Promise<Metadata> {\n  const params = await props.params;\n  const page = source.getPage(params.slug);\n  if (!page) notFound();\n\n  return {\n    title: page.data.title,\n    description: page.data.description,\n    openGraph: {\n      images: getPageImage(page).url,\n    },\n  };\n}\n"
  },
  {
    "path": "docs/app/api/search/route.ts",
    "content": "import { source } from '@/lib/source';\nimport { createFromSource } from 'fumadocs-core/search/server';\n\nexport const { GET } = createFromSource(source, {\n  // https://docs.orama.com/docs/orama-js/supported-languages\n  language: 'english',\n});\n"
  },
  {
    "path": "docs/app/global.css",
    "content": "@import 'tailwindcss';\n@import 'fumadocs-ui/css/neutral.css';\n@import 'fumadocs-ui/css/preset.css';\n@import 'fumadocs-openapi/css/preset.css';\n\n:root {\n  --color-fd-primary: hsl(43, 50%, 50%);\n  --color-fd-primary-foreground: hsl(222.2, 47.4%, 11.2%);\n}\n\n.dark {\n  --color-fd-primary: hsl(43, 50%, 45%);\n  --color-fd-primary-foreground: hsl(0, 0%, 95%);\n}\n"
  },
  {
    "path": "docs/app/layout.tsx",
    "content": "import { RootProvider } from 'fumadocs-ui/provider/next';\nimport './global.css';\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n});\n\nexport default function Layout({ children }: LayoutProps<'/'>) {\n  return (\n    <html lang=\"en\" className={inter.className} suppressHydrationWarning>\n      <body className=\"flex flex-col min-h-screen\">\n        <RootProvider>{children}</RootProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "docs/app/llms-full.txt/route.ts",
    "content": "import { getLLMText, source } from '@/lib/source';\n\nexport const revalidate = false;\n\nexport async function GET() {\n  const scan = source.getPages().map(getLLMText);\n  const scanned = await Promise.all(scan);\n\n  return new Response(scanned.join('\\n\\n'));\n}\n"
  },
  {
    "path": "docs/app/llms.mdx/docs/[[...slug]]/route.ts",
    "content": "import { notFound } from 'next/navigation';\nimport { getLLMText, source } from '@/lib/source';\n\nexport const revalidate = false;\n\nexport async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {\n  const { slug } = await params;\n  const page = source.getPage(slug);\n  if (!page) notFound();\n\n  return new Response(await getLLMText(page), {\n    headers: {\n      'Content-Type': 'text/markdown',\n    },\n  });\n}\n\nexport function generateStaticParams() {\n  return source.generateParams();\n}\n"
  },
  {
    "path": "docs/app/og/docs/[...slug]/route.tsx",
    "content": "import { getPageImage, source } from '@/lib/source';\nimport { notFound } from 'next/navigation';\nimport { ImageResponse } from 'next/og';\nimport { generate as DefaultImage } from 'fumadocs-ui/og';\n\nexport const revalidate = false;\n\nexport async function GET(\n  _req: Request,\n  { params }: RouteContext<'/og/docs/[...slug]'>,\n) {\n  const { slug } = await params;\n  const page = source.getPage(slug.slice(0, -1));\n  if (!page) notFound();\n\n  return new ImageResponse(\n    (\n      <DefaultImage\n        title={page.data.title}\n        description={page.data.description}\n        site=\"My App\"\n      />\n    ),\n    {\n      width: 1200,\n      height: 630,\n    },\n  );\n}\n\nexport function generateStaticParams() {\n  return source.getPages().map((page) => ({\n    lang: page.locale,\n    slug: getPageImage(page).segments,\n  }));\n}\n"
  },
  {
    "path": "docs/cli.json",
    "content": "{\n  \"$schema\": \"node_modules/@fumadocs/cli/dist/schema/default.json\",\n  \"aliases\": {\n    \"uiDir\": \"./components/ui\",\n    \"componentsDir\": \"./components\",\n    \"blockDir\": \"./components\",\n    \"cssDir\": \"./styles\",\n    \"libDir\": \"./lib\"\n  },\n  \"baseDir\": \"\",\n  \"uiLibrary\": \"radix-ui\",\n  \"commands\": {}\n}"
  },
  {
    "path": "docs/components/ai/page-actions.tsx",
    "content": "'use client';\nimport { type ComponentProps, useMemo, useState } from 'react';\nimport { Check, ChevronDown, Copy, ExternalLinkIcon, TextIcon } from 'lucide-react';\nimport { cn } from '../../lib/cn';\nimport { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';\nimport { Popover, PopoverTrigger, PopoverContent } from '../ui/popover';\nimport { buttonVariants } from '../ui/button';\n\nconst cache = new Map<string, Promise<string>>();\n\nexport function MarkdownCopyButton({\n  markdownUrl,\n  ...props\n}: ComponentProps<'button'> & {\n  /**\n   * A URL to fetch the raw Markdown/MDX content of page\n   */\n  markdownUrl: string;\n}) {\n  const [isLoading, setLoading] = useState(false);\n  const [checked, onClick] = useCopyButton(async () => {\n    const cached = cache.get(markdownUrl);\n    if (cached) return navigator.clipboard.writeText(await cached);\n\n    setLoading(true);\n\n    try {\n      const promise = fetch(markdownUrl).then((res) => res.text());\n      cache.set(markdownUrl, promise);\n      await navigator.clipboard.write([\n        new ClipboardItem({\n          'text/plain': promise,\n        }),\n      ]);\n    } finally {\n      setLoading(false);\n    }\n  });\n\n  return (\n    <button\n      disabled={isLoading}\n      onClick={onClick}\n      {...props}\n      className={cn(\n        buttonVariants({\n          variant: 'secondary',\n          size: 'sm',\n          className: 'gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground',\n        }),\n        props.className,\n      )}\n    >\n      {checked ? <Check /> : <Copy />}\n      Copy Markdown\n    </button>\n  );\n}\n\nexport function ViewOptionsPopover({\n  markdownUrl,\n  githubUrl,\n  ...props\n}: ComponentProps<typeof PopoverTrigger> & {\n  /**\n   * A URL to the raw Markdown/MDX content of page\n   */\n  markdownUrl: string;\n\n  /**\n   * Source file URL on GitHub\n   */\n  githubUrl: string;\n}) {\n  const items = useMemo(() => {\n    const pageUrl = typeof window !== 'undefined' ? window.location.href : 'loading';\n    const q = `Read ${pageUrl}, I want to ask questions about it.`;\n\n    return [\n      {\n        title: 'Open in GitHub',\n        href: githubUrl,\n        icon: (\n          <svg fill=\"currentColor\" role=\"img\" viewBox=\"0 0 24 24\">\n            <title>GitHub</title>\n            <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" />\n          </svg>\n        ),\n      },\n      {\n        title: 'View as Markdown',\n        href: markdownUrl,\n        icon: <TextIcon />,\n      },\n      {\n        title: 'Open in Scira AI',\n        href: `https://scira.ai/?${new URLSearchParams({\n          q,\n        })}`,\n        icon: (\n          <svg\n            width=\"910\"\n            height=\"934\"\n            viewBox=\"0 0 910 934\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <title>Scira AI</title>\n            <path\n              d=\"M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z\"\n              fill=\"currentColor\"\n              stroke=\"currentColor\"\n              strokeWidth=\"8\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z\"\n              fill=\"currentColor\"\n              stroke=\"currentColor\"\n              strokeWidth=\"8\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z\"\n              stroke=\"currentColor\"\n              strokeWidth=\"20\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z\"\n              stroke=\"currentColor\"\n              strokeWidth=\"20\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z\"\n              fill=\"currentColor\"\n              stroke=\"currentColor\"\n              strokeWidth=\"8\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z\"\n              fill=\"currentColor\"\n              stroke=\"currentColor\"\n              strokeWidth=\"8\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442\"\n              stroke=\"currentColor\"\n              strokeWidth=\"30\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            />\n          </svg>\n        ),\n      },\n      {\n        title: 'Open in ChatGPT',\n        href: `https://chatgpt.com/?${new URLSearchParams({\n          hints: 'search',\n          q,\n        })}`,\n        icon: (\n          <svg\n            role=\"img\"\n            viewBox=\"0 0 24 24\"\n            fill=\"currentColor\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <title>OpenAI</title>\n            <path d=\"M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z\" />\n          </svg>\n        ),\n      },\n      {\n        title: 'Open in Claude',\n        href: `https://claude.ai/new?${new URLSearchParams({\n          q,\n        })}`,\n        icon: (\n          <svg\n            fill=\"currentColor\"\n            role=\"img\"\n            viewBox=\"0 0 24 24\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <title>Anthropic</title>\n            <path d=\"M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z\" />\n          </svg>\n        ),\n      },\n      {\n        title: 'Open in Cursor',\n        icon: (\n          <svg\n            fill=\"currentColor\"\n            role=\"img\"\n            viewBox=\"0 0 24 24\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <title>Cursor</title>\n            <path d=\"M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23\" />\n          </svg>\n        ),\n        href: `https://cursor.com/link/prompt?${new URLSearchParams({\n          text: q,\n        })}`,\n      },\n    ];\n  }, [githubUrl, markdownUrl]);\n\n  return (\n    <Popover>\n      <PopoverTrigger\n        {...props}\n        className={cn(\n          buttonVariants({\n            variant: 'secondary',\n            size: 'sm',\n          }),\n          'gap-2 data-[state=open]:bg-fd-accent data-[state=open]:text-fd-accent-foreground',\n          props.className,\n        )}\n      >\n        Open\n        <ChevronDown className=\"size-3.5 text-fd-muted-foreground\" />\n      </PopoverTrigger>\n      <PopoverContent className=\"flex flex-col\">\n        {items.map((item) => (\n          <a\n            key={item.href}\n            href={item.href}\n            rel=\"noreferrer noopener\"\n            target=\"_blank\"\n            className=\"text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4\"\n          >\n            {item.icon}\n            {item.title}\n            <ExternalLinkIcon className=\"text-fd-muted-foreground size-3.5 ms-auto\" />\n          </a>\n        ))}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "docs/components/api-page.client.tsx",
    "content": "'use client';\nimport { defineClientConfig } from 'fumadocs-openapi/ui/client';\n\nexport default defineClientConfig({\n  // Client-side configuration for API playground\n});\n"
  },
  {
    "path": "docs/components/api-page.tsx",
    "content": "import { openapi } from '@/lib/openapi';\nimport { createAPIPage } from 'fumadocs-openapi/ui';\nimport client from './api-page.client';\n\nexport const APIPage = createAPIPage(openapi, {\n  client,\n});\n"
  },
  {
    "path": "docs/components/ui/button.tsx",
    "content": "import { cva, type VariantProps } from 'class-variance-authority';\n\nconst variants = {\n  primary:\n    'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80 disabled:bg-fd-secondary disabled:text-fd-secondary-foreground',\n  outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground',\n  ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground',\n  secondary:\n    'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',\n} as const;\n\nexport const buttonVariants = cva(\n  'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring',\n  {\n    variants: {\n      variant: variants,\n      // fumadocs use `color` instead of `variant`\n      color: variants,\n      size: {\n        sm: 'gap-1 px-2 py-1.5 text-xs',\n        icon: 'p-1.5 [&_svg]:size-5',\n        'icon-sm': 'p-1.5 [&_svg]:size-4.5',\n        'icon-xs': 'p-1 [&_svg]:size-4',\n      },\n    },\n  },\n);\n\nexport type ButtonProps = VariantProps<typeof buttonVariants>;\n"
  },
  {
    "path": "docs/components/ui/popover.tsx",
    "content": "'use client';\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport * as React from 'react';\nimport { cn } from '../../lib/cn';\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ComponentRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      side=\"bottom\"\n      className={cn(\n        'z-50 origin-(--radix-popover-content-transform-origin) overflow-y-auto max-h-(--radix-popover-content-available-height) min-w-[240px] max-w-[98vw] rounded-xl border bg-fd-popover/60 backdrop-blur-lg p-2 text-sm text-fd-popover-foreground shadow-lg focus-visible:outline-none data-[state=closed]:animate-fd-popover-out data-[state=open]:animate-fd-popover-in',\n        className,\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nconst PopoverClose = PopoverPrimitive.PopoverClose;\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverClose };\n"
  },
  {
    "path": "docs/content/docs/README.md",
    "content": "---\ntitle: \"Documentation README\"\ndescription: \"Voicebox documentation development guide\"\n---\n\nThis directory contains the documentation for Voicebox, built with [Fumadocs](https://fumadocs.dev).\n\n## Development\n\n### Prerequisites\n\nInstall Mintlify globally using bun:\n\n```bash\nbun add -g mintlify\n```\n\nOr use the helper script:\n\n```bash\nbun run install:mintlify\n```\n\n### Running Locally\n\n```bash\nbun run dev\n```\n\nThis will start the Mintlify dev server.\n\nThe docs will be available at `http://localhost:3000`\n\n### Structure\n\n```\ndocs/\n├── mint.json           # Mintlify configuration\n├── custom.css          # Custom styles\n├── overview/           # Getting started & feature docs\n├── guides/             # User guides\n├── api/                # API reference\n├── development/        # Developer documentation\n├── logo/               # Logo assets\n└── public/             # Static assets\n```\n\n### Writing Docs\n\n- Use `.mdx` files for all documentation pages\n- Follow the existing structure in `mint.json` for navigation\n- Use Mintlify components for enhanced formatting (Card, CardGroup, Accordion, etc.)\n- Reference the [Mintlify documentation](https://mintlify.com/docs) for available components\n\n## Deployment\n\nDocs are automatically deployed when changes are pushed to the main branch.\n\nTo manually deploy:\n\n```bash\nmintlify deploy\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](../CONTRIBUTING.md) for contribution guidelines.\n"
  },
  {
    "path": "docs/content/docs/TROUBLESHOOTING.md",
    "content": "---\ntitle: \"Troubleshooting Guide\"\ndescription: \"Common issues and solutions for Voicebox\"\n---\n\nCommon issues and solutions for Voicebox.\n\n## Installation Issues\n\n### macOS: \"Voicebox cannot be opened because it is from an unidentified developer\"\n\n**Solution:**\n1. Right-click the `.dmg` file\n2. Select \"Open\"\n3. Click \"Open\" in the security dialog\n4. Alternatively, go to System Settings → Privacy & Security → Allow Voicebox\n\n### Windows: \"Windows protected your PC\"\n\n**Solution:**\n1. Click \"More info\"\n2. Click \"Run anyway\"\n3. Windows Defender may flag new software; this is normal for unsigned apps\n\n### Linux: AppImage won't run\n\n**Solution:**\n```bash\nchmod +x voicebox-*.AppImage\n./voicebox-*.AppImage\n```\n\n## Runtime Issues\n\n### Server won't start\n\n**Symptoms:** App opens but shows \"Server not connected\"\n\n**Solutions:**\n1. **Check Python installation**\n   ```bash\n   python --version  # Should be 3.11+\n   ```\n\n2. **Check server binary exists**\n   - Look in `tauri/src-tauri/binaries/` for your platform\n   - Binary should match your system architecture\n\n3. **Check permissions**\n   ```bash\n   # macOS/Linux\n   chmod +x tauri/src-tauri/binaries/voicebox-server-*\n   ```\n\n4. **Check logs**\n   - macOS: Open Console.app and search for \"voicebox\"\n   - Linux: Check `~/.local/share/voicebox/` for logs\n   - Windows: Check Event Viewer\n\n### \"Model download failed\"\n\n**Symptoms:** First generation fails with download error\n\n**Solutions:**\n1. **Check internet connection**\n   - Models download from HuggingFace Hub (~2-4GB)\n   - First download may take several minutes\n\n2. **Check disk space**\n   - Models are cached in `~/.cache/huggingface/`\n   - Ensure at least 5GB free space\n\n3. **Manual download** (if automatic fails)\n   ```bash\n   pip install huggingface_hub\n   huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-Base\n   ```\n\n### \"Out of memory\" errors\n\n**Symptoms:** Generation fails with CUDA/VRAM errors\n\n**Solutions:**\n1. **Use smaller model**\n   - Switch to 0.6B model instead of 1.7B\n   - Settings → Model Management → Load 0.6B\n\n2. **Close other applications**\n   - Free up GPU memory\n   - Close browser tabs, other ML apps\n\n3. **Use CPU mode**\n   - Slower but works without GPU\n   - Backend automatically falls back to CPU\n\n### MLX \"Failed to load the default metallib\" error (Apple Silicon)\n\n**Symptoms:** Generation fails with \"library not found\" or \"metallib\" errors\n\n**Solutions:**\n1. **Rebuild server binary**\n   ```bash\n   bun run build:server\n   ```\n   The build script should automatically include MLX Metal shader libraries.\n\n2. **Check MLX installation**\n   ```bash\n   pip install -r backend/requirements-mlx.txt\n   ```\n\n3. **Verify backend detection**\n   - Check server logs for \"Backend: MLX\"\n   - If showing \"Backend: PYTORCH\", MLX may not be installed correctly\n\n### Audio playback issues\n\n**Symptoms:** Generated audio won't play\n\n**Solutions:**\n1. **Check audio format**\n   - Audio is saved as WAV files\n   - Ensure your system supports WAV playback\n\n2. **Try downloading audio**\n   - Right-click → Download\n   - Play in external player\n\n3. **Check browser permissions** (web version)\n   - Allow audio autoplay in browser settings\n\n### Slow generation\n\n**Symptoms:** Generation takes >30 seconds\n\n**Solutions:**\n1. **Check backend type** (Apple Silicon)\n   - Check Settings → Server Status\n   - Should show \"Backend: MLX\" on Apple Silicon\n   - If showing \"Backend: PYTORCH\", install MLX: `pip install -r backend/requirements-mlx.txt`\n   - MLX provides 4-5x faster inference on Apple Silicon\n\n2. **Use GPU** (if available)\n   - Check Settings → Server Status\n   - Should show \"GPU available: true\"\n   - Apple Silicon: Should show \"Metal (Apple Silicon via MLX)\"\n   - Windows/Linux: Should show \"CUDA\" if GPU available\n\n3. **Enable caching**\n   - Voice prompts are cached automatically\n   - Second generation with same voice should be faster\n\n4. **Use smaller model**\n   - 0.6B model is faster than 1.7B\n   - Quality difference is minimal for most voices\n\n5. **Check system resources**\n   - Close other CPU/GPU intensive apps\n   - Ensure adequate RAM (8GB+ recommended)\n\n## API Issues\n\n### \"Connection refused\" when using API\n\n**Solutions:**\n1. **Check server is running**\n   ```bash\n   curl http://localhost:17493/health\n   ```\n\n2. **Check remote mode**\n   - If connecting remotely, ensure server is started with `--host 0.0.0.0`\n   - Check firewall settings\n\n3. **Check port availability**\n   - The current local app and dev workflow uses port 17493 by default\n   - Ensure no other service is using it\n\n### CORS errors in browser\n\n**Solutions:**\n1. **Use desktop app** (recommended)\n   - Desktop app doesn't have CORS restrictions\n\n2. **Configure CORS** (for web deployment)\n   - Update `backend/main.py` CORS settings\n   - Add your domain to allowed origins\n\n## Update Issues\n\n### \"Update check failed\"\n\n**Solutions:**\n1. **Check internet connection**\n   - Updates are fetched from GitHub releases\n\n2. **Check GitHub access**\n   - Ensure `github.com` is accessible\n   - Check firewall/proxy settings\n\n3. **Manual update**\n   - Download latest release from GitHub\n   - Install manually\n\n### \"Invalid signature\" error\n\n**Solutions:**\n1. **Re-download installer**\n   - Signature may be corrupted\n   - Download fresh copy from GitHub\n\n2. **Check release integrity**\n   - Verify `.sig` file matches installer\n   - Report issue if signature is invalid\n\n## Data Issues\n\n### Profiles disappeared\n\n**Solutions:**\n1. **Check data directory**\n   - macOS: `~/Library/Application Support/voicebox/`\n   - Windows: `%APPDATA%/voicebox/`\n   - Linux: `~/.local/share/voicebox/`\n\n2. **Check database**\n   - Database: `data/voicebox.db`\n   - Ensure file exists and is readable\n\n3. **Restore from backup**\n   - Profiles can be exported/imported\n   - Check for backup files\n\n### \"Database locked\" error\n\n**Solutions:**\n1. **Close other instances**\n   - Ensure only one Voicebox instance is running\n\n2. **Restart app**\n   - Close and reopen Voicebox\n\n3. **Check file permissions**\n   - Ensure database file is writable\n   - Check directory permissions\n\n## Development Issues\n\n### Build fails\n\n**Solutions:**\n1. **Check Rust installation**\n   ```bash\n   rustc --version\n   rustup update\n   ```\n\n2. **Check Tauri dependencies**\n   ```bash\n   cd tauri\n   bun install\n   ```\n\n3. **Clean build**\n   ```bash\n   cd tauri/src-tauri\n   cargo clean\n   cd ../..\n   bun run build\n   ```\n\n### API client generation fails\n\n**Solutions:**\n1. **Start backend server**\n   ```bash\n   bun run dev:server\n   ```\n\n2. **Check OpenAPI endpoint**\n   ```bash\n   curl http://localhost:17493/openapi.json\n   ```\n\n3. **Regenerate client**\n   ```bash\n   bun run generate:api\n   ```\n\n## Still Having Issues?\n\n1. **Check existing issues**\n   - Search GitHub issues for similar problems\n   - Check closed issues for solutions\n\n2. **Create new issue**\n   - Include:\n     - OS and version\n     - Voicebox version\n     - Steps to reproduce\n     - Error messages/logs\n     - Screenshots (if applicable)\n\n3. **Get help**\n   - Check documentation in `docs/`\n   - Review `backend/README.md` for API details\n   - See `CONTRIBUTING.md` for development help\n\n---\n\nFor more help, open an issue on [GitHub](https://github.com/jamiepine/voicebox/issues).\n"
  },
  {
    "path": "docs/content/docs/api-reference/general/health_health_get.mdx",
    "content": "---\ntitle: Health\ndescription: Health check endpoint.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Health check endpoint.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/health\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/general/meta.json",
    "content": "{\n  \"title\": \"General\",\n  \"pages\": [\"root__get\", \"health_health_get\"]\n}\n"
  },
  {
    "path": "docs/content/docs/api-reference/general/root__get.mdx",
    "content": "---\ntitle: Root\ndescription: Root endpoint.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Root endpoint.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/generation/generate_speech_generate_post.mdx",
    "content": "---\ntitle: Generate Speech\ndescription: Generate speech from text using a voice profile.\nfull: true\n_openapi:\n  method: POST\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Generate speech from text using a voice profile.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/generate\",\"method\":\"post\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/generation/get_audio_audio__generation_id__get.mdx",
    "content": "---\ntitle: Get Audio\ndescription: Serve generated audio file.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Serve generated audio file.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/audio/{generation_id}\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/generation/meta.json",
    "content": "{\n  \"title\": \"Generation\",\n  \"pages\": [\n    \"generate_speech_generate_post\",\n    \"transcribe_audio_transcribe_post\",\n    \"get_audio_audio__generation_id__get\"\n  ]\n}\n"
  },
  {
    "path": "docs/content/docs/api-reference/generation/transcribe_audio_transcribe_post.mdx",
    "content": "---\ntitle: Transcribe Audio\ndescription: Transcribe audio file to text.\nfull: true\n_openapi:\n  method: POST\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Transcribe audio file to text.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/transcribe\",\"method\":\"post\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/history/delete_generation_history__generation_id__delete.mdx",
    "content": "---\ntitle: Delete Generation\ndescription: Delete a generation.\nfull: true\n_openapi:\n  method: DELETE\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Delete a generation.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/history/{generation_id}\",\"method\":\"delete\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/history/get_generation_history__generation_id__get.mdx",
    "content": "---\ntitle: Get Generation\ndescription: Get a generation by ID.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Get a generation by ID.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/history/{generation_id}\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/history/get_stats_history_stats_get.mdx",
    "content": "---\ntitle: Get Stats\ndescription: Get generation statistics.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Get generation statistics.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/history/stats\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/history/list_history_history_get.mdx",
    "content": "---\ntitle: List History\ndescription: List generation history with optional filters.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: List generation history with optional filters.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/history\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/history/meta.json",
    "content": "{\n  \"title\": \"History\",\n  \"pages\": [\n    \"list_history_history_get\",\n    \"get_generation_history__generation_id__get\",\n    \"delete_generation_history__generation_id__delete\",\n    \"get_stats_history_stats_get\"\n  ]\n}\n"
  },
  {
    "path": "docs/content/docs/api-reference/meta.json",
    "content": "{\n  \"title\": \"API Reference\",\n  \"defaultOpen\": true,\n  \"pages\": [\"general\", \"profiles\", \"generation\", \"history\", \"models\"]\n}\n"
  },
  {
    "path": "docs/content/docs/api-reference/models/get_model_progress_models_progress__model_name__get.mdx",
    "content": "---\ntitle: Get Model Progress\ndescription: Get model download progress via Server-Sent Events.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Get model download progress via Server-Sent Events.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/models/progress/{model_name}\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/models/get_model_status_models_status_get.mdx",
    "content": "---\ntitle: Get Model Status\ndescription: Get status of all available models.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Get status of all available models.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/models/status\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/models/load_model_models_load_post.mdx",
    "content": "---\ntitle: Load Model\ndescription: Manually load TTS model.\nfull: true\n_openapi:\n  method: POST\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Manually load TTS model.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/models/load\",\"method\":\"post\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/models/meta.json",
    "content": "{\n  \"title\": \"Models\",\n  \"pages\": [\n    \"get_model_status_models_status_get\",\n    \"load_model_models_load_post\",\n    \"unload_model_models_unload_post\",\n    \"trigger_model_download_models_download_post\",\n    \"get_model_progress_models_progress__model_name__get\"\n  ]\n}\n"
  },
  {
    "path": "docs/content/docs/api-reference/models/trigger_model_download_models_download_post.mdx",
    "content": "---\ntitle: Trigger Model Download\ndescription: Trigger download of a specific model.\nfull: true\n_openapi:\n  method: POST\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Trigger download of a specific model.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/models/download\",\"method\":\"post\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/models/unload_model_models_unload_post.mdx",
    "content": "---\ntitle: Unload Model\ndescription: Unload TTS model to free memory.\nfull: true\n_openapi:\n  method: POST\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Unload TTS model to free memory.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/models/unload\",\"method\":\"post\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/add_profile_sample_profiles__profile_id__samples_post.mdx",
    "content": "---\ntitle: Add Profile Sample\ndescription: Add a sample to a voice profile.\nfull: true\n_openapi:\n  method: POST\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Add a sample to a voice profile.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/profiles/{profile_id}/samples\",\"method\":\"post\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/create_profile_profiles_post.mdx",
    "content": "---\ntitle: Create Profile\ndescription: Create a new voice profile.\nfull: true\n_openapi:\n  method: POST\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Create a new voice profile.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/profiles\",\"method\":\"post\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/delete_profile_profiles__profile_id__delete.mdx",
    "content": "---\ntitle: Delete Profile\ndescription: Delete a voice profile.\nfull: true\n_openapi:\n  method: DELETE\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Delete a voice profile.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/profiles/{profile_id}\",\"method\":\"delete\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/delete_profile_sample_profiles_samples__sample_id__delete.mdx",
    "content": "---\ntitle: Delete Profile Sample\ndescription: Delete a profile sample.\nfull: true\n_openapi:\n  method: DELETE\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Delete a profile sample.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/profiles/samples/{sample_id}\",\"method\":\"delete\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/get_profile_profiles__profile_id__get.mdx",
    "content": "---\ntitle: Get Profile\ndescription: Get a voice profile by ID.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Get a voice profile by ID.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/profiles/{profile_id}\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/get_profile_samples_profiles__profile_id__samples_get.mdx",
    "content": "---\ntitle: Get Profile Samples\ndescription: Get all samples for a profile.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Get all samples for a profile.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/profiles/{profile_id}/samples\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/list_profiles_profiles_get.mdx",
    "content": "---\ntitle: List Profiles\ndescription: List all voice profiles.\nfull: true\n_openapi:\n  method: GET\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: List all voice profiles.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/profiles\",\"method\":\"get\"}]} />"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/meta.json",
    "content": "{\n  \"title\": \"Profiles\",\n  \"pages\": [\n    \"list_profiles_profiles_get\",\n    \"create_profile_profiles_post\",\n    \"get_profile_profiles__profile_id__get\",\n    \"update_profile_profiles__profile_id__put\",\n    \"delete_profile_profiles__profile_id__delete\",\n    \"get_profile_samples_profiles__profile_id__samples_get\",\n    \"add_profile_sample_profiles__profile_id__samples_post\",\n    \"delete_profile_sample_profiles_samples__sample_id__delete\"\n  ]\n}\n"
  },
  {
    "path": "docs/content/docs/api-reference/profiles/update_profile_profiles__profile_id__put.mdx",
    "content": "---\ntitle: Update Profile\ndescription: Update a voice profile.\nfull: true\n_openapi:\n  method: PUT\n  toc: []\n  structuredData:\n    headings: []\n    contents:\n      - content: Update a voice profile.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./openapi.json\"} operations={[{\"path\":\"/profiles/{profile_id}\",\"method\":\"put\"}]} />"
  },
  {
    "path": "docs/content/docs/developer/architecture.mdx",
    "content": "---\ntitle: \"Architecture\"\ndescription: \"Understanding Voicebox's technical architecture\"\n---\n\n## System Overview\n\nVoicebox uses a client-server architecture with a React frontend and Python backend. The desktop app is built with Tauri and contains two main layers:\n\n**Frontend Layer:** A React application that handles the UI components, state management with Zustand, and data fetching with React Query (TanStack Query).\n\n**Backend Layer:** A Python FastAPI server that provides the REST API, runs the TTS engine (Qwen3-TTS), manages the SQLite database, and handles audio processing.\n\nThese two layers communicate via HTTP, with the frontend making API requests to the backend.\n\n## Frontend Architecture\n\n### Tech Stack\n\n- **Framework**: React 18 with TypeScript\n- **State Management**: Zustand stores\n- **Data Fetching**: React Query (TanStack Query)\n- **Styling**: Tailwind CSS\n- **Audio**: WaveSurfer.js\n- **Desktop**: Tauri (Rust)\n\n### Component Structure\n\n<Files>\n  <Folder name=\"app/src\" defaultOpen>\n    <Folder name=\"components\">\n      <File name=\"profiles/\" />\n      <File name=\"generation/\" />\n      <File name=\"stories/\" />\n      <File name=\"shared/\" />\n    </Folder>\n    <Folder name=\"lib\">\n      <File name=\"api/\" />\n      <File name=\"utils/\" />\n    </Folder>\n    <Folder name=\"hooks\" />\n    <Folder name=\"stores\" />\n  </Folder>\n</Files>\n\n### State Management\n\n```typescript\n// Example: Profile store\nconst useProfileStore = create((set) => ({\n  profiles: [],\n  selectedProfile: null,\n  setProfiles: (profiles) => set({ profiles }),\n  selectProfile: (id) => set({ selectedProfile: id })\n}))\n```\n\n## Backend Architecture\n\n### Tech Stack\n\n- **Framework**: FastAPI (Python 3.11+)\n- **TTS Model**: Qwen3-TTS\n- **Transcription**: Whisper\n- **Database**: SQLite\n- **Audio**: librosa, soundfile\n\n### API Structure\n\n<Files>\n  <Folder name=\"backend\" defaultOpen>\n    <File name=\"app.py\" />\n    <File name=\"main.py\" />\n    <File name=\"config.py\" />\n    <File name=\"models.py\" />\n    <File name=\"server.py\" />\n    <Folder name=\"routes\">\n      <File name=\"profiles.py\" />\n      <File name=\"generate.py\" />\n      <File name=\"history.py\" />\n      <File name=\"...\" />\n    </Folder>\n    <Folder name=\"services\">\n      <File name=\"generation.py\" />\n      <File name=\"task_queue.py\" />\n      <File name=\"...\" />\n    </Folder>\n    <Folder name=\"backends\">\n      <File name=\"__init__.py\" />\n      <File name=\"base.py\" />\n      <File name=\"...\" />\n    </Folder>\n    <Folder name=\"database\">\n      <File name=\"models.py\" />\n      <File name=\"session.py\" />\n    </Folder>\n    <Folder name=\"utils\">\n      <File name=\"audio.py\" />\n      <File name=\"effects.py\" />\n      <File name=\"...\" />\n    </Folder>\n  </Folder>\n</Files>\n\n### Request Flow\n\nHTTP request → **routes/** (validate input, parse params) → **services/** (business logic, orchestration) → **backends/** (TTS/STT inference) → **utils/** (audio processing)\n\nRoute handlers are intentionally thin. They validate input, delegate to a service function, and format the response. All business logic lives in `services/`.\n\n### Key Modules\n\n- **app.py** — FastAPI app factory, CORS, lifecycle events\n- **main.py** — Entry point (imports app, runs uvicorn)\n- **server.py** — Tauri sidecar launcher, parent-pid watchdog\n- **services/generation.py** — Single function handling all generation modes (generate, retry, regenerate)\n- **services/task_queue.py** — Serial generation queue for GPU inference\n- **backends/__init__.py** — Protocol definitions and backend factory\n- **backends/base.py** — Shared utilities across all engine implementations\n\n### Backend Selection\n\nThe server detects the best inference backend at startup:\n\n| Platform | Backend | Acceleration |\n|----------|---------|-------------|\n| macOS (Apple Silicon) | MLX | Metal / Neural Engine |\n| Windows / Linux (NVIDIA) | PyTorch | CUDA |\n| Linux (AMD) | PyTorch | ROCm |\n| Intel Arc | PyTorch | IPEX / XPU |\n| Windows (any GPU) | PyTorch | DirectML |\n| Any | PyTorch | CPU fallback |\n\n### Data Model\n\nThe database uses three main tables:\n\n**Profile Table:** Stores voice profiles with fields for id, name, and language.\n\n**Sample Table:** Stores audio samples linked to profiles via profile_id, with fields for audio_path and duration.\n\n**Generation Table:** Stores generated audio with fields for id, profile_id, text, and audio_path.\n\n## Desktop App (Tauri)\n\n### Rust Backend\n\n<Files>\n  <Folder name=\"tauri/src-tauri\" defaultOpen>\n    <File name=\"Cargo.toml\" />\n    <File name=\"src/\" />\n    <Folder name=\"binaries\" />\n  </Folder>\n</Files>\n\n### Responsibilities\n\n- Launch Python backend as sidecar process\n- Native file dialogs\n- System tray integration\n- Auto-updates\n- OS-specific features\n\n## Build Process\n\n### Development\n\n```bash\n# Frontend (Vite dev server)\ncd app && bun run dev\n\n# Backend (manual start)\ncd backend && uvicorn main:app --reload\n\n# Desktop app (connects to manual backend)\nbun run dev\n```\n\n### Production\n\n```bash\n# Build everything (server binary + Tauri app)\nbun run build\n\n# Or build separately:\n# 1. Build server binary (PyInstaller)\nbun run build:server\n\n# 2. Build Tauri app (includes server)\ncd tauri && bun run tauri build\n```\n\n## Data Flow\n\n### Generation Flow\n\nWhen a user generates speech, the data flows through the following stages:\n\n1. **User Input** - User enters text in a React component\n2. **State Update** - Text is stored in Zustand state\n3. **API Request** - React Query mutation triggers an API call via fetch\n4. **Backend Processing** - FastAPI endpoint receives the request\n5. **TTS Generation** - Qwen3-TTS model generates the audio\n6. **Storage** - Audio file is saved to disk and a database record is created\n7. **Response** - Backend returns the audio URL\n8. **Cache Update** - React Query updates its cache with the response\n9. **UI Update** - Component re-renders with new data\n10. **Playback** - User can play the generated audio\n\n## Performance Considerations\n\n### Frontend\n\n- **Code splitting** - Lazy load routes\n- **Memoization** - React.memo for heavy components\n- **Virtual scrolling** - For large lists\n- **Debouncing** - Search and input handling\n\n### Backend\n\n- **Async operations** - All I/O is async\n- **Model caching** - Keep TTS model in memory\n- **Voice prompt caching** - Reuse embeddings\n- **Connection pooling** - Database connections\n\n## Security\n\n### Current\n\n- Local-only by default\n- No authentication (localhost trust)\n- File system sandboxing via Tauri\n\n### Planned\n\n- API key authentication\n- User accounts\n- Rate limiting\n- HTTPS support\n\n## Deployment Modes\n\n### Local Mode\n\n- Backend runs as sidecar\n- All data stays on device\n- No network required\n\n### Remote Mode\n\n- Backend on separate machine\n- Frontend connects via HTTP\n- Shared infrastructure possible\n\n## Next Steps\n\n<Cards>\n  <Card title=\"Development Setup\" href=\"/development/setup\">\n    Set up your dev environment\n  </Card>\n  <Card title=\"Contributing\" href=\"/development/contributing\">\n    Contribute to Voicebox\n  </Card>\n</Cards>\n"
  },
  {
    "path": "docs/content/docs/developer/audio-channels.mdx",
    "content": "---\ntitle: \"Audio Channels\"\ndescription: \"How audio output routing works in Voicebox\"\n---\n\n## Overview\n\nAudio channels allow routing voice output to different audio devices. This is useful for multi-output setups where different voices should play through different speakers or applications.\n\n## Architecture\n\n**Channel:** A named audio bus that can be assigned to output devices.\n\n**Device Mapping:** Links channels to OS audio device identifiers.\n\n**Profile Mapping:** Links voice profiles to channels (many-to-many).\n\n## Data Model\n\n### AudioChannel Table\n\n```python\nclass AudioChannel(Base):\n    __tablename__ = \"audio_channels\"\n    \n    id = Column(String, primary_key=True)\n    name = Column(String, nullable=False)\n    is_default = Column(Boolean, default=False)\n    created_at = Column(DateTime)\n```\n\n### ChannelDeviceMapping Table\n\n```python\nclass ChannelDeviceMapping(Base):\n    __tablename__ = \"channel_device_mappings\"\n    \n    id = Column(String, primary_key=True)\n    channel_id = Column(String, ForeignKey(\"audio_channels.id\"))\n    device_id = Column(String)  # OS device identifier\n```\n\n### ProfileChannelMapping Table\n\n```python\nclass ProfileChannelMapping(Base):\n    __tablename__ = \"profile_channel_mappings\"\n    \n    profile_id = Column(String, ForeignKey(\"profiles.id\"), primary_key=True)\n    channel_id = Column(String, ForeignKey(\"audio_channels.id\"), primary_key=True)\n```\n\n## Default Channel\n\nA default channel is created on database initialization:\n\n```python\ndef init_db():\n    # Create default channel if it doesn't exist\n    default_channel = db.query(AudioChannel).filter(\n        AudioChannel.is_default == True\n    ).first()\n    \n    if not default_channel:\n        default_channel = AudioChannel(\n            id=str(uuid.uuid4()),\n            name=\"Default\",\n            is_default=True\n        )\n        db.add(default_channel)\n        \n        # Assign all existing profiles to default channel\n        profiles = db.query(VoiceProfile).all()\n        for profile in profiles:\n            mapping = ProfileChannelMapping(\n                profile_id=profile.id,\n                channel_id=default_channel.id\n            )\n            db.add(mapping)\n```\n\n## Core Operations\n\n### Creating a Channel\n\n```python\nasync def create_channel(\n    data: AudioChannelCreate,\n    db: Session,\n) -> AudioChannelResponse:\n    # Check name uniqueness\n    existing = db.query(DBAudioChannel).filter_by(name=data.name).first()\n    if existing:\n        raise ValueError(f\"Channel with name '{data.name}' already exists\")\n    \n    # Create channel\n    channel = DBAudioChannel(\n        id=str(uuid.uuid4()),\n        name=data.name,\n        is_default=False,\n    )\n    db.add(channel)\n    \n    # Add device mappings\n    for device_id in data.device_ids:\n        mapping = DBChannelDeviceMapping(\n            id=str(uuid.uuid4()),\n            channel_id=channel.id,\n            device_id=device_id,\n        )\n        db.add(mapping)\n    \n    db.commit()\n```\n\n### Updating a Channel\n\n```python\nasync def update_channel(\n    channel_id: str,\n    data: AudioChannelUpdate,\n    db: Session,\n) -> AudioChannelResponse:\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    \n    # Cannot modify default channel\n    if channel.is_default:\n        raise ValueError(\"Cannot modify the default channel\")\n    \n    # Update name\n    if data.name is not None:\n        channel.name = data.name\n    \n    # Update device mappings\n    if data.device_ids is not None:\n        # Delete existing\n        db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete()\n        \n        # Add new\n        for device_id in data.device_ids:\n            mapping = DBChannelDeviceMapping(\n                channel_id=channel.id,\n                device_id=device_id,\n            )\n            db.add(mapping)\n    \n    db.commit()\n```\n\n### Deleting a Channel\n\n```python\nasync def delete_channel(channel_id: str, db: Session) -> bool:\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    \n    # Cannot delete default channel\n    if channel.is_default:\n        raise ValueError(\"Cannot delete the default channel\")\n    \n    # Delete device mappings\n    db.query(DBChannelDeviceMapping).filter_by(channel_id=channel_id).delete()\n    \n    # Delete profile-channel mappings\n    db.query(DBProfileChannelMapping).filter_by(channel_id=channel_id).delete()\n    \n    # Delete channel\n    db.delete(channel)\n    db.commit()\n```\n\n## Voice Assignment\n\n### Assigning Voices to Channel\n\n```python\nasync def set_channel_voices(\n    channel_id: str,\n    data: ChannelVoiceAssignment,\n    db: Session,\n) -> None:\n    # Verify channel exists\n    channel = db.query(DBAudioChannel).filter_by(id=channel_id).first()\n    if not channel:\n        raise ValueError(f\"Channel {channel_id} not found\")\n    \n    # Verify all profiles exist\n    for profile_id in data.profile_ids:\n        profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n        if not profile:\n            raise ValueError(f\"Profile {profile_id} not found\")\n    \n    # Delete existing mappings\n    db.query(DBProfileChannelMapping).filter_by(channel_id=channel_id).delete()\n    \n    # Add new mappings\n    for profile_id in data.profile_ids:\n        mapping = DBProfileChannelMapping(\n            profile_id=profile_id,\n            channel_id=channel_id,\n        )\n        db.add(mapping)\n    \n    db.commit()\n```\n\n### Assigning Channels to Voice\n\n```python\nasync def set_profile_channels(\n    profile_id: str,\n    data: ProfileChannelAssignment,\n    db: Session,\n) -> None:\n    # Verify profile exists\n    profile = db.query(DBVoiceProfile).filter_by(id=profile_id).first()\n    if not profile:\n        raise ValueError(f\"Profile {profile_id} not found\")\n    \n    # Delete existing mappings\n    db.query(DBProfileChannelMapping).filter_by(profile_id=profile_id).delete()\n    \n    # Add new mappings\n    for channel_id in data.channel_ids:\n        mapping = DBProfileChannelMapping(\n            profile_id=profile_id,\n            channel_id=channel_id,\n        )\n        db.add(mapping)\n    \n    db.commit()\n```\n\n## API Endpoints\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| GET | `/channels` | List all channels |\n| POST | `/channels` | Create a channel |\n| GET | `/channels/{id}` | Get channel by ID |\n| PUT | `/channels/{id}` | Update channel |\n| DELETE | `/channels/{id}` | Delete channel |\n| GET | `/channels/{id}/voices` | Get assigned voices |\n| PUT | `/channels/{id}/voices` | Set assigned voices |\n| GET | `/profiles/{id}/channels` | Get profile's channels |\n| PUT | `/profiles/{id}/channels` | Set profile's channels |\n\n## Request/Response Schemas\n\n### AudioChannelCreate\n\n```json\n{\n  \"name\": \"Speakers\",\n  \"device_ids\": [\"device_uuid_1\", \"device_uuid_2\"]\n}\n```\n\n### AudioChannelResponse\n\n```json\n{\n  \"id\": \"channel_uuid\",\n  \"name\": \"Speakers\",\n  \"is_default\": false,\n  \"device_ids\": [\"device_uuid_1\", \"device_uuid_2\"],\n  \"created_at\": \"2024-01-15T10:30:00Z\"\n}\n```\n\n### ChannelVoiceAssignment\n\n```json\n{\n  \"profile_ids\": [\"profile_1\", \"profile_2\"]\n}\n```\n\n## Use Cases\n\n### Multi-Output Setup\n\n**Scenario:** Stream with different voice characters\n\n1. Create \"Stream\" channel → OBS virtual audio\n2. Create \"Monitor\" channel → Headphones\n3. Assign \"Narrator\" profile → Both channels\n4. Assign \"Character 1\" profile → Stream only\n\n### Virtual Audio Cables\n\nCommon device IDs for virtual audio:\n- VB-Audio Virtual Cable\n- BlackHole (macOS)\n- Soundflower (macOS)\n\n## Frontend Integration\n\nThe frontend needs to:\n\n1. **Enumerate devices** using Web Audio API or Tauri\n2. **Display channel list** with device assignments\n3. **Allow profile assignment** via drag/drop or dropdown\n4. **Route playback** to correct device based on profile's channel\n\n## Limitations\n\n- Device IDs are OS-specific\n- Hot-plugging may invalidate device IDs\n- Default channel cannot be modified/deleted\n- Frontend handles actual audio routing (backend just stores config)\n"
  },
  {
    "path": "docs/content/docs/developer/autoupdater.mdx",
    "content": "---\ntitle: \"Auto-Updater\"\ndescription: \"How Voicebox automatic updates work\"\n---\n\n## Overview\n\nVoicebox uses Tauri's built-in auto-updater to deliver signed updates to users. The system verifies updates cryptographically before installation.\n\n## How It Works\n\nWhen Voicebox launches (in production Tauri builds only), it checks GitHub Releases for a `latest.json` manifest. If a newer version is available:\n\n1. **Notification** - An update banner appears at the top of the app\n2. **Download** - User clicks \"Install Now\" to download the update package\n3. **Verification** - The downloaded package is cryptographically verified using the public key embedded in `tauri.conf.json`\n4. **Installation** - After verification, the update is installed\n5. **Restart** - The app restarts automatically with the new version\n\nUsers can also check for updates manually via **Settings → Check for Updates**.\n\n## Configuration\n\nThe updater is configured in `tauri/src-tauri/tauri.conf.json`:\n\n```json\n{\n  \"plugins\": {\n    \"updater\": {\n      \"active\": true,\n      \"dialog\": false,\n      \"endpoints\": [\n        \"https://github.com/jamiepine/voicebox/releases/latest/download/latest.json\"\n      ],\n      \"pubkey\": \"PASTE_PUBLIC_KEY_CONTENT_HERE\"\n    }\n  }\n}\n```\n\n**Key settings:**\n- `endpoints` - URL to the `latest.json` manifest (checked on app startup)\n- `pubkey` - Public key for verifying update signatures\n- `dialog` - Set to `false` (we use custom UI instead of Tauri's built-in dialog)\n\n## Release Manifest\n\nThe `latest.json` file defines available updates per platform:\n\n```json\n{\n  \"version\": \"0.2.0\",\n  \"notes\": \"Bug fixes and improvements\",\n  \"pub_date\": \"2026-01-25T12:00:00Z\",\n  \"platforms\": {\n    \"darwin-aarch64\": {\n      \"signature\": \"base64_encoded_signature\",\n      \"url\": \"https://github.com/jamiepine/voicebox/releases/download/v0.2.0/voicebox_0.2.0_aarch64.app.tar.gz\"\n    },\n    \"darwin-x86_64\": {\n      \"signature\": \"base64_encoded_signature\",\n      \"url\": \"https://github.com/jamiepine/voicebox/releases/download/v0.2.0/voicebox_0.2.0_x64.app.tar.gz\"\n    },\n    \"linux-x86_64\": {\n      \"signature\": \"base64_encoded_signature\",\n      \"url\": \"https://github.com/jamiepine/voicebox/releases/download/v0.2.0/voicebox_0.2.0_amd64.AppImage\"\n    },\n    \"windows-x86_64\": {\n      \"signature\": \"base64_encoded_signature\",\n      \"url\": \"https://github.com/jamiepine/voicebox/releases/download/v0.2.0/voicebox_0.2.0_x64_en-US.msi\"\n    }\n  }\n}\n```\n\n## Signing\n\nUpdates must be cryptographically signed to be accepted. The signing process:\n\n1. **Generate keys** (one-time setup):\n   ```bash\n   bun tauri signer generate -w ~/.tauri/voicebox.key\n   ```\n   This creates:\n   - Private key: `~/.tauri/voicebox.key` (stored in GitHub Secrets, never committed)\n   - Public key: `~/.tauri/voicebox.key.pub` (pasted into `tauri.conf.json`)\n\n2. **Build with signing** (GitHub Actions handles this):\n   - Set `TAURI_SIGNING_PRIVATE_KEY` environment variable\n   - Tauri signs the update package during build\n   - Generates `.sig` signature file alongside the installer\n\n3. **Verification** - The updater compares the signature against the public key before installing\n\n## GitHub Actions Workflow\n\nThe release workflow (`.github/workflows/release.yml`) automatically:\n\n- Builds signed releases for macOS, Windows, and Linux\n- Creates the `latest.json` manifest with signatures\n- Uploads everything to the GitHub Release\n\nTriggered by pushing a git tag:\n\n```bash\ngit tag v0.2.0 && git push --tags\n```\n\n## Environment Variables\n\nGitHub Actions needs these secrets set:\n\n- `TAURI_SIGNING_PRIVATE_KEY` - Content of `~/.tauri/voicebox.key`\n- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` - Password for the key (if set)\n\n## Security\n\n<Callout type=\"warn\">\n  **Critical:** Never commit the private key. Store it only in GitHub Secrets. The public key in `tauri.conf.json` is safe to commit and distribute.\n</Callout>\n\n- Updates are cryptographically signed using Ed25519\n- HTTP endpoints are blocked (HTTPS only)\n- Signature verification happens before installation\n- Failed verification aborts the update\n\n## Troubleshooting\n\n### \"Invalid signature\" error\n- Public key in `tauri.conf.json` doesn't match the private key used to sign\n- Signature file wasn't uploaded to the release\n\n### \"No update available\" when one exists\n- `latest.json` version isn't higher than current version\n- Wrong endpoint URL in configuration\n- Manifest hasn't propagated to GitHub's CDN yet\n\n### Update check fails in dev mode\nThe updater only works in production Tauri builds. It doesn't run during `just dev` or web mode.\n\n### Build fails with signing error\n- GitHub Secrets aren't set correctly\n- Private key file is missing or corrupted\n- Key format is wrong (should start with `dW50cnVzdGVkIGNvbW1lbnQ6`)\n\n## CUDA Backend Updates\n\nThe CUDA-enabled backend is distributed separately from the main app due to its large size (~2.43 GB). Unlike the Tauri auto-updater, this uses a custom download system built into the Python backend.\n\n**Size comparison:**\n- Standard app bundle: ~410 MB\n- CUDA backend binary: ~2.43 GB (6× larger)\n\n### Why Split?\n\nGitHub Releases has file size limits, and the CUDA-enabled `voicebox-server` binary is too large to include in the main Tauri bundle. Instead:\n\n- **Standard release**: Includes CPU-only backend (~50MB)\n- **CUDA release**: Split into multiple parts and downloaded on-demand by users who need GPU acceleration\n\n### Download Process\n\nWhen a user clicks \"Enable CUDA\" in the settings:\n\n1. **Manifest Fetch** - Backend fetches `{version}/voicebox-server-cuda.manifest` from GitHub Releases\n2. **Part Download** - Downloads each split part sequentially (e.g., `voicebox-server-cuda.part1`, `.part2`, etc.)\n3. **Assembly** - Concatenates parts into a single binary\n4. **Verification** - SHA-256 checksum verification (optional, if `.sha256` file exists)\n5. **Placement** - Binary moved to `{data_dir}/backends/voicebox-server-cuda.exe`\n6. **Restart** - Backend must restart to use the CUDA binary\n\n### Auto-Update on Startup\n\nOn server startup, `check_and_update_cuda_binary()` compares the installed CUDA binary version with the app version:\n\n```python\n# backend/services/cuda.py\ncuda_version = get_cuda_binary_version()  # runs `voicebox-server-cuda --version`\ncurrent_version = __version__\n\nif cuda_version != current_version:\n    await download_cuda_binary()  # Auto-download in background\n```\n\nIf versions mismatch, the backend automatically downloads the matching CUDA binary version without user intervention.\n\n### Storage Location\n\nDownloaded CUDA binaries are stored in the app's data directory:\n\n```\n{data_dir}/\n  backends/\n    voicebox-server-cuda.exe    # Windows\n    voicebox-server-cuda        # macOS/Linux\n```\n\n### API Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/backend/cuda-status` | GET | Check if CUDA binary available/active |\n| `/backend/download-cuda` | POST | Start download |\n| `/backend/cuda-progress` | GET | SSE stream of download progress |\n| `/backend/cuda` | DELETE | Remove downloaded binary |\n\n### Progress Tracking\n\nDownloads report progress via Server-Sent Events (SSE):\n\n```\nGET /backend/cuda-progress\n\nevent: progress\ndata: {\"current\": 52428800, \"total\": 104857600, \"filename\": \"Downloading CUDA backend (2/4)\", \"status\": \"downloading\"}\n```\n\nThe frontend subscribes to this endpoint to show real-time download progress in the UI.\n\n### Release Artifacts\n\nFor each release, these CUDA-related files are uploaded to GitHub:\n\n- `voicebox-server-cuda.manifest` - List of split part filenames\n- `voicebox-server-cuda.part1` through `voicebox-server-cuda.partN` - Binary chunks\n- `voicebox-server-cuda.sha256` - SHA-256 checksum for integrity verification\n"
  },
  {
    "path": "docs/content/docs/developer/building.mdx",
    "content": "---\ntitle: \"Building\"\ndescription: \"How Voicebox is built for production\"\n---\n\n## Overview\n\nVoicebox uses a two-stage build process:\n\n1. **Python Server Binary** — PyInstaller bundles the FastAPI backend into a standalone executable\n2. **Tauri Desktop App** — Bundles the React frontend, Rust wrapper, and Python server as a sidecar\n\n## Build Commands\n\n```bash\njust build          # Build everything (server + Tauri)\njust build-server   # Build Python server binary only\njust build-tauri    # Build Tauri app only\n```\n\n## Server Binary Build\n\n### Build Script\n\n`scripts/build-server.sh` orchestrates the build:\n\n```bash\n# Determine platform (e.g., x86_64-apple-darwin)\nPLATFORM=$(rustc --print host-tuple)\n\n# Run PyInstaller via build_binary.py\ncd backend\npython build_binary.py\n\n# Copy to Tauri's binaries directory\ncp dist/voicebox-server ../tauri/src-tauri/binaries/voicebox-server-${PLATFORM}\n```\n\n### PyInstaller Configuration\n\n`backend/build_binary.py` contains the PyInstaller configuration:\n\n**Entry Point:** Uses `server.py` (not `main.py`) for Tauri sidecar support\n\n**Key Options:**\n- `--onefile` — Single executable\n- `--hidden-import` — Explicitly import modules PyInstaller can't detect\n- `--collect-all` — Bundle data files and native libraries for packages like `mlx`, `zipvoice`\n- `--exclude-module` — Strip NVIDIA packages from CPU builds\n\n**Platform-Specific Logic:**\n\n```python\n# Apple Silicon — include MLX backend\nif is_apple_silicon() and not cuda:\n    args.extend([\n        \"--hidden-import\", \"mlx\",\n        \"--collect-all\", \"mlx\",        # Bundles .dylib and .metallib files\n    ])\n\n# CUDA builds — include torch.cuda\nif cuda:\n    args.extend([\"--hidden-import\", \"torch.cuda\"])\n\n# CPU builds — exclude NVIDIA packages to save ~3GB\nelse:\n    for pkg in [\"nvidia\", \"nvidia.cublas\", \"nvidia.cudnn\", ...]:\n        args.extend([\"--exclude-module\", pkg])\n```\n\n**Environment Variable:**\n\n```bash\nexport QWEN_TTS_PATH=~/path/to/Qwen3-TTS  # Use local Qwen3-TTS source\n```\n\n### CUDA Binary\n\nThe CUDA-enabled server is built separately due to size (~2.43 GB vs ~410 MB CPU version):\n\n```bash\ncd backend\npython build_binary.py --cuda\n```\n\nThe resulting binary is too large for GitHub Releases, so it's split into parts for distribution (see Auto-Updater docs for the download mechanism).\n\n## Tauri App Build\n\nTauri bundles everything together:\n\n```bash\ncd tauri\nbun run tauri build\n```\n\n**What happens:**\n1. Vite builds the React frontend\n2. Rust compiles the Tauri wrapper\n3. Sidecar binary is copied from `src-tauri/binaries/`\n4. Platform-specific installer created (DMG, MSI, AppImage)\n\n**Output locations:**\n\n<Files>\n  <Folder name=\"tauri/src-tauri/target/release/bundle\" defaultOpen>\n    <File name=\"dmg/\" />\n    <File name=\"msi/\" />\n    <File name=\"nsis/\" />\n    <File name=\"appimage/\" />\n  </Folder>\n</Files>\n\n### Sidecar Configuration\n\nThe server binary is declared as an external binary in `tauri.conf.json`:\n\n```json\n{\n  \"tauri\": {\n    \"bundle\": {\n      \"externalBin\": [\"binaries/voicebox-server\"]\n    }\n  }\n}\n```\n\nTauri looks for `voicebox-server-${PLATFORM}` in `src-tauri/binaries/` and bundles it.\n\n## GitHub Actions Release\n\n`.github/workflows/release.yml` automates the full build:\n\n### Matrix Strategy\n\n| Platform | Target | Backend | Notes |\n|----------|--------|---------|-------|\n| macos-latest | aarch64-apple-darwin | MLX | Apple Silicon native |\n| macos-15-intel | x86_64-apple-darwin | PyTorch | Intel Macs |\n| windows-latest | x86_64-pc-windows-msvc | PyTorch | Windows with CUDA optional |\n\n### Build Steps\n\n1. **Setup** — Python, Rust, Bun, dependencies\n2. **Build Server** — `build-server.sh` (Unix) or `build_binary.py` (Windows)\n3. **Build Tauri** — `tauri-action` with signing keys\n4. **Upload** — Release artifacts and `latest.json`\n\n### Code Signing\n\n**macOS:**\n- Apple Developer certificate imported from secrets\n- Notarization via App Store Connect API\n\n**Windows:**\n- Tauri handles signing via `TAURI_SIGNING_PRIVATE_KEY`\n\n### CUDA Binary (Separate Job)\n\nThe `build-cuda-windows` job runs separately:\n\n1. Install PyTorch with CUDA 12.8\n2. Build with `build_binary.py --cuda` (produces `--onedir` output)\n3. Package with `scripts/package_cuda.py` into two archives:\n   - `voicebox-server-cuda.tar.gz` — server core (~945 MB)\n   - `cuda-libs-cu128-v1.tar.gz` — NVIDIA runtime libraries (~1.7 GB, cached independently)\n4. Upload archives as release artifacts\n\nThis binary is downloaded on-demand by users who enable CUDA in settings. The CUDA libs archive is only re-downloaded when the CUDA toolkit version changes, not on every app update.\n\n## Troubleshooting\n\n<AccordionGroup>\n  <Accordion title=\"Binary not found in dist/\">\n    PyInstaller failed to create the output. Check:\n    - Python venv is activated\n    - All dependencies installed: `pip install -r requirements.txt`\n    - PyInstaller installed: `pip install pyinstaller`\n  </Accordion>\n\n  <Accordion title=\"MLX/Metal libraries missing in bundle\">\n    macOS Apple Silicon builds need `--collect-all mlx` to include `.dylib` and `.metallib` files, not just `--collect-data`.\n  </Accordion>\n\n  <Accordion title=\"CUDA DLLs bloating CPU build\">\n    If building CPU version but CUDA torch is installed locally, the script auto-detects and swaps to CPU torch temporarily, then restores CUDA torch after.\n  </Accordion>\n\n  <Accordion title=\"Tauri can't find sidecar\">\n    Ensure binary exists at `tauri/src-tauri/binaries/voicebox-server-${PLATFORM}` before running Tauri build.\n  </Accordion>\n</AccordionGroup>\n"
  },
  {
    "path": "docs/content/docs/developer/contributing.mdx",
    "content": "---\ntitle: \"Contributing\"\ndescription: \"How to contribute to Voicebox\"\n---\n\nThank you for your interest in contributing to Voicebox! This guide will help you get started.\n\n## Code of Conduct\n\n- Be respectful and inclusive\n- Welcome newcomers and help them learn\n- Focus on constructive feedback\n- Respect different viewpoints and experiences\n\n## Getting Started\n\nBefore you start contributing, make sure you have:\n\n1. **Read the documentation** to understand how Voicebox works\n2. **Set up your development environment** - see [Development Setup](/development/setup)\n3. **Explored the codebase** to understand the project structure\n4. **Checked existing issues** to see if someone else is working on something similar\n\n## Ways to Contribute\n\n<Cards>\n  <Card title=\"Report Bugs\">\n    Found a bug? Open an issue with reproduction steps\n  </Card>\n  <Card title=\"Request Features\">\n    Have an idea? Start a discussion or open an issue\n  </Card>\n  <Card title=\"Improve Docs\">\n    Fix typos, add examples, or clarify instructions\n  </Card>\n  <Card title=\"Write Code\">\n    Fix bugs, add features, or optimize performance\n  </Card>\n</Cards>\n\n## Development Workflow\n\n### 1. Fork & Clone\n\n```bash\n# Fork the repository on GitHub\n# Then clone your fork\ngit clone https://github.com/YOUR_USERNAME/voicebox.git\ncd voicebox\n```\n\n### 2. Create a Branch\n\nUse descriptive branch names:\n\n```bash\n# For features\ngit checkout -b feature/voice-effects\n\n# For bug fixes\ngit checkout -b fix/audio-playback-issue\n\n# For documentation\ngit checkout -b docs/api-examples\n```\n\n### 3. Make Your Changes\n\nFollow these guidelines:\n\n<AccordionGroup>\n  <Accordion title=\"Code Style\">\n    **TypeScript/React:**\n    - Use TypeScript strict mode\n    - Prefer functional components with hooks\n    - Use named exports\n    - Format with Biome (runs automatically)\n\n    **Python:**\n    - Follow PEP 8\n    - Use type hints\n    - Use async/await for I/O\n    - Document functions with docstrings\n\n    **Rust:**\n    - Follow Rust conventions\n    - Use meaningful names\n    - Handle errors explicitly\n    - Run `rustfmt`\n  </Accordion>\n\n  <Accordion title=\"Commit Messages\">\n    Write clear, descriptive commit messages:\n\n    ```bash\n    # Good\n    git commit -m \"Add voice profile export feature\"\n    git commit -m \"Fix audio playback stopping after 30 seconds\"\n\n    # Avoid\n    git commit -m \"Update code\"\n    git commit -m \"Fix bug\"\n    ```\n\n    Format:\n    - Use imperative mood (\"Add feature\" not \"Added feature\")\n    - Keep first line under 50 characters\n    - Add detailed description if needed\n  </Accordion>\n\n  <Accordion title=\"Testing\">\n    - Test your changes manually in the app\n    - Ensure backend API endpoints work\n    - Check for TypeScript/Python errors\n    - Verify UI components render correctly\n    - Add automated tests when possible\n  </Accordion>\n</AccordionGroup>\n\n### 4. Push & Create PR\n\n```bash\n# Push your branch\ngit push origin feature/your-feature-name\n\n# Then create a pull request on GitHub\n```\n\n## Pull Request Guidelines\n\nWhen creating a pull request:\n\n<Steps>\n  <Step title=\"Use a Clear Title\">\n    Examples:\n    - \"Add voice profile export functionality\"\n    - \"Fix audio playback stopping after 30 seconds\"\n    - \"Improve generation speed with caching\"\n  </Step>\n\n  <Step title=\"Provide Description\">\n    Include:\n    - What changes you made\n    - Why you made them\n    - How to test them\n    - Screenshots (for UI changes)\n    - Reference related issues\n  </Step>\n\n  <Step title=\"Update Documentation\">\n    - Update relevant docs if behavior changes\n    - Add API documentation for new endpoints\n    - Update README if needed\n  </Step>\n\n  <Step title=\"Check the Checklist\">\n    - [ ] Code follows style guidelines\n    - [ ] Documentation updated\n    - [ ] Changes tested\n    - [ ] No breaking changes (or documented)\n    - [ ] CHANGELOG.md updated\n  </Step>\n</Steps>\n\n## Project Structure\n\n<Files>\n  <Folder name=\"voicebox\" defaultOpen>\n    <Folder name=\"app/src\">\n      <File name=\"components/\" />\n      <File name=\"lib/\" />\n      <File name=\"hooks/\" />\n      <File name=\"stores/\" />\n    </Folder>\n    <Folder name=\"backend\">\n      <File name=\"main.py\" />\n      <File name=\"tts.py\" />\n      <File name=\"database.py\" />\n      <File name=\"models.py\" />\n    </Folder>\n    <Folder name=\"tauri\">\n      <File name=\"src-tauri/\" />\n    </Folder>\n    <File name=\"web/\" />\n    <File name=\"landing/\" />\n    <File name=\"scripts/\" />\n  </Folder>\n</Files>\n\n## Areas for Contribution\n\n### Bug Fixes\n\n- Check [existing issues](https://github.com/jamiepine/voicebox/issues) for bugs\n- Test your fix thoroughly\n- Add regression tests if possible\n\n### New Features\n\n- Check the [roadmap](https://github.com/jamiepine/voicebox#roadmap) for planned features\n- Discuss major features in an issue first\n- Keep features focused and well-scoped\n\n### Documentation\n\n- Improve clarity and fix typos\n- Add code examples\n- Create tutorials or guides\n- Document API endpoints\n\n### UI/UX Improvements\n\n- Improve accessibility\n- Enhance visual design\n- Optimize performance\n- Add animations/transitions\n\n### Infrastructure\n\n- Improve build process\n- Add CI/CD improvements\n- Optimize bundle size\n- Add testing infrastructure\n\n## API Development\n\nWhen adding new API endpoints:\n\n<Steps>\n  <Step title=\"Add Route\">\n    In `backend/main.py`:\n\n    ```python\n    @app.post(\"/api/new-endpoint\")\n    async def new_endpoint(data: RequestModel) -> ResponseModel:\n        \"\"\"Endpoint description.\"\"\"\n        # Implementation\n        return response\n    ```\n  </Step>\n\n  <Step title=\"Create Models\">\n    In `backend/models.py`:\n\n    ```python\n    class RequestModel(BaseModel):\n        field: str\n\n    class ResponseModel(BaseModel):\n        result: str\n    ```\n  </Step>\n\n  <Step title=\"Regenerate Client\">\n    ```bash\n    bun run generate:api\n    ```\n\n    This updates the TypeScript client with type-safe bindings.\n  </Step>\n\n  <Step title=\"Update Docs\">\n    The API documentation is automatically generated from the OpenAPI schema. Ensure your endpoint has proper docstrings and type hints, then regenerate the docs:\n    \n    ```bash\n    bun run generate:api\n    ```\n  </Step>\n</Steps>\n\n## Testing\n\nCurrently testing is primarily manual. When adding tests:\n\n**Backend:**\n```bash\ncd backend\npytest\n```\n\n**Frontend:**\n```bash\nbun run test\n```\n\n**E2E (future):**\n```bash\nbun run test:e2e\n```\n\n## Release Process\n\nReleases are managed by maintainers using `bumpversion`:\n\n```bash\n# Bump version (patch, minor, or major)\nbumpversion patch\n\n# Push with tags\ngit push && git push --tags\n```\n\nGitHub Actions automatically builds and publishes releases when tags are pushed.\n\n## Community\n\n- **GitHub Issues:** Bug reports and feature requests\n- **GitHub Discussions:** General questions and ideas\n- **Discord:** Real-time chat (coming soon)\n\n## Recognition\n\nContributors are recognized in:\n- [CHANGELOG.md](https://github.com/jamiepine/voicebox/blob/main/CHANGELOG.md)\n- GitHub contributor list\n- Release notes\n\n## License\n\nBy contributing, you agree that your contributions will be licensed under the MIT License.\n\n## Questions?\n\nIf you have questions:\n\n1. Check the [documentation](/overview/introduction)\n2. Search [existing issues](https://github.com/jamiepine/voicebox/issues)\n3. Open a new issue or discussion\n4. See [CONTRIBUTING.md](https://github.com/jamiepine/voicebox/blob/main/CONTRIBUTING.md) in the repo\n\nThank you for contributing to Voicebox! 🎉\n"
  },
  {
    "path": "docs/content/docs/developer/effects-pipeline.mdx",
    "content": "---\ntitle: \"Effects Pipeline\"\ndescription: \"Audio post-processing effects and generation versioning\"\n---\n\nThe effects pipeline provides professional-grade DSP audio processing using Spotify's Pedalboard library. Each generation can have multiple versions with different effect chains applied.\n\n## Overview\n\n**Key concepts:**\n\n- **Effects Chain** — JSON-serializable list of effect configurations applied sequentially\n- **Generation Version** — A processed variant of a generation with its own audio file and effects chain\n- **Effect Preset** — Saved effects chain configuration (built-in or user-created)\n- **Clean Version** — The original unprocessed generation audio\n\n**Flow:**\n\n1. TTS Generation creates clean audio\n2. Effects Chain processes the audio\n3. Processed Version is saved as a new generation version\n\nEach generation maintains a clean version (original) plus any number of processed versions with different effect chains applied.\n\n## Effect Types\n\nThe following effect types are available, each with configurable parameters:\n\n### Chorus / Flanger\n\nModulated delay effect. Short centre_delay_ms gives flanger; longer gives chorus.\n\n**Parameters:**\n- rate_hz: LFO speed in Hz (range: 0.01 to 20, default: 1.0)\n- depth: Modulation depth (range: 0.0 to 1.0, default: 0.5)\n- feedback: Feedback amount (range: 0.0 to 0.95, default: 0.0)\n- centre_delay_ms: Centre delay in milliseconds (range: 0.5 to 50, default: 7.0)\n- mix: Wet/dry mix (range: 0.0 to 1.0, default: 0.5)\n\n### Reverb\n\nRoom reverb effect.\n\n**Parameters:**\n- room_size: Room size (range: 0.0 to 1.0, default: 0.5)\n- damping: High frequency damping (range: 0.0 to 1.0, default: 0.5)\n- wet_level: Wet level (range: 0.0 to 1.0, default: 0.33)\n- dry_level: Dry level (range: 0.0 to 1.0, default: 0.4)\n- width: Stereo width (range: 0.0 to 1.0, default: 1.0)\n\n### Delay\n\nEcho / delay line.\n\n**Parameters:**\n- delay_seconds: Delay time in seconds (range: 0.01 to 2.0, default: 0.3)\n- feedback: Feedback amount (range: 0.0 to 0.95, default: 0.3)\n- mix: Wet/dry mix (range: 0.0 to 1.0, default: 0.3)\n\n### Compressor\n\nDynamic range compression for consistent loudness.\n\n**Parameters:**\n- threshold_db: Threshold in dB (range: -60 to 0, default: -20.0)\n- ratio: Compression ratio (range: 1.0 to 20.0, default: 4.0)\n- attack_ms: Attack time in ms (range: 0.1 to 100, default: 10.0)\n- release_ms: Release time in ms (range: 10 to 1000, default: 100.0)\n\n### Gain\n\nVolume adjustment in decibels.\n\n**Parameters:**\n- gain_db: Gain in dB (range: -40 to 40, default: 0.0)\n\n### High-Pass Filter\n\nRemoves frequencies below the cutoff.\n\n**Parameters:**\n- cutoff_frequency_hz: Cutoff frequency in Hz (range: 20 to 8000, default: 80.0)\n\n### Low-Pass Filter\n\nRemoves frequencies above the cutoff.\n\n**Parameters:**\n- cutoff_frequency_hz: Cutoff frequency in Hz (range: 200 to 20000, default: 8000.0)\n\n### Pitch Shift\n\nShift pitch up or down by semitones.\n\n**Parameters:**\n- semitones: Semitones to shift (range: -12 to 12, default: 0.0)\n\n## Generation Versions\n\nEach generation starts with a clean version (no effects). Users can create processed versions by applying effect chains.\n\n**Version properties:**\n- id — Unique version identifier\n- label — User-defined name (e.g., \"robotic\", \"with reverb\")\n- audio_path — Path to the processed audio file\n- effects_chain — JSON array of effect configurations\n- source_version_id — Which version this was derived from\n- is_default — Whether this is the default audio for the generation\n\n**File storage:**\n\n<Files>\n  <Folder name=\"data/generations\" defaultOpen>\n    <File name=\"{generation_id}.wav\" />\n    <File name=\"{generation_id}_{version_id}.wav\" />\n  </Folder>\n</Files>\n\n**Default version behavior:**\n- One version per generation is marked as default\n- The generation's audio_path always points to the default version's audio\n- Deleting the default version automatically promotes another version\n\n## Effect Presets\n\nPresets are saved effects chains that can be reused across generations.\n\n**Built-in presets:**\n\n- **Robotic**: Metallic robotic voice using chorus (flanger-style)\n- **Radio**: Thin AM-radio voice with band-pass filtering and light compression\n- **Echo Chamber**: Spacious reverb with trailing echo\n- **Deep Voice**: Lower pitch with added warmth using pitch shift and compression\n\n**User presets:**\n- Created via the effects UI\n- Stored in the database (SQLite)\n- Cannot modify/delete built-in presets\n- Used to quickly apply favorite effect combinations\n\n## API Endpoints\n\n### Effects Management\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| /effects/available | GET | List all effect types with parameter definitions |\n| /effects/presets | GET | List all presets (built-in + user) |\n| /effects/presets | POST | Create a new user preset |\n| /effects/presets/:id | GET | Get a specific preset |\n| /effects/presets/:id | PUT | Update a user preset |\n| /effects/presets/:id | DELETE | Delete a user preset |\n| /effects/preview/:generation_id | POST | Preview effects on a generation (returns audio stream) |\n\n### Generation Versions\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| /generations/:id/versions | GET | List all versions for a generation |\n| /generations/:id/versions/apply-effects | POST | Apply effects chain, create new version |\n| /generations/:id/versions/:version_id/set-default | PUT | Set a version as default |\n| /generations/:id/versions/:version_id | DELETE | Delete a version |\n\n### Request Body: Apply Effects\n\nRequest body for applying effects:\n\n- effects_chain: Array of effect objects\n- label: Version label (e.g., \"with reverb\")\n- set_as_default: Whether to set as default\n- source_version_id: Source version ID (optional)\n\n## Implementation\n\n### Backend Architecture\n\n**Files:**\n\n| File | Purpose |\n|------|---------|\n| backend/utils/effects.py | Effect registry, validation, and audio processing |\n| backend/services/versions.py | Generation version CRUD operations |\n| backend/services/effects.py | Effect preset CRUD operations |\n| backend/routes/effects.py | API endpoints for effects and versions |\n\n**Effect Registry:**\n\nThe EFFECT_REGISTRY dict in utils/effects.py defines all available effects with their parameters, defaults, and ranges.\n\n**Validation:**\n\nEffects chains are validated before application:\n- Each effect type must exist in the registry\n- Parameters must be numbers within min/max bounds\n- Unknown parameters are rejected\n\n**Audio Processing:**\n\nUses Spotify's Pedalboard library:\n\n```python\nfrom pedalboard import Pedalboard\n\n# Build pedalboard from chain\nboard = build_pedalboard(effects_chain)\n\n# Apply to audio (async via thread)\nprocessed = await asyncio.to_thread(lambda: board(audio, sample_rate))\n```\n\n### Frontend Integration\n\n**Key components:**\n\n| Component | Location |\n|-----------|----------|\n| Effects chain editor | app/src/components/Effects/ |\n| Version selector | Generation detail view |\n| Preset manager | Effects panel |\n| Live preview | Preview button (streams processed audio) |\n\n**State management:**\n- Effects chains are stored as JSON arrays\n- Live preview fetches processed audio without saving\n- Applied effects create new versions via POST endpoint\n\n## Adding New Effects\n\nTo add a new effect type:\n\n1. **Add to registry** (backend/utils/effects.py):\n   - Add entry to EFFECT_REGISTRY with cls, label, description, and params\n   - Import the effect class from Pedalboard\n\n2. **Update frontend types** if needed\n\nThe new effect automatically appears in /effects/available and the chain editor UI.\n\n## Best Practices\n\n**Effect ordering matters.** Process effects in this order for best results:\n1. Pitch shift (if needed)\n2. High/low-pass filters\n3. Chorus/flanger (time-based)\n4. Reverb/delay (spatial)\n5. Compressor\n6. Gain (final level adjustment)\n\n**CPU usage:**\n- Effects are applied in real-time during generation\n- Pitch shift and reverb are the most CPU-intensive\n- Consider previewing complex chains before applying\n\n**Storage:**\n- Each version creates a new audio file\n- Clean version always exists (can be reverted to)\n- Processed versions can be deleted to save space\n"
  },
  {
    "path": "docs/content/docs/developer/history.mdx",
    "content": "---\ntitle: \"Generation History\"\ndescription: \"How generation history tracking works in Voicebox\"\n---\n\n## Overview\n\nThe history module tracks all generated audio, providing a searchable record of past generations. Each generation stores the text, settings, and a reference to the audio file.\n\n## Data Model\n\n### Generation Table\n\n```python\nclass Generation(Base):\n    __tablename__ = \"generations\"\n\n    id = Column(String, primary_key=True)\n    profile_id = Column(String, ForeignKey(\"profiles.id\"))\n    text = Column(Text, nullable=False)\n    language = Column(String, default=\"en\")\n    audio_path = Column(String, nullable=False)\n    duration = Column(Float, nullable=False)\n    seed = Column(Integer)\n    instruct = Column(Text)\n    created_at = Column(DateTime)\n```\n\n## File Storage\n\nGenerated audio is stored in:\n\n<Files>\n  <Folder name=\"data\" defaultOpen>\n    <Folder name=\"generations\">\n      <File name=\"{generation_id}.wav\" />\n    </Folder>\n  </Folder>\n</Files>\n\n## Core Functions\n\n### Creating a Generation Record\n\nAfter TTS generates audio, a history entry is created:\n\n```python\nasync def create_generation(\n    profile_id: str,\n    text: str,\n    language: str,\n    audio_path: str,\n    duration: float,\n    seed: Optional[int],\n    db: Session,\n    instruct: Optional[str] = None,\n) -> GenerationResponse:\n    db_generation = DBGeneration(\n        id=str(uuid.uuid4()),\n        profile_id=profile_id,\n        text=text,\n        language=language,\n        audio_path=audio_path,\n        duration=duration,\n        seed=seed,\n        instruct=instruct,\n        created_at=datetime.utcnow(),\n    )\n    \n    db.add(db_generation)\n    db.commit()\n    \n    return GenerationResponse.model_validate(db_generation)\n```\n\n### Listing Generations\n\nSupports filtering and pagination:\n\n```python\nasync def list_generations(\n    query: HistoryQuery,\n    db: Session,\n) -> HistoryListResponse:\n    # Build query with profile name join\n    q = db.query(\n        DBGeneration,\n        DBVoiceProfile.name.label('profile_name')\n    ).join(\n        DBVoiceProfile,\n        DBGeneration.profile_id == DBVoiceProfile.id\n    )\n    \n    # Apply filters\n    if query.profile_id:\n        q = q.filter(DBGeneration.profile_id == query.profile_id)\n    \n    if query.search:\n        q = q.filter(DBGeneration.text.like(f\"%{query.search}%\"))\n    \n    # Order and paginate\n    total = q.count()\n    q = q.order_by(DBGeneration.created_at.desc())\n    q = q.offset(query.offset).limit(query.limit)\n    \n    return HistoryListResponse(items=results, total=total)\n```\n\n### Getting Statistics\n\nAggregate statistics for the dashboard:\n\n```python\nasync def get_generation_stats(db: Session) -> dict:\n    total = db.query(func.count(DBGeneration.id)).scalar()\n    total_duration = db.query(func.sum(DBGeneration.duration)).scalar()\n    \n    by_profile = db.query(\n        DBGeneration.profile_id,\n        func.count(DBGeneration.id).label('count')\n    ).group_by(DBGeneration.profile_id).all()\n    \n    return {\n        \"total_generations\": total,\n        \"total_duration_seconds\": total_duration,\n        \"generations_by_profile\": {\n            profile_id: count for profile_id, count in by_profile\n        },\n    }\n```\n\n## Deletion\n\nDeleting a generation removes both the database record and audio file:\n\n```python\nasync def delete_generation(generation_id: str, db: Session) -> bool:\n    generation = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not generation:\n        return False\n    \n    # Delete audio file\n    audio_path = Path(generation.audio_path)\n    if audio_path.exists():\n        audio_path.unlink()\n    \n    # Delete database record\n    db.delete(generation)\n    db.commit()\n    \n    return True\n```\n\n### Cascade Delete\n\nWhen deleting a profile, all its generations are also deleted:\n\n```python\nasync def delete_generations_by_profile(profile_id: str, db: Session) -> int:\n    generations = db.query(DBGeneration).filter_by(profile_id=profile_id).all()\n    \n    for generation in generations:\n        Path(generation.audio_path).unlink(missing_ok=True)\n        db.delete(generation)\n    \n    db.commit()\n    return len(generations)\n```\n\n## Export/Import\n\n### Exporting a Generation\n\nGenerations can be exported as ZIP archives:\n\n<Files>\n  <Folder name=\"generation_export.zip\" defaultOpen>\n    <File name=\"generation.json\" />\n    <File name=\"audio.wav\" />\n  </Folder>\n</Files>\n\n### Importing a Generation\n\nThe import process:\n\n1. Extract ZIP archive\n2. Validate metadata and audio\n3. Create new generation ID\n4. Copy audio to generations directory\n5. Create database record\n\n## API Endpoints\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| GET | `/history` | List generations with filters |\n| GET | `/history/stats` | Get aggregate statistics |\n| GET | `/history/{id}` | Get generation by ID |\n| DELETE | `/history/{id}` | Delete generation |\n| GET | `/history/{id}/export` | Export as ZIP |\n| GET | `/history/{id}/export-audio` | Export audio only |\n| POST | `/history/import` | Import from ZIP |\n\n### Query Parameters\n\n```\nGET /history?profile_id=uuid&search=hello&limit=50&offset=0\n```\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `profile_id` | string | null | Filter by profile |\n| `search` | string | null | Search in text |\n| `limit` | int | 50 | Results per page |\n| `offset` | int | 0 | Pagination offset |\n\n### Response Schema\n\n```json\n{\n  \"items\": [\n    {\n      \"id\": \"uuid\",\n      \"profile_id\": \"uuid\",\n      \"profile_name\": \"My Voice\",\n      \"text\": \"Hello world\",\n      \"language\": \"en\",\n      \"audio_path\": \"/path/to/audio.wav\",\n      \"duration\": 1.5,\n      \"seed\": 42,\n      \"instruct\": null,\n      \"created_at\": \"2024-01-15T10:30:00Z\"\n    }\n  ],\n  \"total\": 150\n}\n```\n\n## Usage in Stories\n\nGenerations can be added to stories for multi-voice narratives. The story system references generations by ID:\n\n```python\nclass StoryItem(Base):\n    generation_id = Column(String, ForeignKey(\"generations.id\"))\n```\n\nThis allows the same generation to be reused across multiple stories without duplicating audio files.\n\n## Storage Considerations\n\n### Disk Usage\n\nEach generation creates a WAV file. For a 10-second clip at 24kHz:\n- ~480KB per file (mono, 16-bit)\n\n### Cleanup Strategy\n\nConsider implementing:\n- Automatic cleanup of old generations\n- Storage quota per profile\n- Compression for archival\n"
  },
  {
    "path": "docs/content/docs/developer/meta.json",
    "content": "{\n  \"title\": \"Developer\",\n  \"defaultOpen\": true,\n  \"pages\": [\n    \"setup\",\n    \"architecture\",\n    \"contributing\",\n    \"building\",\n    \"autoupdater\",\n    \"voice-profiles\",\n    \"tts-generation\",\n    \"tts-engines\",\n    \"effects-pipeline\",\n    \"history\",\n    \"stories\",\n    \"transcription\",\n    \"audio-channels\",\n    \"model-management\"\n  ]\n}\n"
  },
  {
    "path": "docs/content/docs/developer/model-management.mdx",
    "content": "---\ntitle: \"Model Management\"\ndescription: \"How model downloading, loading, and status tracking works in Voicebox\"\n---\n\n## Overview\n\nVoicebox manages two types of models:\n\n**TTS Models:** Qwen3-TTS for voice cloning (0.6B and 1.7B variants).\n\n**ASR Models:** Whisper for transcription (tiny through large).\n\nModels are downloaded from HuggingFace Hub on first use and cached locally.\n\n## Available Models\n\n### TTS Models\n\n| Model | HuggingFace ID | Size | VRAM |\n|-------|----------------|------|------|\n| 0.6B | `Qwen/Qwen3-TTS-12Hz-0.6B-Base` | ~1.2GB | ~2GB |\n| 1.7B | `Qwen/Qwen3-TTS-12Hz-1.7B-Base` | ~3.4GB | ~6GB |\n\n### Whisper Models\n\n| Model | HuggingFace ID | Size | VRAM |\n|-------|----------------|------|------|\n| tiny | `openai/whisper-tiny` | ~150MB | ~1GB |\n| base | `openai/whisper-base` | ~300MB | ~1GB |\n| small | `openai/whisper-small` | ~500MB | ~2GB |\n| medium | `openai/whisper-medium` | ~1.5GB | ~5GB |\n| large | `openai/whisper-large` | ~3GB | ~10GB |\n\n## Model Storage\n\nModels are cached in the HuggingFace cache directory:\n\n<Files>\n  <Folder name=\"~/.cache/huggingface/hub\" defaultOpen>\n    <File name=\"models--Qwen--Qwen3-TTS-12Hz-1.7B-Base/\" />\n    <File name=\"models--Qwen--Qwen3-TTS-12Hz-0.6B-Base/\" />\n    <File name=\"models--openai--whisper-base/\" />\n  </Folder>\n</Files>\n\n## Progress Tracking\n\n### Progress Manager\n\nTracks download progress across all models:\n\n```python\nclass ProgressManager:\n    def __init__(self):\n        self._progress = {}  # model_name -> progress_info\n    \n    def update_progress(\n        self,\n        model_name: str,\n        current: int,\n        total: int,\n        filename: str,\n        status: str,\n    ):\n        self._progress[model_name] = {\n            \"current\": current,\n            \"total\": total,\n            \"filename\": filename,\n            \"status\": status,  # downloading, complete, error\n            \"updated_at\": datetime.utcnow(),\n        }\n    \n    def get_progress(self, model_name: str) -> Optional[dict]:\n        return self._progress.get(model_name)\n```\n\n### HuggingFace Progress Callback\n\nHooks into HuggingFace's download system:\n\n```python\nclass HFProgressTracker:\n    def __init__(self, callback):\n        self.callback = callback\n    \n    @contextmanager\n    def patch_download(self):\n        \"\"\"Context manager to intercept HF downloads.\"\"\"\n        original_download = hf_hub_download\n        \n        def patched_download(*args, **kwargs):\n            # Intercept progress\n            result = original_download(*args, **kwargs)\n            self.callback(progress_info)\n            return result\n        \n        # Apply patch\n        with patch('huggingface_hub.hf_hub_download', patched_download):\n            yield\n```\n\n### Server-Sent Events (SSE)\n\nProgress is streamed to the frontend:\n\n```python\n@app.get(\"/models/progress/{model_name}\")\nasync def get_model_progress(model_name: str):\n    async def event_generator():\n        while True:\n            progress = progress_manager.get_progress(model_name)\n            if progress:\n                yield f\"data: {json.dumps(progress)}\\n\\n\"\n            \n            if progress and progress[\"status\"] in [\"complete\", \"error\"]:\n                break\n            \n            await asyncio.sleep(0.5)\n    \n    return StreamingResponse(\n        event_generator(),\n        media_type=\"text/event-stream\"\n    )\n```\n\n## Task Manager\n\nTracks active downloads and generations:\n\n```python\nclass TaskManager:\n    def __init__(self):\n        self._active_downloads = {}\n        self._active_generations = {}\n    \n    def start_download(self, model_name: str):\n        self._active_downloads[model_name] = {\n            \"status\": \"downloading\",\n            \"started_at\": datetime.utcnow(),\n        }\n    \n    def complete_download(self, model_name: str):\n        if model_name in self._active_downloads:\n            del self._active_downloads[model_name]\n    \n    def get_active_tasks(self) -> dict:\n        return {\n            \"downloads\": list(self._active_downloads.values()),\n            \"generations\": list(self._active_generations.values()),\n        }\n```\n\n## Model Status\n\nCheck which models are downloaded and loaded:\n\n```python\n@app.get(\"/models/status\")\nasync def get_model_status() -> ModelStatusListResponse:\n    models = []\n    \n    # Check TTS models\n    for size, hf_id in [(\"1.7B\", \"Qwen/Qwen3-TTS-12Hz-1.7B-Base\"), ...]:\n        downloaded = is_model_downloaded(hf_id)\n        loaded = tts_model._current_model_size == size\n        \n        models.append(ModelStatus(\n            model_name=f\"qwen-tts-{size}\",\n            display_name=f\"Qwen3-TTS {size}\",\n            downloaded=downloaded,\n            size_mb=get_model_size_mb(hf_id),\n            loaded=loaded,\n        ))\n    \n    # Check Whisper models\n    for size in [\"tiny\", \"base\", \"small\", \"medium\", \"large\"]:\n        hf_id = f\"openai/whisper-{size}\"\n        downloaded = is_model_downloaded(hf_id)\n        \n        models.append(ModelStatus(\n            model_name=f\"whisper-{size}\",\n            display_name=f\"Whisper {size}\",\n            downloaded=downloaded,\n            size_mb=get_model_size_mb(hf_id),\n            loaded=False,  # Whisper is loaded on-demand\n        ))\n    \n    return ModelStatusListResponse(models=models)\n```\n\n## Manual Model Operations\n\n### Load Model\n\n```python\n@app.post(\"/models/load\")\nasync def load_model(model_size: str = \"1.7B\"):\n    tts_model = get_tts_model()\n    await tts_model.load_model_async(model_size)\n    return {\"status\": \"loaded\", \"model_size\": model_size}\n```\n\n### Unload Model\n\n```python\n@app.post(\"/models/unload\")\nasync def unload_model():\n    tts_model = get_tts_model()\n    tts_model.unload_model()\n    return {\"status\": \"unloaded\"}\n```\n\n### Trigger Download\n\n```python\n@app.post(\"/models/download\")\nasync def trigger_model_download(request: ModelDownloadRequest):\n    # This triggers the download in background\n    # Progress is tracked via /models/progress/{model_name}\n    \n    if request.model_name.startswith(\"qwen-tts\"):\n        size = request.model_name.split(\"-\")[-1]\n        asyncio.create_task(download_tts_model(size))\n    elif request.model_name.startswith(\"whisper\"):\n        size = request.model_name.split(\"-\")[-1]\n        asyncio.create_task(download_whisper_model(size))\n    \n    return {\"status\": \"downloading\"}\n```\n\n### Delete Model\n\n```python\n@app.delete(\"/models/{model_name}\")\nasync def delete_model(model_name: str):\n    # Find and delete from HuggingFace cache\n    cache_dir = Path.home() / \".cache\" / \"huggingface\" / \"hub\"\n    \n    model_dirs = list(cache_dir.glob(f\"models--*--{model_name}*\"))\n    for model_dir in model_dirs:\n        shutil.rmtree(model_dir)\n    \n    return {\"status\": \"deleted\"}\n```\n\n## API Endpoints\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| GET | `/models/status` | Get status of all models |\n| POST | `/models/load` | Load TTS model |\n| POST | `/models/unload` | Unload TTS model |\n| POST | `/models/download` | Trigger model download |\n| GET | `/models/progress/{name}` | Stream download progress (SSE) |\n| DELETE | `/models/{name}` | Delete downloaded model |\n| GET | `/tasks/active` | Get active downloads/generations |\n\n## Response Schemas\n\n### ModelStatus\n\n```json\n{\n  \"model_name\": \"qwen-tts-1.7B\",\n  \"display_name\": \"Qwen3-TTS 1.7B\",\n  \"downloaded\": true,\n  \"size_mb\": 3400,\n  \"loaded\": true\n}\n```\n\n### ActiveTasksResponse\n\n```json\n{\n  \"downloads\": [\n    {\n      \"model_name\": \"whisper-medium\",\n      \"status\": \"downloading\",\n      \"started_at\": \"2024-01-15T10:30:00Z\"\n    }\n  ],\n  \"generations\": [\n    {\n      \"task_id\": \"uuid\",\n      \"profile_id\": \"uuid\",\n      \"text_preview\": \"Hello world...\",\n      \"started_at\": \"2024-01-15T10:30:00Z\"\n    }\n  ]\n}\n```\n\n## Frontend Integration\n\n### Progress Display\n\n```typescript\n// Subscribe to download progress via SSE\nconst eventSource = new EventSource(`/models/progress/${modelName}`);\n\neventSource.onmessage = (event) => {\n  const progress = JSON.parse(event.data);\n  updateProgressBar(progress.current / progress.total);\n  \n  if (progress.status === 'complete') {\n    eventSource.close();\n  }\n};\n```\n\n### Model Status UI\n\n```typescript\n// Fetch model status\nconst { data: models } = useQuery({\n  queryKey: ['models', 'status'],\n  queryFn: () => api.getModelStatus(),\n});\n\n// Display download/load buttons based on status\nmodels.map(model => (\n  <ModelCard\n    name={model.display_name}\n    downloaded={model.downloaded}\n    loaded={model.loaded}\n    onDownload={() => triggerDownload(model.model_name)}\n    onLoad={() => loadModel(model.model_name)}\n  />\n));\n```\n\n## Error Handling\n\n| Error | Cause | Solution |\n|-------|-------|----------|\n| Download failed | Network issue | Retry download |\n| OOM on load | Model too large | Use smaller model |\n| Model not found | Cache corrupted | Re-download |\n| Slow download | HF rate limit | Wait and retry |\n"
  },
  {
    "path": "docs/content/docs/developer/setup.mdx",
    "content": "---\ntitle: \"Development Setup\"\ndescription: \"Set up your local development environment for Voicebox\"\n---\n\n## Quick Setup (Recommended)\n\nGet started in two commands:\n\n```bash\n# Clone and enter the repository\ngit clone https://github.com/jamiepine/voicebox.git\ncd voicebox\n\n# Setup everything (Python venv, JS deps, dev sidecar)\njust setup\n\n# Start development (backend + desktop app)\njust dev\n```\n\nThe `just dev` command automatically starts the Python backend (if not already running) and launches the Tauri desktop app.\n\n## Prerequisites\n\nEnsure you have these installed:\n\n<Cards>\n  <Card title=\"Bun\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"m7.5 4.27 9 5.15\"/><path d=\"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z\"/><path d=\"m3.3 7 8.7 5 8.7-5\"/><path d=\"M12 22V12\"/></svg>}>\n    [Download Bun](https://bun.sh)\n    ```bash\n    curl -fsSL https://bun.sh/install | bash\n    ```\n  </Card>\n  <Card title=\"Python 3.11+\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M12 2L2 7l10 5 10-5-10-5z\"/><path d=\"M2 17l10 5 10-5\"/><path d=\"M2 12l10 5 10-5\"/></svg>}>\n    [Download Python](https://python.org)\n    ```bash\n    python --version\n    ```\n  </Card>\n  <Card title=\"Rust\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>}>\n    [Install Rust](https://rustup.rs)\n    ```bash\n    rustc --version\n    ```\n  </Card>\n  <Card title=\"Just\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M4 7V4h3\"/><path d=\"M7 4h14v6h-2V6H7V4Z\"/><path d=\"M4 10v10h16V10H4Z\"/></svg>}>\n    [Install Just](https://github.com/casey/just)\n    ```bash\n    brew install just  # macOS\n    cargo install just # Linux/Windows\n    ```\n  </Card>\n</Cards>\n\n<Callout type=\"info\">\n  Just works on macOS, Linux, and Windows.\n</Callout>\n\n## Just Commands\n\nRun `just --list` to see all available commands:\n\n| Command | Description |\n|---------|-------------|\n| `just setup` | Full setup (Python venv + JS deps) |\n| `just dev` | Start backend + desktop app |\n| `just dev-web` | Start backend + web app (no Tauri) |\n| `just dev-backend` | Start backend only |\n| `just dev-frontend` | Start desktop app only (backend must be running) |\n| `just build` | Build desktop app for production |\n| `just build-web` | Build web app for production |\n| `just check` | Run all checks (JS + Python lint + format) |\n| `just fix` | Fix lint + format issues |\n| `just test` | Run Python tests |\n| `just db-init` | Initialize SQLite database |\n| `just db-reset` | Reset database (delete + reinit) |\n| `just clean` | Clean build artifacts |\n| `just clean-all` | Nuclear clean (includes node_modules) |\n\n## Project Structure\n\n<Files>\n  <Folder name=\"voicebox\" defaultOpen>\n    <Folder name=\"app\">\n      <Folder name=\"src\">\n        <File name=\"components/\" />\n        <File name=\"lib/\" />\n        <File name=\"hooks/\" />\n      </Folder>\n    </Folder>\n    <Folder name=\"backend\">\n      <File name=\"app.py\" />\n      <File name=\"main.py\" />\n      <File name=\"config.py\" />\n      <File name=\"models.py\" />\n      <File name=\"server.py\" />\n      <Folder name=\"routes\">\n        <File name=\"...\" />\n      </Folder>\n      <Folder name=\"services\">\n        <File name=\"...\" />\n      </Folder>\n      <Folder name=\"backends\">\n        <File name=\"...\" />\n      </Folder>\n      <Folder name=\"database\">\n        <File name=\"...\" />\n      </Folder>\n      <Folder name=\"utils\">\n        <File name=\"...\" />\n      </Folder>\n    </Folder>\n    <Folder name=\"tauri\">\n      <Folder name=\"src-tauri\" />\n    </Folder>\n    <Folder name=\"web\" />\n    <Folder name=\"scripts\" />\n  </Folder>\n</Files>\n\n### Request Flow\n\nHTTP request → **routes/** (validate input) → **services/** (business logic) → **backends/** (TTS/STT inference) → **utils/** (audio processing)\n\n### Key Modules\n\n- **app.py** — FastAPI app factory, CORS, lifecycle events\n- **main.py** — Entry point (imports app, runs uvicorn)\n- **server.py** — Tauri sidecar launcher, parent-pid watchdog\n- **services/generation.py** — Single function handling all generation modes\n- **backends/** — TTS/STT engine implementations (MLX, PyTorch, etc.)\n\n## Model Downloads\n\nModels are automatically downloaded from HuggingFace Hub on first use:\n\n- **Whisper** (transcription): Auto-downloads on first transcription\n- **Qwen3-TTS** (voice cloning): Auto-downloads on first generation (~2-4GB)\n\n<Callout type=\"warn\">\n  First-time usage will be slower due to model downloads, but subsequent runs will use cached models.\n</Callout>\n\n## Generate OpenAPI Client\n\nAfter starting the backend server, generate the TypeScript API client:\n\n```bash\njust generate-api\n```\n\nThis downloads the OpenAPI schema and generates the TypeScript client in `app/src/lib/api/`\n\n## Manual Setup (Advanced)\n\nIf you prefer not to use Just, follow these manual steps:\n\n### 1. Install JavaScript Dependencies\n\n```bash\nbun install\n```\n\nThis installs dependencies for:\n- `app/` - Shared React frontend\n- `tauri/` - Tauri desktop wrapper\n- `web/` - Web deployment wrapper\n\n### 2. Set Up Python Backend\n\n```bash\ncd backend\n\n# Create virtual environment\npython -m venv venv\n\n# Activate virtual environment\nsource venv/bin/activate  # macOS/Linux\n# or\nvenv\\Scripts\\activate  # Windows\n\n# Install Python dependencies\npip install -r requirements.txt\n\n# Apple Silicon: install MLX dependencies\npip install -r requirements-mlx.txt\n\n# Install Qwen3-TTS\npip install git+https://github.com/QwenLM/Qwen3-TTS.git\n```\n\n### 3. Start Development\n\nStart the backend:\n```bash\ncd backend\nsource venv/bin/activate\nuvicorn main:app --reload --port 17493\n```\n\nIn a new terminal, start the desktop app:\n```bash\ncd tauri\nbun run tauri dev\n```\n\n## Next Steps\n\n<Cards>\n  <Card title=\"Architecture\" href=\"/development/architecture\">\n    Understand the system architecture\n  </Card>\n  <Card title=\"Contributing\" href=\"/development/contributing\">\n    Read the contribution guidelines\n  </Card>\n  <Card title=\"Building\" href=\"/development/building\">\n    Learn how to build production releases\n  </Card>\n  <Card title=\"API Reference\" href=\"/api-reference\">\n    Explore the REST API\n  </Card>\n</Cards>\n\n## Troubleshooting\n\n<AccordionGroup>\n  <Accordion title=\"Backend won't start\">\n    - Check Python version (must be 3.11+)\n    - Ensure virtual environment is activated: `source backend/venv/bin/activate`\n    - Verify all dependencies are installed: `pip install -r requirements.txt`\n    - Check if port 17493 is available\n  </Accordion>\n\n  <Accordion title=\"Tauri build fails\">\n    - Ensure Rust is installed: `rustc --version`\n    - Clean the build: `cd tauri/src-tauri && cargo clean`\n    - Try rebuilding: `just dev`\n  </Accordion>\n\n  <Accordion title=\"OpenAPI client generation fails\">\n    - Ensure backend is running: `curl http://localhost:17493/openapi.json`\n    - Check network connectivity\n    - Verify the backend is accessible at localhost:17493\n  </Accordion>\n</AccordionGroup>\n\nSee the full [Troubleshooting Guide](/overview/troubleshooting) for more issues and solutions.\n"
  },
  {
    "path": "docs/content/docs/developer/stories.mdx",
    "content": "---\ntitle: \"Stories & Timeline\"\ndescription: \"How the multi-voice timeline editor works in Voicebox\"\n---\n\n## Overview\n\nStories allow users to arrange multiple voice generations on a timeline to create multi-voice narratives. The system supports tracks, trimming, splitting, and audio mixing.\n\n## Architecture\n\n**Story:** A container that holds story items with metadata.\n\n**Story Item:** Links a generation to a story with timeline position, track, and trim data.\n\n**Export:** Combines all items into a single mixed audio file.\n\n## Data Model\n\n### Story Table\n\n```python\nclass Story(Base):\n    __tablename__ = \"stories\"\n    \n    id = Column(String, primary_key=True)\n    name = Column(String, nullable=False)\n    description = Column(Text)\n    created_at = Column(DateTime)\n    updated_at = Column(DateTime)\n```\n\n### StoryItem Table\n\n```python\nclass StoryItem(Base):\n    __tablename__ = \"story_items\"\n    \n    id = Column(String, primary_key=True)\n    story_id = Column(String, ForeignKey(\"stories.id\"))\n    generation_id = Column(String, ForeignKey(\"generations.id\"))\n    start_time_ms = Column(Integer, default=0)  # Timeline position\n    track = Column(Integer, default=0)          # Track number\n    trim_start_ms = Column(Integer, default=0)  # Trim from start\n    trim_end_ms = Column(Integer, default=0)    # Trim from end\n    created_at = Column(DateTime)\n```\n\n## Timeline Concepts\n\n### Start Time\n\n`start_time_ms` defines when an item begins on the timeline:\n\n```\nTimeline (ms):  0----1000----2000----3000----4000\nItem 1:         [======]\nItem 2:                      [==========]\nItem 3:                [====]\n```\n\n### Tracks\n\nMultiple tracks allow overlapping audio:\n\n```\nTrack 0:  [Item 1]        [Item 3]\nTrack 1:            [Item 2]\n```\n\n### Trimming\n\nTrim values cut audio from the start or end without destroying the original:\n\n```\nOriginal:    [=========AUDIO=========]\ntrim_start:   ^^\ntrim_end:                           ^^\nResult:        [=====AUDIO=====]\n```\n\n## Core Operations\n\n### Adding Items\n\nWhen adding a generation to a story:\n\n```python\nasync def add_item_to_story(\n    story_id: str,\n    data: StoryItemCreate,\n    db: Session,\n) -> StoryItemDetail:\n    # Calculate start time if not provided\n    if data.start_time_ms is None:\n        # Find the end of all existing items\n        existing_items = get_items_with_durations(story_id, db)\n        max_end_time_ms = max(\n            item.start_time_ms + int(gen.duration * 1000)\n            for item, gen in existing_items\n        )\n        start_time_ms = max_end_time_ms + 200  # 200ms gap\n    \n    # Create the item\n    item = DBStoryItem(\n        id=str(uuid.uuid4()),\n        story_id=story_id,\n        generation_id=data.generation_id,\n        start_time_ms=start_time_ms,\n        track=data.track or 0,\n    )\n    db.add(item)\n    db.commit()\n```\n\n### Moving Items\n\nUpdate position and/or track:\n\n```python\nasync def move_story_item(\n    story_id: str,\n    item_id: str,\n    data: StoryItemMove,\n    db: Session,\n) -> StoryItemDetail:\n    item = get_item(story_id, item_id, db)\n    \n    item.start_time_ms = data.start_time_ms\n    item.track = data.track\n    \n    db.commit()\n```\n\n### Trimming Items\n\nNon-destructive trimming:\n\n```python\nasync def trim_story_item(\n    story_id: str,\n    item_id: str,\n    data: StoryItemTrim,\n    db: Session,\n) -> StoryItemDetail:\n    item = get_item(story_id, item_id, db)\n    generation = get_generation(item.generation_id, db)\n    \n    # Validate trim doesn't exceed duration\n    max_duration_ms = int(generation.duration * 1000)\n    if data.trim_start_ms + data.trim_end_ms >= max_duration_ms:\n        return None  # Invalid trim\n    \n    item.trim_start_ms = data.trim_start_ms\n    item.trim_end_ms = data.trim_end_ms\n    \n    db.commit()\n```\n\n### Splitting Items\n\nSplit one item into two at a specific time:\n\n```python\nasync def split_story_item(\n    story_id: str,\n    item_id: str,\n    data: StoryItemSplit,\n    db: Session,\n) -> List[StoryItemDetail]:\n    item = get_item(story_id, item_id, db)\n    generation = get_generation(item.generation_id, db)\n    \n    # Calculate split point\n    current_trim_start = item.trim_start_ms\n    current_trim_end = item.trim_end_ms\n    original_duration_ms = int(generation.duration * 1000)\n    absolute_split_ms = current_trim_start + data.split_time_ms\n    \n    # Update original: trim from end\n    item.trim_end_ms = original_duration_ms - absolute_split_ms\n    \n    # Create new item: trim from start\n    new_item = DBStoryItem(\n        generation_id=item.generation_id,  # Same generation\n        start_time_ms=item.start_time_ms + data.split_time_ms,\n        track=item.track,\n        trim_start_ms=absolute_split_ms,\n        trim_end_ms=current_trim_end,\n    )\n    \n    db.add(new_item)\n    db.commit()\n    \n    return [item, new_item]\n```\n\n### Duplicating Items\n\nCreate a copy with all properties:\n\n```python\nasync def duplicate_story_item(\n    story_id: str,\n    item_id: str,\n    db: Session,\n) -> StoryItemDetail:\n    original = get_item(story_id, item_id, db)\n    generation = get_generation(original.generation_id, db)\n    \n    # Calculate effective duration for positioning\n    effective_duration_ms = (\n        int(generation.duration * 1000) \n        - original.trim_start_ms \n        - original.trim_end_ms\n    )\n    \n    # Place copy after original with 200ms gap\n    new_item = DBStoryItem(\n        generation_id=original.generation_id,\n        start_time_ms=original.start_time_ms + effective_duration_ms + 200,\n        track=original.track,\n        trim_start_ms=original.trim_start_ms,\n        trim_end_ms=original.trim_end_ms,\n    )\n    \n    db.add(new_item)\n    db.commit()\n```\n\n## Audio Export\n\n### Mixing Algorithm\n\nThe export function mixes all items into a single audio file:\n\n```python\nasync def export_story_audio(story_id: str, db: Session) -> bytes:\n    items = get_all_items_with_generations(story_id, db)\n    \n    # Calculate total duration\n    max_end_time_ms = max(\n        data['start_time_ms'] + data['duration_ms']\n        for data in audio_data\n    )\n    \n    # Create output buffer\n    total_samples = int((max_end_time_ms / 1000.0) * sample_rate)\n    final_audio = np.zeros(total_samples, dtype=np.float32)\n    \n    # Mix each item at its position\n    for data in audio_data:\n        audio = data['audio']\n        start_sample = int((data['start_time_ms'] / 1000.0) * sample_rate)\n        \n        # Apply trim\n        trimmed_audio = audio[trim_start_sample:len(audio) - trim_end_sample]\n        \n        # Add to buffer (overlapping items sum together)\n        final_audio[start_sample:start_sample + len(trimmed_audio)] += trimmed_audio\n    \n    # Normalize to prevent clipping\n    max_val = np.abs(final_audio).max()\n    if max_val > 1.0:\n        final_audio = final_audio / max_val\n    \n    return audio_to_bytes(final_audio, sample_rate)\n```\n\n## API Endpoints\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| GET | `/stories` | List all stories |\n| POST | `/stories` | Create a story |\n| GET | `/stories/{id}` | Get story with items |\n| PUT | `/stories/{id}` | Update story metadata |\n| DELETE | `/stories/{id}` | Delete story |\n| POST | `/stories/{id}/items` | Add item to story |\n| DELETE | `/stories/{id}/items/{item_id}` | Remove item |\n| PUT | `/stories/{id}/items/{item_id}/move` | Move item |\n| PUT | `/stories/{id}/items/{item_id}/trim` | Trim item |\n| POST | `/stories/{id}/items/{item_id}/split` | Split item |\n| POST | `/stories/{id}/items/{item_id}/duplicate` | Duplicate item |\n| PUT | `/stories/{id}/items/times` | Batch update times |\n| PUT | `/stories/{id}/items/reorder` | Reorder items |\n| GET | `/stories/{id}/export-audio` | Export mixed audio |\n\n## Response Schemas\n\n### StoryItemDetail\n\n```json\n{\n  \"id\": \"item_uuid\",\n  \"story_id\": \"story_uuid\",\n  \"generation_id\": \"generation_uuid\",\n  \"start_time_ms\": 1500,\n  \"track\": 0,\n  \"trim_start_ms\": 200,\n  \"trim_end_ms\": 100,\n  \"profile_id\": \"profile_uuid\",\n  \"profile_name\": \"Narrator\",\n  \"text\": \"Hello world\",\n  \"audio_path\": \"/path/to/audio.wav\",\n  \"duration\": 2.5,\n  \"created_at\": \"2024-01-15T10:30:00Z\"\n}\n```\n\n## Frontend Integration\n\nThe timeline UI needs to:\n\n1. **Fetch story** with all items\n2. **Render waveforms** for each item\n3. **Handle drag/drop** to move items\n4. **Handle edge drag** for trimming\n5. **Sync playhead** across all tracks\n6. **Export** when user clicks download\n"
  },
  {
    "path": "docs/content/docs/developer/transcription.mdx",
    "content": "---\ntitle: \"Transcription\"\ndescription: \"How Whisper-based audio transcription works in Voicebox\"\n---\n\n## Overview\n\nVoicebox uses OpenAI's Whisper model for automatic speech recognition (ASR). This powers the transcription feature for creating reference text from audio recordings.\n\n## Architecture\n\nThe transcription system is built around the `WhisperModel` class:\n\n**Model Loading:** Lazy loading with HuggingFace Hub download.\n\n**Audio Processing:** Resampling and preprocessing for Whisper.\n\n**Inference:** Running transcription with optional language hints.\n\n## WhisperModel Class\n\n```python\nclass WhisperModel:\n    def __init__(self, model_size: str = \"base\"):\n        self.model = None\n        self.processor = None\n        self.model_size = model_size\n        self.device = self._get_device()\n```\n\n### Model Sizes\n\n| Size | Parameters | VRAM | Speed | Quality |\n|------|------------|------|-------|---------|\n| tiny | 39M | ~1GB | Fastest | Basic |\n| base | 74M | ~1GB | Fast | Good |\n| small | 244M | ~2GB | Medium | Better |\n| medium | 769M | ~5GB | Slow | High |\n| large | 1550M | ~10GB | Slowest | Best |\n\nDefault is `base` for balance of speed and quality.\n\n## Model Loading\n\nModels are downloaded from HuggingFace Hub:\n\n```python\ndef load_model(self, model_size: Optional[str] = None):\n    from transformers import WhisperProcessor, WhisperForConditionalGeneration\n    \n    model_name = f\"openai/whisper-{model_size}\"\n    \n    # Track download progress\n    progress_manager = get_progress_manager()\n    task_manager = get_task_manager()\n    task_manager.start_download(f\"whisper-{model_size}\")\n    \n    # Load processor and model\n    with tracker.patch_download():\n        self.processor = WhisperProcessor.from_pretrained(model_name)\n        self.model = WhisperForConditionalGeneration.from_pretrained(model_name)\n    \n    self.model.to(self.device)\n    \n    # Mark complete\n    progress_manager.mark_complete(f\"whisper-{model_size}\")\n    task_manager.complete_download(f\"whisper-{model_size}\")\n```\n\n### Async Loading\n\nLike TTS, loading runs in a thread pool:\n\n```python\nasync def load_model_async(self, model_size: Optional[str] = None):\n    if self.model is not None and self.model_size == model_size:\n        return\n    await asyncio.to_thread(self.load_model, model_size)\n```\n\n## Transcription\n\n### Basic Transcription\n\n```python\nasync def transcribe(\n    self,\n    audio_path: str,\n    language: Optional[str] = None,\n) -> str:\n    await self.load_model_async()\n    \n    def _transcribe_sync():\n        # Load and resample to 16kHz (Whisper requirement)\n        audio, sr = load_audio(audio_path, sample_rate=16000)\n        \n        # Process audio\n        inputs = self.processor(\n            audio,\n            sampling_rate=16000,\n            return_tensors=\"pt\",\n        )\n        inputs = inputs.to(self.device)\n        \n        # Set language hint if provided\n        forced_decoder_ids = None\n        if language:\n            forced_decoder_ids = self.processor.get_decoder_prompt_ids(\n                language=language,\n                task=\"transcribe\",\n            )\n        \n        # Generate\n        with torch.no_grad():\n            predicted_ids = self.model.generate(\n                inputs[\"input_features\"],\n                forced_decoder_ids=forced_decoder_ids,\n            )\n        \n        # Decode\n        transcription = self.processor.batch_decode(\n            predicted_ids,\n            skip_special_tokens=True,\n        )[0]\n        \n        return transcription.strip()\n    \n    return await asyncio.to_thread(_transcribe_sync)\n```\n\n### Supported Languages\n\nWhisper supports 99+ languages. Common ones in Voicebox:\n\n| Code | Language |\n|------|----------|\n| en | English |\n| zh | Chinese |\n| ja | Japanese |\n| ko | Korean |\n| de | German |\n| fr | French |\n| ru | Russian |\n| pt | Portuguese |\n| es | Spanish |\n| it | Italian |\n\n### Language Detection\n\nWhen no language is specified, Whisper auto-detects:\n\n```python\n# Without language hint - auto-detect\ntranscription = await whisper.transcribe(audio_path)\n\n# With language hint - more accurate for short clips\ntranscription = await whisper.transcribe(audio_path, language=\"en\")\n```\n\n## Transcription with Timestamps\n\nFor advanced use cases, word-level timestamps are available:\n\n```python\nasync def transcribe_with_timestamps(\n    self,\n    audio_path: str,\n    language: Optional[str] = None,\n) -> List[Dict[str, any]]:\n    await self.load_model_async()\n    \n    def _transcribe_timestamps_sync():\n        audio, sr = load_audio(audio_path, sample_rate=16000)\n        inputs = self.processor(audio, sampling_rate=16000, return_tensors=\"pt\")\n        \n        with torch.no_grad():\n            predicted_ids = self.model.generate(\n                inputs[\"input_features\"],\n                return_timestamps=True,\n            )\n        \n        # Parse timestamps\n        return [\n            {\n                \"text\": transcription,\n                \"start\": 0.0,\n                \"end\": len(audio) / sr,\n            }\n        ]\n    \n    return await asyncio.to_thread(_transcribe_timestamps_sync)\n```\n\n## Memory Management\n\n### Unloading\n\nFree memory when not needed:\n\n```python\ndef unload_model(self):\n    if self.model is not None:\n        del self.model\n        del self.processor\n        self.model = None\n        self.processor = None\n        \n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n```\n\n### Global Instance\n\nA singleton pattern manages the model:\n\n```python\n_whisper_model: Optional[WhisperModel] = None\n\ndef get_whisper_model() -> WhisperModel:\n    global _whisper_model\n    if _whisper_model is None:\n        _whisper_model = WhisperModel()\n    return _whisper_model\n```\n\n## Audio Preprocessing\n\n### Resampling\n\nWhisper requires 16kHz audio:\n\n```python\naudio, sr = load_audio(audio_path, sample_rate=16000)\n```\n\n### Format Support\n\nThe `load_audio` utility handles:\n- WAV\n- MP3\n- FLAC\n- OGG\n- M4A\n\nAll formats are converted to mono 16kHz.\n\n## API Endpoints\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| POST | `/transcribe` | Transcribe audio file |\n\n### Request\n\nMultipart form data:\n\n```\nPOST /transcribe\nContent-Type: multipart/form-data\n\nfile: <audio_file>\nlanguage: en  (optional)\n```\n\n### Response\n\n```json\n{\n  \"text\": \"Hello, this is a test transcription.\",\n  \"duration\": 3.5\n}\n```\n\n## Use Cases\n\n### Reference Text for Voice Cloning\n\n1. User records audio sample\n2. Audio is sent to `/transcribe`\n3. Transcription becomes `reference_text`\n4. Both are added to voice profile\n\n### Quality Tips\n\n- Provide language hint for short audio\n- Use clean audio with minimal noise\n- Longer audio (>5s) improves accuracy\n- Consider `small` or `medium` model for better quality\n\n## Error Handling\n\nCommon issues:\n\n| Error | Cause | Solution |\n|-------|-------|----------|\n| Model not found | First run, download failed | Retry with network |\n| OOM | Model too large | Use smaller model |\n| Empty result | No speech detected | Check audio has speech |\n| Wrong language | Auto-detect failed | Provide language hint |\n"
  },
  {
    "path": "docs/content/docs/developer/tts-engines.mdx",
    "content": "---\ntitle: \"TTS Engines\"\ndescription: \"How to add new text-to-speech engines to Voicebox\"\n---\n\n> **For humans:** This doc is optimized for AI agents to implement new TTS engines autonomously. It's structured as a phased workflow with explicit gates and a checklist so an agent can do the full integration — dependency research, backend, frontend, bundling — and hand you a draft release or prod build to test locally. It's also a useful reference if you're doing it yourself.\n\nAdding an engine touches ~10 files across 4 layers. The backend protocol work is straightforward — the real time sink is dependency hell, upstream library bugs, and PyInstaller bundling.\n\n**Do not start writing code until you complete Phase 0.** The v0.2.3 release was three patch releases of PyInstaller fixes because dependency research was skipped. Every issue — `inspect.getsource()` failures, missing native data files, metadata lookups, dtype mismatches — was discoverable by reading the model library's source code before integration began.\n\n## Architecture Overview\n\nThe backend is split into layers:\n\n| Layer | Purpose | Files Touched |\n|-------|---------|---------------|\n| `routes/` | Thin HTTP handlers | None (auto-dispatch) |\n| `services/` | Business logic | None (auto-dispatch) |\n| `backends/` | Engine implementations | `your_engine_backend.py` |\n| `utils/` | Shared utilities | As needed |\n\nNew engines only need to touch `backends/` and `models.py` on the backend side — the route and service layers use a model config registry that handles dispatch automatically.\n\n## Phase 0: Dependency Research\n\n**This phase is mandatory.** Clone the model library and its key dependencies into a temporary directory and inspect them before writing any integration code. The goal is to produce a dependency audit that identifies every PyInstaller-incompatible pattern, every native data file, and every upstream bug you'll need to work around.\n\n### 0.1 Clone and Inspect the Model Library\n\n```bash\n# Create a throwaway workspace\nmkdir /tmp/engine-research && cd /tmp/engine-research\n\n# Clone the model library\ngit clone https://github.com/org/model-library.git\ncd model-library\n```\n\n**Read these files first, in order:**\n\n1. **`setup.py` / `setup.cfg` / `pyproject.toml`** — Check pinned dependency versions. If the library pins `torch==2.6.0` or `numpy<1.26`, you'll need `--no-deps` installation and manual sub-dependency listing (this is what happened with `chatterbox-tts`).\n\n2. **`__init__.py` and the main model class** — Trace the import chain. Look for:\n   - `from_pretrained()` — does it call `huggingface_hub` internally? Does it pass `token=True` (which crashes without a stored HF token)?\n   - `from_local()` — does it exist? You may need manual `snapshot_download()` + `from_local()` to bypass download bugs.\n   - Device handling — does it default to CUDA? Does it support MPS? Many libraries crash on MPS with unsupported operators.\n\n3. **All `import` statements** — Recursively trace what the library imports. You're looking for:\n   - `inspect.getsource()` anywhere in the chain (search all `.py` files)\n   - `typeguard` / `@typechecked` decorators (these call `inspect.getsource()` at import time)\n   - `importlib.metadata.version()` or `pkg_resources.get_distribution()` (need `--copy-metadata`)\n   - `lazy_loader` (needs `--collect-all` to bundle `.pyi` stubs)\n\n### 0.2 Scan for PyInstaller-Incompatible Patterns\n\nRun these searches against the cloned library **and** its transitive dependencies:\n\n```bash\n# inspect.getsource — will crash in frozen binary without --collect-all\ngrep -r \"inspect.getsource\\|getsource(\" .\n\n# typeguard / @typechecked — calls inspect.getsource at import time\ngrep -r \"@typechecked\\|from typeguard\" .\n\n# importlib.metadata — needs --copy-metadata\ngrep -r \"importlib.metadata\\|pkg_resources.get_distribution\\|pkg_resources.require\" .\n\n# Data files loaded at runtime — need --collect-all or --collect-data\ngrep -r \"Path(__file__).parent\\|os.path.dirname(__file__)\\|resources_path\\|pkg_resources.resource_filename\" .\n\n# Native library paths — may need env var override in frozen builds\ngrep -r \"/usr/share\\|/usr/lib\\|/usr/local\\|espeak\\|phonemize\" .\n\n# torch.load without map_location — will crash on CPU-only builds\ngrep -r \"torch.load(\" . | grep -v \"map_location\"\n\n# HuggingFace token bugs\ngrep -r 'token=True\\|token=os.getenv' .\n\n# Float64/Float32 assumptions — librosa returns float64, many models assume float32\ngrep -r \"torch.from_numpy\\|\\.double()\\|float64\" .\n\n# @torch.jit.script — calls inspect.getsource(), crashes in frozen builds\ngrep -r \"@torch.jit.script\\|torch.jit.script\" .\n\n# torchaudio.load — requires torchcodec in torchaudio 2.10+, use soundfile.read() instead\ngrep -r \"torchaudio.load\\|torchaudio.save\" .\n\n# Gated HuggingFace repos — models that hardcode gated repos as tokenizer/config sources\ngrep -r \"from_pretrained\\|tokenizer_name\\|AutoTokenizer\" . | grep -i \"llama\\|meta-llama\\|gated\"\n```\n\n### 0.3 Install and Trace in a Throwaway Venv\n\n```bash\n# Create isolated venv\npython -m venv /tmp/engine-venv\nsource /tmp/engine-venv/bin/activate\n\n# Install the package (try normally first)\npip install model-package\n\n# Check if it conflicts with our stack\npip install model-package torch==2.10 transformers==4.57.3 numpy>=1.26\n# If this fails, you need --no-deps:\npip install --no-deps model-package\n\n# Get the full dependency tree\npip show model-package  # Check Requires: field\npip show -f model-package  # List all installed files (look for data files)\n\n# Check for non-PyPI dependencies\npip install model-package 2>&1 | grep -i \"no matching distribution\"\n```\n\n### 0.4 Test Model Loading on CPU\n\nBefore writing any integration code, verify the model works on CPU in a plain Python script:\n\n```python\nimport torch\n# Force CPU to catch map_location bugs early\nmodel = ModelClass.from_pretrained(\"org/model\", device=\"cpu\")\n\n# Test with a float32 audio array (not float64)\nimport numpy as np\naudio = np.random.randn(16000).astype(np.float32)\noutput = model.generate(\"Hello world\", audio)\nprint(f\"Output shape: {output.shape}, dtype: {output.dtype}, sample rate: {model.sample_rate}\")\n```\n\nIf this crashes, you've found a bug you'll need to monkey-patch. Common ones:\n- `RuntimeError: expected scalar type Float but found Double` → needs float32 cast\n- `RuntimeError: map_location` → needs `torch.load` patch\n- `RuntimeError: Unsupported operator aten::...` → needs MPS skip\n\n### 0.5 Produce a Dependency Audit\n\nBefore proceeding to Phase 1, write down:\n\n1. **PyPI vs non-PyPI deps** — which packages need `--find-links`, `git+https://`, or `--no-deps`?\n2. **PyInstaller directives needed** — which packages need `--collect-all`, `--copy-metadata`, `--hidden-import`?\n3. **Runtime data files** — which packages ship data files (YAML, pretrained weights, phoneme tables, shader libraries) that must be bundled?\n4. **Native library paths** — which packages look for data at system paths that won't exist in a frozen binary?\n5. **Monkey-patches needed** — `torch.load` map_location, float64→float32 casts, MPS skip, HF token bypass, etc.\n6. **Sample rate** — what does the engine output? (24kHz, 44.1kHz, 48kHz)\n7. **Model download method** — `from_pretrained()` with library-managed download, or manual `snapshot_download()` + `from_local()`?\n\nThis audit becomes your implementation plan for Phases 1, 4, and 5.\n\n## Phase 1: Backend Implementation\n\n### 1.1 Create the Backend File\n\nCreate `backend/backends/<engine>_backend.py` (~200-300 lines) implementing the `TTSBackend` protocol:\n\n```python\nclass YourBackend:\n    \"\"\"Must satisfy the TTSBackend protocol.\"\"\"\n\n    async def load_model(self, model_size: str = \"default\") -> None: ...\n    async def create_voice_prompt(self, audio_path: str, reference_text: str, use_cache: bool = True) -> tuple[dict, bool]: ...\n    async def combine_voice_prompts(self, audio_paths: list[str], ref_texts: list[str]) -> tuple[np.ndarray, str]: ...\n    async def generate(self, text: str, voice_prompt: dict, language: str = \"en\", seed: int | None = None, instruct: str | None = None) -> tuple[np.ndarray, int]: ...\n    def unload_model(self) -> None: ...\n    def is_loaded(self) -> bool: ...\n    def _get_model_path(self, model_size: str) -> str: ...\n```\n\n**Key decisions per engine:**\n\n| Decision | Options | Examples |\n|----------|---------|---------|\n| **Voice prompt storage** | Pre-computed tensors vs deferred file paths | Qwen stores tensor dicts; Chatterbox stores paths |\n| **Caching** | Use voice prompt cache or skip it | LuxTTS caches with prefix; Chatterbox skips caching |\n| **Device selection** | CUDA / MPS / CPU | Chatterbox forces CPU on macOS (MPS bugs) |\n| **Model download** | Library handles it vs manual `snapshot_download` | Turbo uses manual download to bypass `token=True` bug |\n| **Sample rate** | Engine-specific | LuxTTS outputs 48kHz, everything else is 24kHz |\n\n### 1.2 Voice Prompt Patterns\n\n**Pattern A: Pre-computed tensors** (Qwen, LuxTTS)\n```python\nencoded = model.encode_prompt(audio_path)\nreturn encoded, False  # (prompt_dict, was_cached)\n```\n\n**Pattern B: Deferred file paths** (Chatterbox, MLX)\n```python\nreturn {\"ref_audio\": audio_path, \"ref_text\": reference_text}, False\n```\n\n**Pattern C: Hybrid** (possible for new engines)\n```python\nembedding = model.extract_speaker(audio_path)\nreturn {\"embedding\": embedding, \"ref_audio\": audio_path}, False\n```\n\nIf caching, prefix your cache keys:\n```python\ncache_key = \"yourengine_\" + get_cache_key(audio_path, reference_text)\n```\n\n### 1.3 Register the Engine\n\nIn `backend/backends/__init__.py`:\n\n**Add a `ModelConfig` entry:**\n\n```python\nModelConfig(\n    model_name=\"your-engine\",\n    display_name=\"Your Engine\",\n    engine=\"your_engine\",\n    hf_repo_id=\"org/model-repo\",\n    size_mb=3200,\n    needs_trim=False,  # set True if output needs trim_tts_output()\n    languages=[\"en\", \"fr\", \"de\"],\n),\n```\n\n**Add to `TTS_ENGINES` dict:**\n\n```python\nTTS_ENGINES = {\n    ...\n    \"your_engine\": \"Your Engine\",\n}\n```\n\n**Add factory branch:**\n\n```python\nelif engine == \"your_engine\":\n    from .your_backend import YourBackend\n    backend = YourBackend()\n```\n\n### 1.4 Update Request Models\n\nIn `backend/models.py`:\n- Add engine name to `GenerationRequest.engine` regex pattern\n- Add any new language codes to the language regex\n\n## Phase 2: Route and Service Integration\n\nWith the model config registry, route and service layers have **zero per-engine dispatch points**. All endpoints use registry helpers like `get_model_config()`, `load_engine_model()`, `engine_needs_trim()`, `check_model_loaded()`, etc.\n\n**You don't need to touch any route or service files** unless your engine needs custom behavior in the generate pipeline.\n\n### Post-Processing\n\nIf your model produces trailing silence, set `needs_trim=True` on your `ModelConfig`. The generation service applies `trim_tts_output()` automatically.\n\n## Phase 3: Frontend Integration\n\n### 3.1 TypeScript Types\n\nIn `app/src/lib/api/types.ts`:\n- Add to the `engine` union type on `GenerationRequest`\n\n### 3.2 Language Maps\n\nIn `app/src/lib/constants/languages.ts`:\n- Add entry to `ENGINE_LANGUAGES` record\n- Add any new language codes to `ALL_LANGUAGES` if needed\n\n### 3.3 Engine/Model Selector\n\nIn `app/src/components/Generation/EngineModelSelector.tsx`:\n- Add entry to `ENGINE_OPTIONS` and `ENGINE_DESCRIPTIONS`\n- Add to `ENGLISH_ONLY_ENGINES` if applicable\n\n### 3.4 Form Hook\n\nIn `app/src/lib/hooks/useGenerationForm.ts`:\n- Add to Zod schema enum for `engine`\n- Add engine-to-model-name mapping\n- Update payload construction for engine-specific fields\n\n**Watch out for model naming inconsistencies.** The HuggingFace repo name, the model size label, and the API model name don't always follow predictable patterns. For example, TADA's 3B model is named `tada-3b-ml` (not `tada-3b`), because it's a multilingual variant. Always check the actual repo names and build the frontend model name mapping from those, not from assumptions like `{engine}-{size}`.\n\n### 3.5 Model Management\n\nIn `app/src/components/ServerSettings/ModelManagement.tsx`:\n- Add description to `MODEL_DESCRIPTIONS` record\n- Add model name to `voiceModels` filter condition\n\n### 3.6 Non-Cloning Engines (Preset Voices)\n\nIf your engine uses **pre-built voices** instead of zero-shot cloning from reference audio (e.g. Kokoro), additional integration is needed:\n\n**Backend:**\n- In `kokoro_backend.py` (or your engine), define a `VOICES` list of `(voice_id, display_name, gender, language)` tuples\n- `create_voice_prompt()` should return `{\"voice_type\": \"preset\", \"preset_engine\": \"<engine>\", \"preset_voice_id\": \"<id>\"}`\n- `generate()` should read `voice_prompt.get(\"preset_voice_id\")` to select the voice\n- Add a `seed_preset_profiles(\"<engine>\")` call in `backend/routes/models.py` after model download completes\n- The `seed_preset_profiles()` function in `backend/services/profiles.py` creates DB profiles with `voice_type=\"preset\"`\n\n**Frontend:**\n- The `EngineModelSelector` filters options based on `selectedProfile.voice_type`:\n  - `\"cloned\"` profiles → only cloning engines shown (Kokoro hidden)\n  - `\"preset\"` profiles → only the preset's engine shown\n- Profile cards show the engine name as a badge for preset profiles\n- When a preset profile is selected, the engine auto-switches\n\n**Profile schema fields for presets:**\n- `voice_type: \"preset\"` (vs `\"cloned\"` for traditional profiles)\n- `preset_engine: \"<engine>\"` — which engine owns this voice\n- `preset_voice_id: \"<id>\"` — the engine-specific voice identifier\n\n**For future \"designed\" voices** (text description instead of audio, e.g. Qwen CustomVoice):\n- Use `voice_type: \"designed\"` with `design_prompt` field\n- `create_voice_prompt_for_profile()` already returns the design prompt for this type\n\n## Phase 4: Dependencies\n\nUse the dependency audit from Phase 0 to drive this phase. You should already know what packages are needed, which conflict, and which require special installation.\n\n### 4.1 Python Dependencies\n\nAdd to `backend/requirements.txt`. There are three installation patterns, depending on what Phase 0 revealed:\n\n**Normal PyPI packages:**\n```\nsome-model-package>=1.0.0\n```\n\n**Pinned dependency conflicts (`--no-deps`)** — If the model package pins old versions of torch/numpy/transformers, install with `--no-deps` and list sub-dependencies manually. This is the pattern used for `chatterbox-tts`:\n```bash\n# In justfile / CI setup:\npip install --no-deps chatterbox-tts\n\n# In requirements.txt — list each actual sub-dependency:\nconformer>=0.3.2\ndiffusers>=0.31.0\nomegaconf>=2.3.0\nresemble-perth>=0.0.2\ns3tokenizer>=0.1.6\n```\n\nTo identify sub-deps: `pip show chatterbox-tts` → `Requires:` field, then cross-reference against existing `requirements.txt` to avoid duplicates.\n\n**Non-PyPI packages** — Some libraries only exist on GitHub or require custom indexes:\n```\n# Git-only packages (no PyPI release)\nlinacodec @ git+https://github.com/ysharma3501/LinaCodec.git\nZipvoice @ git+https://github.com/ysharma3501/LuxTTS.git\n\n# Custom package indexes (C extensions with platform-specific wheels)\n--find-links https://k2-fsa.github.io/icefall/piper_phonemize.html\npiper-phonemize>=1.2.0\n```\n\n### 4.2 Dependency Conflict Resolution\n\nCheck for conflicts with the existing stack before adding anything:\n\n```bash\n# Our current stack pins (approximate):\n# Python 3.12+, torch>=2.10, transformers>=4.57, numpy>=1.26\n\n# Test compatibility\npip install model-package torch==2.10 transformers==4.57.3 numpy>=1.26\n\n# If it fails, check what the package pins:\npip show model-package | grep Requires\n# Look at setup.py/pyproject.toml for version constraints\n```\n\n**Known incompatible patterns in the wild:**\n- `torch==2.6.0` — many older packages pin this\n- `numpy<1.26` — conflicts with Python 3.12+\n- `transformers==4.46.3` — many packages pin old transformers\n- `onnxruntime` pinned versions — often conflict with torch\n\n### 4.3 Update Installation Scripts\n\nDependencies must be added in multiple places:\n\n| File | What to add |\n|------|------------|\n| `backend/requirements.txt` | Package and version constraint |\n| `justfile` | `--no-deps` install line if needed (in `setup-python` and `setup-python-release` targets) |\n| `.github/workflows/release.yml` | Same `--no-deps` line in CI build steps |\n| `Dockerfile` | Same install commands for Docker builds |\n\n## Phase 5: PyInstaller Bundling (`build_binary.py`)\n\nThis is where most of the pain lives. **The v0.2.3 release was entirely dedicated to fixing bundling issues** — every new engine that shipped in v0.2.1 (LuxTTS, Chatterbox, Chatterbox Turbo) worked in dev but failed in production builds. Don't skip this phase.\n\n### 5.1 Register Your Engine in `build_binary.py`\n\nEvery new engine needs entries in `backend/build_binary.py`. This file drives PyInstaller and is the single most common source of \"works in dev, breaks in prod\" bugs. You need to decide which PyInstaller directives your engine's dependencies require:\n\n| Directive | What It Does | When You Need It |\n|-----------|-------------|-----------------|\n| `--hidden-import <module>` | Includes a module PyInstaller can't detect via static analysis | Dynamic imports, lazy imports, plugin architectures |\n| `--collect-all <package>` | Bundles source `.py` files, data files, AND native libraries | Packages that call `inspect.getsource()` at import time (e.g. `inflect` via `typeguard`'s `@typechecked`), or that ship pretrained model files (e.g. `perth` ships `.pth.tar` + `hparams.yaml`) |\n| `--collect-data <package>` | Bundles only data files (not source or native libs) | Packages with YAML configs, vocab files, etc. |\n| `--collect-submodules <package>` | Bundles all submodules | Packages with deep module trees that PyInstaller misses |\n| `--copy-metadata <package>` | Copies `importlib.metadata` info | Packages that call `importlib.metadata.version()` or `pkg_resources.get_distribution()` at runtime. Already required for: `requests`, `transformers`, `huggingface-hub`, `tokenizers`, `safetensors`, `tqdm` |\n\n**Example: adding hidden imports and collect-all for a new engine:**\n\n```python\n# In build_binary.py, inside the args list:\n\"--hidden-import\",\n\"backend.backends.your_engine_backend\",\n\"--hidden-import\",\n\"your_engine_package\",\n\"--hidden-import\",\n\"your_engine_package.inference\",\n\"--collect-all\",\n\"some_dependency_that_uses_inspect_getsource\",\n\"--copy-metadata\",\n\"some_dependency_that_checks_its_own_version\",\n```\n\n### 5.2 Lessons from v0.2.3 — Real Failures and Their Fixes\n\nThese are actual production failures from shipping new engines. Every one of these passed `python -m uvicorn` in dev:\n\n| Engine | Failure | Root Cause | Fix |\n|--------|---------|-----------|-----|\n| LuxTTS | `\"could not get source code\"` on import | `inflect` uses `typeguard`'s `@typechecked` which calls `inspect.getsource()` — needs `.py` source files, not just bytecode | `--collect-all inflect` |\n| LuxTTS | `espeak-ng-data` not found | `piper_phonemize` C library looks for data at `/usr/share/espeak-ng-data/` which doesn't exist in the bundle | `--collect-all piper_phonemize` + set `ESPEAK_DATA_PATH` env var at runtime (see 5.3) |\n| LuxTTS | `inspect.getsource` error in Vocos codec | `linacodec` and `zipvoice` use source introspection | `--collect-all linacodec` + `--collect-all zipvoice` |\n| Chatterbox | `FileNotFoundError` for watermark model | `perth` ships pretrained model files (`hparams.yaml`, `.pth.tar`) that PyInstaller doesn't bundle by default | `--collect-all perth` |\n| All engines | `importlib.metadata` failures | Frozen binary doesn't include package metadata for `huggingface-hub`, `transformers`, etc. | `--copy-metadata` for each affected package |\n| All engines | Download progress bars stuck at 0% | `huggingface_hub` silently disables tqdm progress bars based on logger level in frozen builds — our progress tracker never receives byte updates | Force-enable tqdm's internal counter in `HFProgressTracker` |\n| TADA | `inspect.getsource` error in DAC's `Snake1d` | `@torch.jit.script` calls `inspect.getsource()` which fails without `.py` source files | Wrote a lightweight shim (`dac_shim.py`) reimplementing `Snake1d` without `@torch.jit.script`, registered fake `dac.*` modules in `sys.modules` |\n| All engines | `NameError: name 'obj' is not defined` on macOS | Python 3.12.0 has a [CPython bug](https://github.com/pyinstaller/pyinstaller/issues/7992) that corrupts bytecode when PyInstaller rewrites code objects | Upgrade to Python 3.12.13+ |\n| All engines | `resource_tracker` subprocess crash | `multiprocessing` in frozen binaries needs `freeze_support()` called before anything else | Added to `server.py` entry point |\n\n### 5.3 Runtime Frozen-Build Handling (`server.py`)\n\nSome fixes can't live in `build_binary.py` — they need runtime detection. The entry point `backend/server.py` handles these before any heavy imports:\n\n```python\n# 1. freeze_support() — MUST be called before any multiprocessing use\nimport multiprocessing\nmultiprocessing.freeze_support()\n\n# 2. Native data paths — redirect C libraries to bundled data\nif getattr(sys, 'frozen', False):\n    _meipass = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))\n    _espeak_data = os.path.join(_meipass, 'piper_phonemize', 'espeak-ng-data')\n    if os.path.isdir(_espeak_data):\n        os.environ.setdefault('ESPEAK_DATA_PATH', _espeak_data)\n\n# 3. stdout/stderr safety — PyInstaller --noconsole on Windows sets these to None\nif not _is_writable(sys.stdout):\n    sys.stdout = open(os.devnull, 'w')\n```\n\nIf your engine's dependencies include native libraries that look for data at system paths (like espeak-ng does), you'll need to add a similar `os.environ.setdefault()` block here.\n\n### 5.4 CUDA vs CPU Build Branching\n\n`build_binary.py` produces two different binaries:\n\n- **`voicebox-server`** (CPU) — excludes all `nvidia.*` packages to avoid bundling ~3 GB of CUDA DLLs\n- **`voicebox-server-cuda`** — includes `torch.cuda` and `torch.backends.cudnn`\n\nOn Windows, if the build environment has CUDA torch installed but you're building the CPU binary, the script temporarily swaps to CPU-only torch and restores CUDA torch afterward. This prevents PyInstaller from accidentally bundling CUDA libraries into the CPU build.\n\nNew engine imports go in the **common section** (not the CUDA or MLX conditional blocks) unless your engine has platform-specific dependencies.\n\n### 5.5 MLX Conditional Inclusion\n\nApple Silicon builds conditionally include MLX hidden imports and `--collect-all mlx` / `--collect-all mlx_audio`. If your engine has an MLX-specific backend variant, add its imports inside the `if is_apple_silicon() and not cuda:` block.\n\n### 5.6 Testing Frozen Builds\n\nYou can't skip this. Models that work in `python -m uvicorn` will break in the PyInstaller binary. The v0.2.3 release required **three patch releases** (v0.2.1 → v0.2.2 → v0.2.3) to get all engines working in production.\n\n1. Build: `just build`\n2. Launch the binary directly (not via `python -m`)\n3. Test the **full chain**: download → load → generate → progress tracking\n4. Check stderr for the actual error (logs go to stderr for Tauri sidecar capture)\n5. Fix, rebuild, repeat\n\n**Common gotcha:** testing only generation with a pre-cached model from your dev install. Always test with a clean model cache to verify downloads work too.\n\n## Phase 6: Common Upstream Workarounds\n\n### torch.load device mismatch\n```python\n_original_torch_load = torch.load\ndef _patched_torch_load(*args, **kwargs):\n    kwargs.setdefault(\"map_location\", \"cpu\")\n    return _original_torch_load(*args, **kwargs)\ntorch.load = _patched_torch_load\n```\n\n### Float64/Float32 dtype mismatch\n```python\noriginal_fn = SomeClass.some_method\ndef patched_fn(self, *args, **kwargs):\n    result = original_fn(self, *args, **kwargs)\n    return result.float()\nSomeClass.some_method = patched_fn\n```\n\n### HuggingFace token bug\n```python\nfrom huggingface_hub import snapshot_download\nlocal_path = snapshot_download(repo_id=REPO, token=None)\nmodel = ModelClass.from_local(local_path, device=device)\n```\n\n### MPS tensor issues\nSkip MPS entirely if operators aren't supported:\n```python\ndef _get_device(self):\n    if torch.cuda.is_available():\n        return \"cuda\"\n    return \"cpu\"  # Skip MPS\n```\n\n### Gated HuggingFace repos as hardcoded config sources\n\nSome models hardcode a gated HuggingFace repo as their tokenizer or config source (e.g., TADA hardcodes `\"meta-llama/Llama-3.2-1B\"` in both its `AlignerConfig` and `TadaConfig`). This silently fails without HF authentication.\n\n**Fix:** Download from an ungated mirror and patch the config objects directly:\n\n```python\n# Download tokenizer from ungated mirror\nUNGATED_TOKENIZER = \"unsloth/Llama-3.2-1B\"\ntokenizer_path = snapshot_download(UNGATED_TOKENIZER, token=None)\n\n# Patch the model config to use the local path instead of the gated repo\nconfig = ModelConfig.from_pretrained(model_path)\nconfig.tokenizer_name = tokenizer_path\nmodel = ModelClass.from_pretrained(model_path, config=config)\n```\n\n**Do NOT monkey-patch `AutoTokenizer.from_pretrained`** — it's a classmethod, and replacing it corrupts the descriptor, which breaks other engines that use different tokenizers (e.g., Qwen uses a Qwen tokenizer via `AutoTokenizer`). Always patch at the config level, not the class method level.\n\n### `torchaudio.load()` requires `torchcodec` in 2.10+\n\nAs of `torchaudio>=2.10`, `torchaudio.load()` requires the `torchcodec` package for audio I/O. If your engine or backend code uses `torchaudio.load()`, replace it with `soundfile`:\n\n```python\n# Before (breaks without torchcodec):\nimport torchaudio\nwaveform, sr = torchaudio.load(\"audio.wav\")\n\n# After:\nimport soundfile as sf\nimport torch\ndata, sr = sf.read(\"audio.wav\", dtype=\"float32\")\nwaveform = torch.from_numpy(data).unsqueeze(0)\n```\n\nNote: `torchaudio.functional.resample()` and other pure-PyTorch math functions work fine without `torchcodec` — only the I/O functions are affected.\n\n### `@torch.jit.script` breaks in frozen builds\n\n`torch.jit.script` calls `inspect.getsource()` to parse the decorated function's source code. In a PyInstaller binary, `.py` source files aren't available, so this crashes at import time.\n\n**Fix:** Remove or avoid `@torch.jit.script` decorators. If the decorated function comes from an upstream dependency, write a shim that reimplements the function without the decorator (see \"Toxic dependency chains\" below).\n\n### Toxic dependency chains — the shim pattern\n\nSometimes a model library depends on a package with a massive, hostile transitive dependency tree, but only uses a tiny piece of it. When the dependency chain is unbuildable or would pull in dozens of unwanted packages, the right move is to write a lightweight shim.\n\n**Example:** TADA depends on `descript-audio-codec` (DAC), which pulls in `descript-audiotools` -> `onnx`, `tensorboard`, `protobuf`, `matplotlib`, `pystoi`, etc. The `onnx` package fails to build from source on macOS. But TADA only uses `Snake1d` from DAC — a 7-line PyTorch module.\n\n**Solution:** Create a shim at `backend/utils/dac_shim.py` that registers fake modules in `sys.modules`:\n\n```python\nimport sys\nimport types\nimport torch\nfrom torch import nn\n\ndef snake(x, alpha):\n    \"\"\"Snake activation — reimplemented without @torch.jit.script.\"\"\"\n    return x + (1.0 / (alpha + 1e-9)) * torch.sin(alpha * x).pow(2)\n\nclass Snake1d(nn.Module):\n    def __init__(self, channels):\n        super().__init__()\n        self.alpha = nn.Parameter(torch.ones(1, channels, 1))\n    def forward(self, x):\n        return snake(x, self.alpha)\n\n# Register fake dac.* modules so \"from dac.nn.layers import Snake1d\" works\n_nn = types.ModuleType(\"dac.nn\")\n_layers = types.ModuleType(\"dac.nn.layers\")\n_layers.Snake1d = Snake1d\n_nn.layers = _layers\n\nfor name, mod in [(\"dac\", types.ModuleType(\"dac\")),\n                   (\"dac.nn\", _nn), (\"dac.nn.layers\", _layers)]:\n    sys.modules[name] = mod\n```\n\n**Key rules for shims:**\n- Import the shim **before** importing the model library (so it finds the fake modules first)\n- Do NOT use `@torch.jit.script` in the shim (see above)\n- Only reimplement what the model actually uses — check the import chain carefully\n\n## Upcoming Engines\n\nBased on the current model landscape, these are candidates for future integration:\n\n| Model | Languages | Size | Key Features | Status |\n|-------|-----------|------|--------------|--------|\n| **CosyVoice2-0.5B** | Multilingual | ~500MB | Instruct support (`inference_instruct2()`) | Ready |\n| **Fish Speech** | 50+ | Medium | Word-level control via inline text | Ready |\n| **Kokoro-82M** | English | 82M | CPU realtime, Apache 2.0 | Ready |\n| **XTTS-v2** | 17+ | Medium | Zero-shot cloning | Ready |\n| **MOSS-TTS** | Multilingual | Medium | Text-to-voice design, multi-speaker dialogue | Needs vetting |\n| **Pocket TTS** | English | ~100M | CPU-first, >1× realtime | Needs vetting |\n\n## Implementation Checklist\n\nUse this as a gate between phases. Do not proceed to the next phase until every item in the current phase is checked.\n\n### Phase 0: Dependency Research\n- [ ] Cloned model library source into a temp directory\n- [ ] Read `setup.py` / `pyproject.toml` — noted pinned dependency versions\n- [ ] Traced all imports from the model class through to leaf dependencies\n- [ ] Searched for `inspect.getsource`, `@typechecked`, `typeguard` in the full dependency tree\n- [ ] Searched for `importlib.metadata`, `pkg_resources.get_distribution` in the dependency tree\n- [ ] Searched for `Path(__file__).parent`, `os.path.dirname(__file__)`, hardcoded system paths\n- [ ] Searched for `torch.load` calls missing `map_location`\n- [ ] Searched for `torch.from_numpy` without `.float()` cast\n- [ ] Searched for `token=True` or `token=os.getenv(\"HF_TOKEN\")` in HuggingFace calls\n- [ ] Searched for `@torch.jit.script` / `torch.jit.script` (crashes in frozen builds)\n- [ ] Searched for `torchaudio.load` / `torchaudio.save` (requires `torchcodec` in 2.10+)\n- [ ] Searched for hardcoded gated HuggingFace repo names (e.g., `meta-llama/*`)\n- [ ] Evaluated whether any dependency is used minimally enough to shim instead of install\n- [ ] Tested model loading and generation on CPU in a throwaway venv\n- [ ] Tested with a clean HuggingFace cache (no pre-downloaded models)\n- [ ] Produced a written dependency audit documenting all findings\n\n### Phase 1: Backend Implementation\n- [ ] Created `backend/backends/<engine>_backend.py` implementing `TTSBackend` protocol\n- [ ] Chose voice prompt pattern (pre-computed tensors vs deferred file paths)\n- [ ] Implemented all monkey-patches identified in Phase 0\n- [ ] Used `get_torch_device()` from `backends/base.py` for device selection\n- [ ] Used `model_load_progress()` from `backends/base.py` for download/load tracking\n- [ ] Tested: model downloads correctly\n- [ ] Tested: model loads on CPU\n- [ ] Tested: generation produces valid audio\n- [ ] Tested: voice cloning from reference audio works\n- [ ] Registered `ModelConfig` in `backends/__init__.py`\n- [ ] Added to `TTS_ENGINES` dict\n- [ ] Added factory branch in `get_tts_backend_for_engine()`\n- [ ] Updated engine regex in `backend/models.py`\n\n### Phase 2–3: Route, Service, and Frontend\n- [ ] Confirmed zero changes needed in routes/services (or documented why custom behavior is needed)\n- [ ] Added engine to TypeScript union type in `app/src/lib/api/types.ts`\n- [ ] Added language map entry in `app/src/lib/constants/languages.ts`\n- [ ] Added to `ENGINE_OPTIONS` and `ENGINE_DESCRIPTIONS` in `EngineModelSelector.tsx`\n- [ ] Added to Zod schema and model-name mapping in `useGenerationForm.ts`\n- [ ] Added description in `ModelManagement.tsx`\n\n### Phase 4: Dependencies\n- [ ] Added packages to `backend/requirements.txt`\n- [ ] If `--no-deps` needed: listed sub-dependencies explicitly\n- [ ] If git-only packages: added `@ git+https://...` entries\n- [ ] If custom index needed: added `--find-links` line\n- [ ] Updated `justfile` setup targets\n- [ ] Updated `.github/workflows/release.yml` build steps\n- [ ] Updated `Dockerfile` if applicable\n- [ ] Verified `pip install` succeeds in a clean venv with existing requirements\n\n### Phase 5: PyInstaller Bundling\n- [ ] Added `--hidden-import` entries in `build_binary.py` for:\n  - [ ] `backend.backends.<engine>_backend`\n  - [ ] The model package and its key submodules\n- [ ] Added `--collect-all` for any packages that:\n  - [ ] Use `inspect.getsource()` / `@typechecked`\n  - [ ] Ship pretrained model data files (`.pth.tar`, `.yaml`, etc.)\n  - [ ] Ship native data files (phoneme tables, shader libraries, etc.)\n- [ ] Added `--copy-metadata` for any packages that use `importlib.metadata`\n- [ ] If engine has native data paths: added `os.environ.setdefault()` in `server.py`\n- [ ] Built frozen binary with `just build`\n- [ ] Tested in frozen binary with **clean model cache** (not pre-cached from dev):\n  - [ ] Model download works with real-time progress\n  - [ ] Model loading works\n  - [ ] Generation produces valid audio\n  - [ ] No errors in stderr logs\n\n### Phase 6: Final Verification\n- [ ] Engine works in dev mode (`just dev`)\n- [ ] Engine works in frozen binary (`just build` → run binary directly)\n- [ ] Tested on target platform (macOS for MLX, Windows/Linux for CUDA)\n- [ ] No regressions in existing engines\n"
  },
  {
    "path": "docs/content/docs/developer/tts-generation.mdx",
    "content": "---\ntitle: \"TTS Generation\"\ndescription: \"How text-to-speech generation works in Voicebox\"\n---\n\n## Overview\n\nVoicebox uses Qwen3-TTS for voice cloning and text-to-speech generation. The TTS module handles model loading, voice prompt creation, and audio synthesis.\n\n## Architecture\n\nThe TTS system is built around the `TTSModel` class which manages:\n\n**Model Loading:** Lazy loading with automatic HuggingFace Hub download.\n\n**Voice Prompts:** Converting reference audio into embeddings.\n\n**Generation:** Synthesizing speech from text using voice prompts.\n\n## TTSModel Class\n\n```python\nclass TTSModel:\n    def __init__(self, model_size: str = \"1.7B\"):\n        self.model = None\n        self.model_size = model_size\n        self.device = self._get_device()  # cuda, mps, or cpu\n```\n\n### Device Selection\n\nThe model automatically selects the best available device:\n\n```python\ndef _get_device(self) -> str:\n    if torch.cuda.is_available():\n        return \"cuda\"\n    elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():\n        return \"cpu\"  # MPS can have issues, use CPU for stability\n    return \"cpu\"\n```\n\n## Model Loading\n\nModels are downloaded from HuggingFace Hub on first use:\n\n```python\ndef load_model(self, model_size: Optional[str] = None):\n    # Model IDs on HuggingFace Hub\n    hf_model_map = {\n        \"1.7B\": \"Qwen/Qwen3-TTS-12Hz-1.7B-Base\",\n        \"0.6B\": \"Qwen/Qwen3-TTS-12Hz-0.6B-Base\",\n    }\n    \n    # Load with progress tracking\n    with tracker.patch_download():\n        self.model = Qwen3TTSModel.from_pretrained(\n            model_path,\n            device_map=self.device,\n            torch_dtype=torch.bfloat16,  # float32 on CPU\n        )\n```\n\n### Async Loading\n\nLoading runs in a thread pool to avoid blocking the event loop:\n\n```python\nasync def load_model_async(self, model_size: Optional[str] = None):\n    if self.model is not None and self._current_model_size == model_size:\n        return\n    await asyncio.to_thread(self.load_model, model_size)\n```\n\n## Voice Prompt Creation\n\nVoice prompts are created from reference audio and cached for reuse:\n\n```python\nasync def create_voice_prompt(\n    self,\n    audio_path: str,\n    reference_text: str,\n    use_cache: bool = True,\n) -> Tuple[dict, bool]:\n    await self.load_model_async()\n    \n    # Check cache\n    if use_cache:\n        cache_key = get_cache_key(audio_path, reference_text)\n        cached = get_cached_voice_prompt(cache_key)\n        if cached:\n            return cached, True\n    \n    # Create prompt (blocking, run in thread pool)\n    voice_prompt = await asyncio.to_thread(\n        self.model.create_voice_clone_prompt,\n        ref_audio=audio_path,\n        ref_text=reference_text,\n    )\n    \n    # Cache the result\n    cache_voice_prompt(cache_key, voice_prompt)\n    return voice_prompt, False\n```\n\n### Combining Multiple Samples\n\nWhen a profile has multiple samples, they're combined:\n\n```python\nasync def combine_voice_prompts(\n    self,\n    audio_paths: List[str],\n    reference_texts: List[str],\n) -> Tuple[np.ndarray, str]:\n    combined_audio = []\n    \n    for audio_path in audio_paths:\n        audio, sr = load_audio(audio_path)\n        audio = normalize_audio(audio)\n        combined_audio.append(audio)\n    \n    # Concatenate and normalize\n    mixed = np.concatenate(combined_audio)\n    mixed = normalize_audio(mixed)\n    \n    # Combine texts\n    combined_text = \" \".join(reference_texts)\n    \n    return mixed, combined_text\n```\n\n## Speech Generation\n\nThe core generation function:\n\n```python\nasync def generate(\n    self,\n    text: str,\n    voice_prompt: dict,\n    language: str = \"en\",\n    seed: Optional[int] = None,\n    instruct: Optional[str] = None,\n) -> Tuple[np.ndarray, int]:\n    await self.load_model_async()\n    \n    def _generate_sync():\n        # Set seed for reproducibility\n        if seed is not None:\n            torch.manual_seed(seed)\n        \n        # Generate audio\n        wavs, sample_rate = self.model.generate_voice_clone(\n            text=text,\n            voice_clone_prompt=voice_prompt,\n            instruct=instruct,  # Natural language delivery control\n        )\n        return wavs[0], sample_rate\n    \n    # Run in thread pool\n    return await asyncio.to_thread(_generate_sync)\n```\n\n### Instruct Feature\n\nThe `instruct` parameter allows natural language control over speech delivery:\n\n```python\n# Examples:\ninstruct = \"Speak slowly and clearly\"\ninstruct = \"Sound excited and enthusiastic\"\ninstruct = \"Whisper softly\"\n```\n\n## Caching Strategy\n\nVoice prompts are cached to avoid recomputation:\n\n```python\ndef get_cache_key(audio_path: str, reference_text: str) -> str:\n    \"\"\"Generate cache key from audio hash and text.\"\"\"\n    audio_hash = hashlib.md5(Path(audio_path).read_bytes()).hexdigest()\n    text_hash = hashlib.md5(reference_text.encode()).hexdigest()\n    return f\"{audio_hash}_{text_hash}\"\n```\n\nCache is stored in `data/cache/voice_prompts/`.\n\n## Memory Management\n\n### Unloading Models\n\nFree VRAM/RAM when not needed:\n\n```python\ndef unload_model(self):\n    if self.model is not None:\n        del self.model\n        self.model = None\n        \n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n```\n\n### Model Switching\n\nWhen switching between model sizes (1.7B ↔ 0.6B):\n\n```python\n# Unload existing model first\nif self.model is not None and self._current_model_size != model_size:\n    self.unload_model()\n```\n\n## Generation Flow\n\n1. **Request** → Validate text and profile ID\n2. **Profile** → Load profile samples from database\n3. **Voice Prompt** → Create or retrieve cached prompt\n4. **Generate** → Run TTS inference\n5. **Save** → Write audio to generations directory\n6. **Record** → Create history entry in database\n7. **Response** → Return audio path and metadata\n\n## API Endpoints\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| POST | `/generate` | Generate speech from text |\n| GET | `/audio/{id}` | Serve generated audio file |\n\n### Request Schema\n\n```json\n{\n  \"profile_id\": \"uuid\",\n  \"text\": \"Text to synthesize\",\n  \"language\": \"en\",\n  \"seed\": 42,\n  \"model_size\": \"1.7B\",\n  \"instruct\": \"Speak clearly\"\n}\n```\n\n### Response Schema\n\n```json\n{\n  \"id\": \"generation_uuid\",\n  \"profile_id\": \"profile_uuid\",\n  \"text\": \"Text to synthesize\",\n  \"language\": \"en\",\n  \"audio_path\": \"/path/to/audio.wav\",\n  \"duration\": 3.5,\n  \"seed\": 42,\n  \"instruct\": \"Speak clearly\",\n  \"created_at\": \"2024-01-15T10:30:00Z\"\n}\n```\n\n## Performance Considerations\n\n### GPU Acceleration\n\n- CUDA provides fastest inference\n- MPS (Apple Silicon) has stability issues, uses CPU fallback\n- CPU inference is slower but always works\n\n### Batch Size\n\nCurrently generates one utterance at a time. For long texts, consider:\n- Splitting into sentences\n- Sequential generation\n- Concatenating results\n\n### Memory Usage\n\n| Model | VRAM/RAM Required |\n|-------|-------------------|\n| 0.6B | ~2GB |\n| 1.7B | ~6GB |\n"
  },
  {
    "path": "docs/content/docs/developer/voice-profiles.mdx",
    "content": "---\ntitle: \"Voice Profiles\"\ndescription: \"How voice profile management works in Voicebox\"\n---\n\n## Overview\n\nVoice profiles are the foundation of Voicebox's voice cloning capability. Each profile stores reference audio samples and metadata that the TTS model uses to clone a voice.\n\n## Architecture\n\nThe voice profile system consists of three main components:\n\n**Database Layer:** SQLite tables store profile metadata and sample references.\n\n**File Storage:** Audio samples are stored on disk in a structured directory format.\n\n**Profile Module:** The `profiles.py` module provides the business logic for CRUD operations.\n\n## Data Model\n\n### VoiceProfile Table\n\n```python\nclass VoiceProfile(Base):\n    __tablename__ = \"profiles\"\n    \n    id = Column(String, primary_key=True)\n    name = Column(String, unique=True, nullable=False)\n    description = Column(Text)\n    language = Column(String, default=\"en\")\n    created_at = Column(DateTime)\n    updated_at = Column(DateTime)\n```\n\n### ProfileSample Table\n\n```python\nclass ProfileSample(Base):\n    __tablename__ = \"profile_samples\"\n    \n    id = Column(String, primary_key=True)\n    profile_id = Column(String, ForeignKey(\"profiles.id\"))\n    audio_path = Column(String, nullable=False)\n    reference_text = Column(Text, nullable=False)\n```\n\n## File Structure\n\nProfiles are stored in the data directory:\n\n<Files>\n  <Folder name=\"data\" defaultOpen>\n    <Folder name=\"profiles\">\n      <Folder name=\"{profile_id}\">\n        <File name=\"{sample_id_1}.wav\" />\n        <File name=\"{sample_id_2}.wav\" />\n      </Folder>\n    </Folder>\n  </Folder>\n</Files>\n\n## Core Functions\n\n### Creating a Profile\n\n```python\nasync def create_profile(data: VoiceProfileCreate, db: Session) -> VoiceProfileResponse:\n    # 1. Create database record\n    db_profile = DBVoiceProfile(\n        id=str(uuid.uuid4()),\n        name=data.name,\n        description=data.description,\n        language=data.language,\n    )\n    db.add(db_profile)\n    db.commit()\n    \n    # 2. Create profile directory\n    profile_dir = profiles_dir / db_profile.id\n    profile_dir.mkdir(parents=True, exist_ok=True)\n    \n    return VoiceProfileResponse.model_validate(db_profile)\n```\n\n### Adding Samples\n\nWhen a sample is added, the audio is validated and copied to the profile directory:\n\n```python\nasync def add_profile_sample(\n    profile_id: str,\n    audio_path: str,\n    reference_text: str,\n    db: Session,\n) -> ProfileSampleResponse:\n    # 1. Validate audio (duration, format, quality)\n    is_valid, error_msg = validate_reference_audio(audio_path)\n    if not is_valid:\n        raise ValueError(f\"Invalid reference audio: {error_msg}\")\n    \n    # 2. Copy to profile directory\n    sample_id = str(uuid.uuid4())\n    dest_path = profile_dir / f\"{sample_id}.wav\"\n    audio, sr = load_audio(audio_path)\n    save_audio(audio, str(dest_path), sr)\n    \n    # 3. Create database record\n    db_sample = DBProfileSample(\n        id=sample_id,\n        profile_id=profile_id,\n        audio_path=str(dest_path),\n        reference_text=reference_text,\n    )\n    db.add(db_sample)\n    db.commit()\n```\n\n### Voice Prompt Creation\n\nWhen generating speech, samples are combined into a voice prompt:\n\n```python\nasync def create_voice_prompt_for_profile(\n    profile_id: str,\n    db: Session,\n) -> dict:\n    samples = db.query(DBProfileSample).filter_by(profile_id=profile_id).all()\n    \n    if len(samples) == 1:\n        # Single sample - use directly\n        voice_prompt, _ = await tts_model.create_voice_prompt(\n            sample.audio_path,\n            sample.reference_text,\n        )\n    else:\n        # Multiple samples - combine them\n        combined_audio, combined_text = await tts_model.combine_voice_prompts(\n            [s.audio_path for s in samples],\n            [s.reference_text for s in samples],\n        )\n        voice_prompt, _ = await tts_model.create_voice_prompt(\n            combined_audio_path,\n            combined_text,\n        )\n    \n    return voice_prompt\n```\n\n## Audio Validation\n\nReference audio is validated before being accepted:\n\n- **Duration:** 3-30 seconds recommended\n- **Format:** WAV, MP3, FLAC, OGG supported\n- **Sample Rate:** Resampled to 24kHz\n- **Channels:** Converted to mono if stereo\n\n## Export/Import\n\nProfiles can be exported as ZIP archives for sharing:\n\n<Files>\n  <Folder name=\"profile_export.zip\" defaultOpen>\n    <File name=\"profile.json\" />\n    <Folder name=\"samples\">\n      <File name=\"sample_1.wav\" />\n      <File name=\"sample_1.json\" />\n    </Folder>\n  </Folder>\n</Files>\n\n## API Endpoints\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| GET | `/profiles` | List all profiles |\n| POST | `/profiles` | Create a profile |\n| GET | `/profiles/{id}` | Get profile by ID |\n| PUT | `/profiles/{id}` | Update profile |\n| DELETE | `/profiles/{id}` | Delete profile |\n| GET | `/profiles/{id}/samples` | Get profile samples |\n| POST | `/profiles/{id}/samples` | Add sample to profile |\n| PUT | `/profiles/samples/{id}` | Update sample text |\n| DELETE | `/profiles/samples/{id}` | Delete sample |\n| GET | `/profiles/{id}/export` | Export as ZIP |\n| POST | `/profiles/import` | Import from ZIP |\n\n## Best Practices\n\n### Sample Quality\n\n- Use clean audio with minimal background noise\n- Ensure the reference text exactly matches what is spoken\n- Multiple samples (3-5) improve voice cloning quality\n\n### Language Matching\n\n- Set the profile language to match the reference audio\n- Supported languages: en, zh, ja, ko, de, fr, ru, pt, es, it\n\n### Naming Conventions\n\n- Use descriptive names that identify the voice\n- Avoid special characters that may cause filesystem issues\n"
  },
  {
    "path": "docs/content/docs/index.mdx",
    "content": "---\ntitle: \"Voicebox Documentation\"\ndescription: \"Voicebox is a local-first voice cloning studio -- a free and open-source alternative to ElevenLabs.\"\n---\n\nVoicebox is a **local-first voice cloning studio** -- a free and open-source alternative to ElevenLabs. Clone voices from a few seconds of audio, generate speech in 23 languages across 5 TTS engines, apply post-processing effects, and compose multi-voice projects with a timeline editor.\n\n![Voicebox App Screenshot](/images/app-screenshot-1.webp)\n\n- **Complete privacy** -- models and voice data stay on your machine\n- **5 TTS engines** -- Qwen3-TTS, LuxTTS, Chatterbox Multilingual, Chatterbox Turbo, and HumeAI TADA\n- **23 languages** -- from English to Arabic, Japanese, Hindi, Swahili, and more\n- **Post-processing effects** -- pitch shift, reverb, delay, chorus, compression, and filters\n- **Expressive speech** -- paralinguistic tags like `[laugh]`, `[sigh]`, `[gasp]` via Chatterbox Turbo\n- **Unlimited length** -- auto-chunking with crossfade for scripts, articles, and chapters\n- **Stories editor** -- multi-track timeline for conversations, podcasts, and narratives\n- **API-first** -- REST API for integrating voice synthesis into your own projects\n- **Native performance** -- built with Tauri (Rust), not Electron\n- **Runs everywhere** -- macOS (MLX/Metal), Windows (CUDA), Linux, AMD ROCm, Intel Arc, Docker\n\n## Download\n\n| Platform | Download |\n|----------|----------|\n| macOS (Apple Silicon) | [Download DMG](https://voicebox.sh/download/mac-arm) |\n| macOS (Intel) | [Download DMG](https://voicebox.sh/download/mac-intel) |\n| Windows | [Download MSI](https://voicebox.sh/download/windows) |\n| Docker | `docker compose up` |\n\n[View all releases](https://github.com/jamiepine/voicebox/releases/latest)\n\n## Get Started\n\n- [Installation](/docs/overview/installation) -- download and install Voicebox\n- [Quick Start](/docs/overview/quick-start) -- get up and running in 5 minutes\n- [API Reference](/docs/api-reference) -- integrate voice synthesis into your apps\n"
  },
  {
    "path": "docs/content/docs/meta.json",
    "content": "{\n  \"title\": \"Voicebox Documentation\",\n  \"pages\": [\"overview\", \"api-reference\", \"developer\"]\n}\n"
  },
  {
    "path": "docs/content/docs/overview/building-stories.mdx",
    "content": "---\ntitle: \"Building Stories\"\ndescription: \"Create multi-voice narratives with the Stories Editor\"\n---\n\n## Getting Started\n\nThe Stories Editor is perfect for creating podcasts, audiobooks, and multi-speaker content.\n\n<Steps>\n  <Step title=\"Create Story\">\n    **Stories** → **+ New Story**\n  </Step>\n  <Step title=\"Add Tracks\">\n    Create tracks for each speaker\n  </Step>\n  <Step title=\"Add Clips\">\n    Generate or drag audio to tracks\n  </Step>\n  <Step title=\"Arrange\">\n    Position and trim clips on timeline\n  </Step>\n  <Step title=\"Export\">\n    Render final audio\n  </Step>\n</Steps>\n\n## Use Cases\n\n- Multi-host podcasts\n- Audiobook narration with character voices\n- Game dialogue scenes\n- Educational content with multiple speakers\n\n## Coming Soon\n\nFull timeline editor documentation will be added as features are finalized.\n"
  },
  {
    "path": "docs/content/docs/overview/creating-voice-profiles.mdx",
    "content": "---\ntitle: \"Creating Voice Profiles\"\ndescription: \"Advanced guide to creating high-quality voice profiles\"\n---\n\n## Overview\n\nVoice profiles are the foundation of voice cloning in Voicebox. This guide covers best practices for creating professional-quality voice profiles.\n\n## Quick Start\n\n<Steps>\n  <Step title=\"Prepare Audio\">\n    10-30 seconds of clear speech\n  </Step>\n  <Step title=\"Create Profile\">\n    **Profiles** → **+ New Profile**\n  </Step>\n  <Step title=\"Upload Sample\">\n    Add your audio file\n  </Step>\n  <Step title=\"Generate\">\n    Use the profile to generate speech\n  </Step>\n</Steps>\n\n## Audio Requirements\n\n### Ideal Sample Characteristics\n\n<Cards>\n  <Card title=\"Duration\">\n    **10-30 seconds**\n\n    Too short: Poor quality\n    Too long: Unnecessary\n  </Card>\n  <Card title=\"Clarity\">\n    **Clear speech**\n\n    No background noise\n    No music or overlapping voices\n  </Card>\n  <Card title=\"Quality\">\n    **High fidelity**\n\n    44.1kHz or 48kHz sample rate\n    Minimal compression\n  </Card>\n  <Card title=\"Content\">\n    **Natural speech**\n\n    Conversational tone\n    Complete sentences\n  </Card>\n</Cards>\n\n### File Formats\n\nSupported formats:\n- **WAV** (recommended) - Lossless quality\n- **MP3** - Acceptable, minimal compression\n- **M4A** - Acceptable\n- **FLAC** - Lossless alternative\n\n<Callout type=\"info\">\n  Use WAV for best results. Avoid heavily compressed formats.\n</Callout>\n\n## Recording Tips\n\n### Environment\n\n<AccordionGroup>\n  <Accordion title=\"Quiet Space\">\n    - Record in a quiet room\n    - Turn off fans, AC, appliances\n    - Close windows to reduce outside noise\n    - Use soft furnishings to reduce echo\n  </Accordion>\n\n  <Accordion title=\"Microphone Placement\">\n    - 6-12 inches from mouth\n    - Slight angle to reduce plosives (p, b, t)\n    - Use a pop filter if available\n    - Maintain consistent distance\n  </Accordion>\n\n  <Accordion title=\"Recording Settings\">\n    - 44.1kHz or 48kHz sample rate\n    - 16-bit or 24-bit depth\n    - Mono is fine (stereo will be converted)\n    - Avoid automatic gain control\n  </Accordion>\n</AccordionGroup>\n\n### Speaking\n\n- **Natural pace** - Don't rush or speak too slowly\n- **Clear articulation** - Pronounce words clearly\n- **Consistent volume** - Maintain steady loudness\n- **Normal tone** - Speak as you normally would\n- **Complete sentences** - Avoid fragments or \"ums\"\n\n## Multiple Samples\n\nAdding multiple samples can significantly improve quality:\n\n### Why Multiple Samples?\n\n<Cards>\n  <Card title=\"Robustness\">\n    Model learns a more complete representation\n  </Card>\n  <Card title=\"Versatility\">\n    Handles different speaking styles better\n  </Card>\n  <Card title=\"Quality\">\n    Reduces artifacts and improves naturalness\n  </Card>\n  <Card title=\"Consistency\">\n    More reliable across different texts\n  </Card>\n</Cards>\n\n### Sample Variety\n\nConsider adding samples with:\n\n1. **Different tones**\n   - Casual conversation\n   - Professional/formal\n   - Excited/enthusiastic\n   - Calm/serious\n\n2. **Different content**\n   - Narratives\n   - Questions\n   - Statements\n   - Emotions (happy, sad, neutral)\n\n3. **Different recording conditions**\n   - Studio quality\n   - Phone call quality (if needed)\n   - Room acoustics\n\n<Callout type=\"warn\">\n  All samples should be from the **same speaker**. Mixing voices will produce poor results.\n</Callout>\n\n## Processing Existing Audio\n\nIf you have existing audio (podcasts, videos, etc.):\n\n### Extracting Clean Segments\n\n<Steps>\n  <Step title=\"Find Clean Speech\">\n    Look for segments with:\n    - Just the target speaker\n    - No background music\n    - Minimal noise\n  </Step>\n\n  <Step title=\"Use Audio Editor\">\n    Tools like Audacity or Adobe Audition:\n    - Cut out clean 10-30s segments\n    - Remove silence at start/end\n    - Normalize volume if needed\n  </Step>\n\n  <Step title=\"Export as WAV\">\n    Save as high-quality WAV file\n  </Step>\n</Steps>\n\n### Noise Reduction\n\nIf you have light background noise:\n\n```\n1. Use noise reduction in Audacity:\n   - Select noise-only section\n   - Get Noise Profile\n   - Select full audio\n   - Apply noise reduction (gentle settings)\n\n2. Avoid over-processing:\n   - Can introduce artifacts\n   - May reduce voice quality\n```\n\n## Testing & Iteration\n\n### Test Your Profile\n\nAfter creating a profile:\n\n<Steps>\n  <Step title=\"Generate Test\">\n    Generate a simple phrase:\n    ```\n    \"Hello, this is a test of my voice profile.\"\n    ```\n  </Step>\n\n  <Step title=\"Evaluate Quality\">\n    Listen for:\n    - Natural tone\n    - Clear pronunciation\n    - Proper prosody\n    - Lack of artifacts\n  </Step>\n\n  <Step title=\"Iterate\">\n    If quality is poor:\n    - Add more samples\n    - Try different source audio\n    - Check sample quality\n  </Step>\n</Steps>\n\n### Common Issues\n\n<AccordionGroup>\n  <Accordion title=\"Robotic Voice\">\n    **Cause**: Poor quality samples or too short\n\n    **Fix**: Use longer, higher quality samples\n  </Accordion>\n\n  <Accordion title=\"Wrong Tone\">\n    **Cause**: Sample tone doesn't match desired output\n\n    **Fix**: Record samples in the style you want to generate\n  </Accordion>\n\n  <Accordion title=\"Artifacts/Glitches\">\n    **Cause**: Background noise or audio issues in samples\n\n    **Fix**: Clean up samples or re-record in quieter environment\n  </Accordion>\n</AccordionGroup>\n\n## Advanced Tips\n\n### Celebrity/Character Voices\n\nFor cloning public figures or characters:\n\n1. **Legal considerations** - Ensure you have rights or it's fair use\n2. **Source quality** - Find high-quality interview audio or clean clips\n3. **Consistency** - Use clips where they speak similarly\n4. **Multiple samples** - Very important for recognizable voices\n\n### Accent & Dialect\n\nThe model will preserve accent and dialect:\n\n- British English will generate British English\n- Southern accent will produce Southern accent\n- Regional pronunciations will be maintained\n\n### Emotion Transfer\n\nThe emotional tone of samples affects generation:\n\n- Energetic samples → Energetic output\n- Calm samples → Calm output\n- Mix samples for versatile profile\n\n## Managing Profiles\n\n### Organization\n\n- **Descriptive names** - \"John Smith - Professional Narrator\"\n- **Add descriptions** - Note recording conditions, use cases\n- **Language tags** - Mark the primary language\n- **Archive unused** - Keep profile list manageable\n\n### Export/Import\n\n- **Export** profiles to share or backup\n- **Import** from colleagues or teammates\n- Profiles include voice embeddings, not original audio\n\n## Next Steps\n\n<Cards>\n  <Card title=\"Generate Speech\" href=\"/overview/generating-speech\">\n    Use your profile to generate speech\n  </Card>\n  <Card title=\"Build Stories\" href=\"/overview/building-stories\">\n    Create multi-voice narratives\n  </Card>\n</Cards>\n"
  },
  {
    "path": "docs/content/docs/overview/docker.mdx",
    "content": "---\ntitle: \"Docker Deployment\"\ndescription: \"Run Voicebox as a headless server with a web UI using Docker\"\n---\n\n## Overview\n\nVoicebox can run as a Docker container with a full web UI -- no desktop app required. This is ideal for headless servers, shared GPU machines, or self-hosted deployments.\n\n## Quick Start\n\n```bash\ngit clone https://github.com/jamiepine/voicebox.git\ncd voicebox\ndocker compose up\n```\n\nOpen [http://localhost:17493](http://localhost:17493) in your browser. The full Voicebox UI is served directly from the backend.\n\n<Callout type=\"info\">\n  The first build takes a few minutes (compiling the frontend, installing Python dependencies). Subsequent starts are fast thanks to Docker layer caching.\n</Callout>\n\n## How It Works\n\nThe Docker image uses a 3-stage build:\n\n1. **Frontend** -- builds the React SPA with Bun and Vite\n2. **Backend** -- installs Python dependencies and TTS model packages\n3. **Runtime** -- combines both into a minimal image running the FastAPI server\n\nThe backend serves the web UI automatically when the built frontend is present. All API routes work exactly as they do in the desktop app.\n\n## Configuration\n\n### docker-compose.yml\n\nThe default `docker-compose.yml` binds to localhost only, mounts persistent volumes for data and model cache, and sets sensible resource limits:\n\n```yaml\nservices:\n  voicebox:\n    build: .\n    container_name: voicebox\n    restart: unless-stopped\n    ports:\n      - \"127.0.0.1:17493:17493\"\n    volumes:\n      - ./output:/app/data/generations\n      - voicebox-data:/app/data\n      - huggingface-cache:/home/voicebox/.cache/huggingface\n    environment:\n      - LOG_LEVEL=info\n    deploy:\n      resources:\n        limits:\n          cpus: '4'\n          memory: 8G\n```\n\n### Exposing to Your Network\n\nBy default the container only listens on `127.0.0.1`. To allow other machines on your network to connect, change the port binding:\n\n```yaml\nports:\n  - \"0.0.0.0:17493:17493\"\n```\n\n<Callout type=\"warn\">\n  The API has no built-in authentication. Only expose to trusted networks, or put a reverse proxy with auth in front of it.\n</Callout>\n\n### Environment Variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `LOG_LEVEL` | `info` | Logging verbosity (`debug`, `info`, `warning`, `error`) |\n| `VOICEBOX_MODELS_DIR` | (HuggingFace cache) | Custom path for model storage |\n| `VOICEBOX_CORS_ORIGINS` | (local origins) | Additional CORS origins, comma-separated |\n\n### Resource Limits\n\nThe default compose file limits the container to 4 CPUs and 8GB RAM. Adjust these based on your hardware:\n\n```yaml\ndeploy:\n  resources:\n    limits:\n      cpus: '8'\n      memory: 16G\n```\n\n<Callout type=\"info\">\n  TTS model inference is memory-intensive. 8GB is the minimum for running a single engine. 16GB+ is recommended if you want multiple engines loaded simultaneously.\n</Callout>\n\n## Volumes\n\n| Volume | Container Path | Purpose |\n|--------|---------------|---------|\n| `./output` | `/app/data/generations` | Generated audio files (bind-mount, easy access from host) |\n| `voicebox-data` | `/app/data` | Profiles, database, cache |\n| `huggingface-cache` | `/home/voicebox/.cache/huggingface` | Downloaded models (persists across rebuilds) |\n\nThe `huggingface-cache` volume is important -- without it, models would be re-downloaded every time the container is rebuilt.\n\n## GPU Acceleration\n\n### NVIDIA GPU (CUDA)\n\nTo use your NVIDIA GPU inside the container, install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) and add GPU access to your compose file:\n\n```yaml\nservices:\n  voicebox:\n    build: .\n    # ... existing config ...\n    deploy:\n      resources:\n        reservations:\n          devices:\n            - driver: nvidia\n              count: 1\n              capabilities: [gpu]\n```\n\n### AMD GPU (ROCm)\n\nFor AMD GPUs, use the ROCm runtime:\n\n```yaml\nservices:\n  voicebox:\n    build: .\n    # ... existing config ...\n    devices:\n      - /dev/kfd\n      - /dev/dri\n    group_add:\n      - video\n```\n\n### CPU Only\n\nThe default configuration runs on CPU. This works fine but generation will be slower. LuxTTS is the fastest engine on CPU (150x realtime).\n\n## Security\n\nThe Docker image follows security best practices:\n\n- **Non-root user** -- the server runs as `voicebox`, not `root`\n- **Localhost binding** -- only accessible from the host machine by default\n- **Health checks** -- automatic restart if the server hangs (`/health` endpoint polled every 30s)\n- **CORS restricted** -- only local origins allowed by default\n\n### Running Behind a Reverse Proxy\n\nFor production deployments, put Voicebox behind nginx or Caddy with TLS and authentication:\n\n```nginx\nserver {\n    listen 443 ssl;\n    server_name voicebox.example.com;\n\n    ssl_certificate /etc/ssl/certs/voicebox.pem;\n    ssl_certificate_key /etc/ssl/private/voicebox.key;\n\n    auth_basic \"Voicebox\";\n    auth_basic_user_file /etc/nginx/.htpasswd;\n\n    location / {\n        proxy_pass http://127.0.0.1:17493;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n    }\n}\n```\n\n## Troubleshooting\n\n### Container starts but UI shows JSON\n\nIf you see `{\"message\": \"voicebox API\", ...}` instead of the web UI, the frontend build may have failed during the Docker build. Check the build logs:\n\n```bash\ndocker compose build --no-cache\n```\n\nLook for errors in the \"Build frontend\" stage.\n\n### Models downloading on every restart\n\nMake sure the `huggingface-cache` volume is configured. Without it, the model cache is lost when the container stops:\n\n```yaml\nvolumes:\n  - huggingface-cache:/home/voicebox/.cache/huggingface\n```\n\n### Out of memory\n\nTTS models are large. If the container is killed by the OOM killer, increase the memory limit:\n\n```yaml\ndeploy:\n  resources:\n    limits:\n      memory: 16G\n```\n\n### Port already in use\n\n```bash\n# Check what's using port 17493\nlsof -i :17493\n\n# Or use a different port\nports:\n  - \"127.0.0.1:8080:17493\"\n```\n\n## Prebuilt Images (Coming Soon)\n\nWe plan to publish prebuilt Docker images to GitHub Container Registry so you won't need to build locally:\n\n```bash\n# Not available yet — coming in a future release\ndocker run -p 17493:17493 ghcr.io/jamiepine/voicebox:latest\n```\n\nThe CPU image will be ~3-4 GB (Python + PyTorch + TTS packages). A separate CUDA tag (~6-8 GB) will be available for NVIDIA GPU users. This is normal for ML containers.\n\nFor now, use `docker compose up` to build from source as described above.\n\n## Connecting the Desktop App\n\nYou can also use the desktop app as a frontend for a Docker-hosted backend. In the desktop app, go to **Settings -> Server**, enable **Remote Mode**, and enter `http://<server-ip>:17493`.\n\nSee the [Remote Mode guide](/overview/remote-mode) for details.\n"
  },
  {
    "path": "docs/content/docs/overview/generating-speech.mdx",
    "content": "---\ntitle: \"Generating Speech\"\ndescription: \"Generate high-quality speech from text\"\n---\n\n## Basic Generation\n\n<Steps>\n  <Step title=\"Select Profile\">\n    Choose a voice profile from the dropdown\n  </Step>\n  <Step title=\"Enter Text\">\n    Type or paste your text\n  </Step>\n  <Step title=\"Generate\">\n    Click **Generate** and wait a few seconds\n  </Step>\n  <Step title=\"Play & Export\">\n    Preview and download the result\n  </Step>\n</Steps>\n\n## Text Formatting Tips\n\nThe way you format text affects the output quality.\n\n### Punctuation\n\nUse proper punctuation for natural pauses:\n\n```\nGood: \"Hello! How are you today? I'm doing great.\"\nBad: \"Hello how are you today Im doing great\"\n```\n\n### Emphasis\n\nUse formatting to suggest emphasis:\n\n```\n- ALL CAPS for louder/emphasized: \"That was AMAZING!\"\n- Italics for subtle emphasis: \"I *really* enjoyed that\"\n- Bold for strong emphasis: \"This is **very** important\"\n```\n\n<Callout type=\"info\">\n  The model interprets these hints but results may vary.\n</Callout>\n\n## Advanced Features\n\n### Batch Generation\n\nFor long-form content, split into smaller chunks for better control and faster processing.\n\n### Voice Caching\n\nVoicebox caches voice prompts for faster re-generation with the same profile.\n\n## Coming Soon\n\n- Real-time streaming\n- Word-level timing control\n- Emotion and style controls\n- SSML support\n"
  },
  {
    "path": "docs/content/docs/overview/generation-history.mdx",
    "content": "---\ntitle: \"Generation History\"\ndescription: \"Track and manage all your generated audio\"\n---\n\n## Overview\n\nVoicebox keeps a complete history of all generated audio, making it easy to find, reuse, and manage your creations.\n\n## Features\n\n<Cards>\n  <Card title=\"Full History\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/></svg>}>\n    Every generation is automatically saved\n  </Card>\n  <Card title=\"Search & Filter\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><path d=\"m21 21-4.3-4.3\"/></svg>}>\n    Find by text, voice, or date\n  </Card>\n  <Card title=\"Re-generate\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></svg>}>\n    Regenerate any past generation with one click\n  </Card>\n  <Card title=\"Export\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><polyline points=\"7 10 12 15 17 10\"/><line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\"/></svg>}>\n    Download individual or batch exports\n  </Card>\n</Cards>\n\n## Viewing History\n\nNavigate to the **History** tab to see all your generations.\n\nEach entry shows:\n- Generated text\n- Voice profile used\n- Timestamp\n- Audio duration\n- Language\n\n## Actions\n\n### Play\nClick any generation to play it immediately.\n\n### Re-generate\nRegenerate with the same settings or modify the text/voice.\n\n### Download\nExport as WAV, MP3, or M4A.\n\n### Delete\nRemove unwanted generations to free up space.\n\n### Add to Story\nDrag generations to the Stories Editor timeline.\n\n## Search & Filter\n\n<Tabs items={[\"By Text\", \"By Voice\", \"By Date\"]}>\n  <Tab value=\"By Text\">\n    Search for specific text content\n    ```\n    \"Hello world\"\n    ```\n  </Tab>\n  <Tab value=\"By Voice\">\n    Filter by voice profile\n    ```\n    Select from dropdown\n    ```\n  </Tab>\n  <Tab value=\"By Date\">\n    Filter by date range\n    ```\n    Last 7 days, Last 30 days, Custom range\n    ```\n  </Tab>\n</Tabs>\n\n## Storage\n\nHistory is stored locally:\n\n- **macOS**: `~/Library/Application Support/com.voicebox.app/data/`\n- **Windows**: `%APPDATA%/com.voicebox.app/data/`\n- **Linux**: `~/.config/com.voicebox.app/data/`\n\n<Callout type=\"warn\">\n  Deleting the data directory will remove all history. Export important files first.\n</Callout>\n"
  },
  {
    "path": "docs/content/docs/overview/installation.mdx",
    "content": "---\ntitle: \"Installation\"\ndescription: \"Download and install Voicebox on macOS, Windows, or Linux\"\n---\n\n## Download\n\nVoicebox is available for macOS and Windows, with Linux builds coming soon.\n\n<Cards>\n  <Card title=\"macOS\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M12 2c-1.5 0-2.8.4-3.9 1.1A5.5 5.5 0 0 0 4 2.5C2.5 2.5 1 4 1 6c0 3.5 2.5 6 5 7.5C5 16 4 18 4 20c0 1.5.5 2.5 1.5 3C6.5 23.5 8 24 9.5 24c2 0 3.5-.5 5-2 1.5 1.5 3 2 5 2 1.5 0 3-.5 4-1 1-.5 1.5-1.5 1.5-3 0-2-1-4-2-6.5 2.5-1.5 5-4 5-7.5 0-2-1.5-3.5-3-3.5-.9 0-2.1.4-3.1 1.1A6.5 6.5 0 0 0 12 2Z\"/></svg>}>\n    Download for Apple Silicon or Intel Macs\n  </Card>\n  <Card title=\"Windows\" icon={<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><rect x=\"2\" y=\"3\" width=\"20\" height=\"14\" rx=\"2\" ry=\"2\"/><line x1=\"8\" y1=\"21\" x2=\"16\" y2=\"21\"/><line x1=\"12\" y1=\"17\" x2=\"12\" y2=\"21\"/></svg>}>\n    Download MSI installer or Setup executable\n  </Card>\n</Cards>\n\n### macOS\n\n<Tabs items={[\"Apple Silicon\", \"Intel\"]}>\n  <Tab value=\"Apple Silicon\">\n    Download: [voicebox_aarch64.app.tar.gz](https://github.com/jamiepine/voicebox/releases/latest/download/voicebox_aarch64.app.tar.gz)\n\n    ```bash\n    # Extract the archive\n    tar -xzf voicebox_aarch64.app.tar.gz\n\n    # Move to Applications\n    mv Voicebox.app /Applications/\n    ```\n  </Tab>\n  <Tab value=\"Intel\">\n    Download: [voicebox_x64.app.tar.gz](https://github.com/jamiepine/voicebox/releases/latest/download/voicebox_x64.app.tar.gz)\n\n    ```bash\n    # Extract the archive\n    tar -xzf voicebox_x64.app.tar.gz\n\n    # Move to Applications\n    mv Voicebox.app /Applications/\n    ```\n  </Tab>\n</Tabs>\n\n### Windows\n\n<Tabs items={[\"MSI Installer\", \"Setup Executable\"]}>\n  <Tab value=\"MSI Installer\">\n    Download: [voicebox_x64_en-US.msi](https://github.com/jamiepine/voicebox/releases/latest/download/voicebox_x64_en-US.msi)\n\n    Double-click the MSI file and follow the installation wizard.\n  </Tab>\n  <Tab value=\"Setup Executable\">\n    Download: [voicebox_x64-setup.exe](https://github.com/jamiepine/voicebox/releases/latest/download/voicebox_x64-setup.exe)\n\n    Run the executable and follow the installation wizard.\n  </Tab>\n</Tabs>\n\n### Linux\n\n<Callout type=\"info\">\n  Linux builds are coming soon. Currently blocked by GitHub runner disk space limitations.\n</Callout>\n\n## First Launch\n\nWhen you launch Voicebox for the first time:\n\n1. **Model Download** — Qwen3-TTS model (~2-4GB) will download automatically on first use\n2. **Data Directory** — Voice profiles and generated audio are stored in:\n   - macOS: `~/Library/Application Support/com.voicebox.app/`\n   - Windows: `%APPDATA%/com.voicebox.app/`\n   - Linux: `~/.config/com.voicebox.app/`\n\n3. **Backend Server** — The bundled Python server starts automatically\n\n<Callout type=\"info\">\n  First generation will be slower due to model downloads. Subsequent runs use cached models.\n</Callout>\n\n## System Requirements\n\n### Minimum\n\n- **OS:** macOS 11+, Windows 10+, or Linux\n- **RAM:** 8GB\n- **Storage:** 5GB free space (for models and data)\n- **CPU:** Modern multi-core processor\n\n### Recommended\n\n- **RAM:** 16GB+\n- **GPU:** CUDA-capable NVIDIA GPU (for faster generation)\n- **Storage:** 10GB+ free space\n\n<Callout type=\"info\">\n  CPU inference is supported but significantly slower than GPU. A CUDA-capable GPU is highly recommended for real-time workflows.\n</Callout>\n\n## Verification\n\nAfter installation, verify everything works:\n\n1. Launch Voicebox\n2. Check the server status indicator in the bottom-left corner (should be green)\n3. Navigate to **Profiles** and create a test profile\n4. Generate a short audio clip to verify the TTS engine works\n\n<Callout type=\"success\">\n  If you see a green status indicator and can generate audio, you're all set!\n</Callout>\n\n## Next Steps\n\n<Card title=\"Quick Start Guide\" href=\"/overview/quick-start\">\n  Create your first voice profile and generate speech\n</Card>\n"
  },
  {
    "path": "docs/content/docs/overview/introduction.mdx",
    "content": "---\ntitle: \"Introduction\"\ndescription: \"Voicebox is a local-first voice cloning studio -- a free and open-source alternative to ElevenLabs.\"\n---\n\n## What is Voicebox?\n\nVoicebox is a **local-first voice cloning studio** -- a free and open-source alternative to ElevenLabs. Clone voices from a few seconds of audio, generate speech in 23 languages across 5 TTS engines, apply post-processing effects, and compose multi-voice projects with a timeline editor.\n\n- **Complete privacy** -- models and voice data stay on your machine\n- **5 TTS engines** -- Qwen3-TTS, LuxTTS, Chatterbox Multilingual, Chatterbox Turbo, and HumeAI TADA\n- **23 languages** -- from English to Arabic, Japanese, Hindi, Swahili, and more\n- **Post-processing effects** -- pitch shift, reverb, delay, chorus, compression, and filters\n- **Expressive speech** -- paralinguistic tags like `[laugh]`, `[sigh]`, `[gasp]` via Chatterbox Turbo\n- **Unlimited length** -- auto-chunking with crossfade for scripts, articles, and chapters\n- **Stories editor** -- multi-track timeline for conversations, podcasts, and narratives\n- **API-first** -- REST API for integrating voice synthesis into your own projects\n- **Native performance** -- built with Tauri (Rust), not Electron\n- **Runs everywhere** -- macOS (MLX/Metal), Windows (CUDA), Linux, AMD ROCm, Intel Arc, Docker\n\n## TTS Engines\n\nFive engines with different strengths, switchable per-generation:\n\n| Engine | Languages | Strengths |\n|--------|-----------|-----------|\n| **Qwen3-TTS** (0.6B / 1.7B) | 10 | High-quality multilingual cloning, delivery instructions |\n| **LuxTTS** | English | Lightweight (~1GB VRAM), 48kHz output, 150x realtime on CPU |\n| **Chatterbox Multilingual** | 23 | Broadest language coverage |\n| **Chatterbox Turbo** | English | Fast 350M model with paralinguistic emotion/sound tags |\n| **TADA** (1B / 3B) | 10 | HumeAI speech-language model -- 700s+ coherent audio |\n\n## GPU Support\n\n| Platform | Backend | Notes |\n|----------|---------|-------|\n| macOS (Apple Silicon) | MLX (Metal) | 4-5x faster via Neural Engine |\n| Windows / Linux (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app |\n| Linux (AMD) | PyTorch (ROCm) | Auto-configures HSA_OVERRIDE_GFX_VERSION |\n| Windows (any GPU) | DirectML | Universal Windows GPU support |\n| Intel Arc | IPEX/XPU | Intel discrete GPU acceleration |\n| Any | CPU | Works everywhere, just slower |\n\n## Use Cases\n\n- **Game development** -- generate dynamic dialogue for characters\n- **Content creation** -- produce podcasts and video voiceovers\n- **Accessibility** -- build text-to-speech tools for users who need them\n- **Voice assistants** -- create custom voice interfaces\n- **Production pipelines** -- automate voiceover workflows via the REST API\n\n## Tech Stack\n\n| Layer | Technology |\n|-------|------------|\n| Desktop App | Tauri (Rust) |\n| Frontend | React, TypeScript, Tailwind CSS |\n| State | Zustand, React Query |\n| Backend | FastAPI (Python) |\n| TTS Engines | Qwen3-TTS, LuxTTS, Chatterbox, Chatterbox Turbo, TADA |\n| Effects | Pedalboard (Spotify) |\n| Transcription | Whisper / Whisper Turbo (PyTorch or MLX) |\n| Inference | MLX (Apple Silicon) / PyTorch (CUDA/ROCm/XPU/CPU) |\n| Database | SQLite |\n| Audio | WaveSurfer.js, librosa |\n"
  },
  {
    "path": "docs/content/docs/overview/meta.json",
    "content": "{\n  \"title\": \"Overview\",\n  \"defaultOpen\": true,\n  \"pages\": [\n    \"introduction\",\n    \"installation\",\n    \"docker\",\n    \"quick-start\",\n    \"voice-cloning\",\n    \"stories-editor\",\n    \"recording-transcription\",\n    \"generation-history\",\n    \"remote-mode\",\n    \"creating-voice-profiles\",\n    \"generating-speech\",\n    \"building-stories\",\n    \"troubleshooting\"\n  ]\n}\n"
  },
  {
    "path": "docs/content/docs/overview/quick-start.mdx",
    "content": "---\ntitle: \"Quick Start\"\ndescription: \"Get started with Voicebox in 5 minutes\"\n---\n\nThis guide will walk you through creating your first voice profile and generating speech.\n\n## Prerequisites\n\nMake sure you have [installed Voicebox](/overview/installation) and launched the app.\n\n## Step 1: Create a Voice Profile\n\nVoice profiles are the foundation of Voicebox. Each profile contains voice samples that the AI uses to clone the voice.\n\n<Steps>\n  <Step title=\"Navigate to Profiles\">\n    Click the **Profiles** tab in the sidebar\n  </Step>\n\n  <Step title=\"Create New Profile\">\n    Click the **+ New Profile** button\n\n    Fill in the details:\n    - **Name:** A descriptive name (e.g., \"John Smith\")\n    - **Language:** Select the primary language\n    - **Description:** Optional notes about the voice\n  </Step>\n\n  <Step title=\"Add Voice Sample\">\n    You have two options:\n\n    **Option A: Upload Audio**\n    - Click **Upload Sample**\n    - Select an audio file (WAV, MP3, or M4A)\n    - Ideal length: 10-30 seconds of clear speech\n\n    **Option B: Record Live**\n    - Click **Record Sample**\n    - Speak clearly for 10-30 seconds\n    - Click stop when finished\n  </Step>\n\n  <Step title=\"Save Profile\">\n    Click **Create Profile** to save\n  </Step>\n</Steps>\n\n<Callout type=\"info\">\n  For best results, use clean audio with minimal background noise and consistent speaking tone.\n</Callout>\n\n## Step 2: Generate Speech\n\nNow let's use your new voice profile to generate speech.\n\n<Steps>\n  <Step title=\"Go to Generation\">\n    Click the **Generate** tab in the sidebar\n  </Step>\n\n  <Step title=\"Select Voice Profile\">\n    Choose your newly created profile from the dropdown\n  </Step>\n\n  <Step title=\"Enter Text\">\n    Type or paste the text you want to generate:\n\n    ```\n    Hello! This is my first voice generation with Voicebox.\n    ```\n  </Step>\n\n  <Step title=\"Generate\">\n    Click **Generate** and wait a few seconds\n\n    <Callout type=\"info\">\n      First generation may take longer due to model initialization. Subsequent generations will be faster.\n    </Callout>\n  </Step>\n\n  <Step title=\"Play & Download\">\n    - Click **Play** to preview the audio\n    - Click **Download** to save the audio file\n    - The generation is also saved to your **History**\n  </Step>\n</Steps>\n\n## Step 3: Build a Story (Optional)\n\nThe Stories Editor lets you create multi-voice narratives with a timeline-based interface.\n\n<Steps>\n  <Step title=\"Create New Story\">\n    Navigate to **Stories** and click **+ New Story**\n  </Step>\n\n  <Step title=\"Add Voice Tracks\">\n    Click **+ Add Track** to create tracks for different speakers\n  </Step>\n\n  <Step title=\"Add Audio Clips\">\n    - Drag generated audio from your History\n    - Or generate new clips directly in the timeline\n    - Arrange clips on the timeline\n  </Step>\n\n  <Step title=\"Edit & Export\">\n    - Trim clips by dragging edges\n    - Adjust timing and spacing\n    - Click **Export** to render the final audio\n  </Step>\n</Steps>\n\n## What's Next?\n\n<Cards>\n  <Card title=\"Voice Cloning Guide\" href=\"/overview/creating-voice-profiles\">\n    Learn advanced techniques for high-quality voice cloning\n  </Card>\n  <Card title=\"API Integration\" href=\"/api-reference\">\n    Integrate Voicebox into your own applications\n  </Card>\n  <Card title=\"Stories Editor\" href=\"/overview/stories-editor\">\n    Master the multi-track timeline editor\n  </Card>\n  <Card title=\"Remote Mode\" href=\"/overview/remote-mode\">\n    Connect to a GPU server for faster generation\n  </Card>\n</Cards>\n\n## Tips for Success\n\n<AccordionGroup>\n  <Accordion title=\"Getting the Best Voice Quality\">\n    - Use 10-30 seconds of clear, consistent speech\n    - Avoid background noise and echo\n    - Multiple samples from the same speaker improve quality\n    - Match the speaking style you want to generate\n  </Accordion>\n\n  <Accordion title=\"Improving Generation Speed\">\n    - Use a CUDA-capable GPU for 5-10x faster generation\n    - Enable voice prompt caching for repeated generations\n    - Consider running the backend on a remote GPU server\n  </Accordion>\n\n  <Accordion title=\"Troubleshooting Common Issues\">\n    - **Server won't start:** Check if port 17493 is available\n    - **Poor audio quality:** Try adding more voice samples\n    - **Slow generation:** Verify GPU acceleration is enabled\n    - See the full [Troubleshooting Guide](/overview/troubleshooting) for more\n  </Accordion>\n</AccordionGroup>\n"
  },
  {
    "path": "docs/content/docs/overview/recording-transcription.mdx",
    "content": "---\ntitle: \"Recording & Transcription\"\ndescription: \"Record audio and transcribe speech with Whisper\"\n---\n\n## Recording\n\nVoicebox includes built-in recording capabilities for creating voice samples and capturing audio.\n\n### Features\n\n- **Microphone input** - Record from any audio input device\n- **System audio capture** - Record desktop audio (macOS/Windows)\n- **Waveform visualization** - See audio levels in real-time\n- **Multiple formats** - Export as WAV, MP3, or M4A\n\n### How to Record\n\n<Steps>\n  <Step title=\"Select Input\">\n    Choose your microphone or system audio\n  </Step>\n  <Step title=\"Start Recording\">\n    Click the record button and speak clearly\n  </Step>\n  <Step title=\"Stop & Save\">\n    Click stop when finished\n  </Step>\n  <Step title=\"Use or Export\">\n    Use as voice sample or export to file\n  </Step>\n</Steps>\n\n## Transcription\n\nAutomatic speech-to-text powered by OpenAI's Whisper model.\n\n### Features\n\n- **High accuracy** - Industry-leading speech recognition\n- **Multiple languages** - Supports 50+ languages\n- **Automatic detection** - Language auto-detection\n- **Timestamps** - Word-level timing information\n\n### How to Transcribe\n\n<Steps>\n  <Step title=\"Select Audio\">\n    Choose a recording or upload an audio file\n  </Step>\n  <Step title=\"Choose Language\">\n    Select language or use auto-detect\n  </Step>\n  <Step title=\"Transcribe\">\n    Click transcribe and wait for processing\n  </Step>\n  <Step title=\"Review & Export\">\n    Review text and export as needed\n  </Step>\n</Steps>\n\n<Callout type=\"info\">\n  Transcription is useful for creating voice samples from existing audio or generating subtitles.\n</Callout>\n"
  },
  {
    "path": "docs/content/docs/overview/remote-mode.mdx",
    "content": "---\ntitle: \"Remote Mode\"\ndescription: \"Connect to a GPU server for faster generation\"\n---\n\n## Overview\n\nRemote Mode allows you to run the Voicebox backend on a separate machine (like a GPU server) while using the desktop app on your local machine.\n\n## Use Cases\n\n- **No local GPU** - Use a cloud GPU or remote workstation\n- **Faster generation** - Leverage powerful remote hardware\n- **Shared infrastructure** - Multiple users connect to one server\n- **Laptop workflows** - Keep your laptop cool and battery-efficient\n\n## Architecture\n\nIn Remote Mode, the Voicebox desktop app (running on your local machine) communicates with the backend server (running on a remote machine) via HTTP. The local app provides only the user interface, while the remote server handles all the heavy processing including the TTS models, API endpoints, and audio generation.\n\n## Setting Up Remote Mode\n\n### On the Server\n\n<Steps>\n  <Step title=\"Install Dependencies\">\n    ```bash\n    # Clone the repo\n    git clone https://github.com/jamiepine/voicebox.git\n    cd voicebox/backend\n\n    # Install Python dependencies\n    pip install -r requirements.txt\n    pip install git+https://github.com/QwenLM/Qwen3-TTS.git\n    ```\n  </Step>\n\n  <Step title=\"Start the Server\">\n    ```bash\n    # Allow external connections\n    uvicorn main:app --host 0.0.0.0 --port 17493\n    ```\n\n    <Callout type=\"warn\">\n      This exposes the server to your network. Use a firewall or VPN for security.\n    </Callout>\n  </Step>\n\n  <Step title=\"Open Firewall\">\n    ```bash\n    # Ubuntu/Debian\n    sudo ufw allow 17493\n\n    # Or use your cloud provider's firewall settings\n    ```\n  </Step>\n</Steps>\n\n### On the Client\n\n<Steps>\n  <Step title=\"Open Settings\">\n    In Voicebox, go to **Settings → Server**\n  </Step>\n\n  <Step title=\"Enable Remote Mode\">\n    Toggle **Use Remote Server**\n  </Step>\n\n  <Step title=\"Enter Server URL\">\n    ```\n    http://<server-ip>:17493\n    ```\n\n    Replace `<server-ip>` with your server's IP address\n  </Step>\n\n  <Step title=\"Test Connection\">\n    Click **Test Connection** to verify\n  </Step>\n</Steps>\n\n## Cloud Deployment\n\n### AWS EC2\n\n```bash\n# Launch a GPU instance (e.g., g4dn.xlarge)\n# Install dependencies\n# Start server with --host 0.0.0.0\n```\n\n### Vast.ai\n\n```bash\n# Rent a GPU instance\n# SSH in and clone repo\n# Start server\n```\n\n### RunPod\n\n```bash\n# Deploy a pod with CUDA support\n# Install Voicebox backend\n# Expose port 17493\n```\n\n## Security Considerations\n\n<Callout type=\"warn\">\n  The API currently has no authentication. Only use on trusted networks or with a VPN.\n</Callout>\n\n**Best Practices:**\n- Use a VPN (WireGuard, Tailscale) instead of exposing to the internet\n- Run behind a reverse proxy with authentication (nginx + basic auth)\n- Use HTTPS with SSL certificates\n- Firewall rules to limit access to specific IPs\n\n## Performance\n\nExpected performance on various GPUs:\n\n| GPU | Generation Speed |\n|-----|------------------|\n| RTX 4090 | ~2-3s per 10 words |\n| RTX 3090 | ~3-4s per 10 words |\n| RTX 3060 | ~5-7s per 10 words |\n| CPU (12-core) | ~20-30s per 10 words |\n\n<Callout type=\"info\">\n  A GPU with 8GB+ VRAM is recommended for best performance.\n</Callout>\n\n## Troubleshooting\n\nSee the [Troubleshooting Guide](/guides/troubleshooting#remote-mode-issues) for common remote mode issues.\n"
  },
  {
    "path": "docs/content/docs/overview/stories-editor.mdx",
    "content": "---\ntitle: \"Stories Editor\"\ndescription: \"Create multi-voice narratives with a timeline-based editor\"\n---\n\n## Overview\n\nThe Stories Editor is a DAW-like timeline interface for creating multi-voice narratives, podcasts, and conversations.\n\n## Features\n\n<Cards>\n  <Card title=\"Multi-Track Timeline\">\n    Arrange multiple voice tracks in parallel\n  </Card>\n  <Card title=\"Inline Editing\">\n    Trim and split clips directly in the timeline\n  </Card>\n  <Card title=\"Auto-Playback\">\n    Preview with synchronized playhead\n  </Card>\n  <Card title=\"Voice Mixing\">\n    Build conversations with multiple speakers\n  </Card>\n</Cards>\n\n## Creating a Story\n\n<Steps>\n  <Step title=\"Create New Story\">\n    Navigate to **Stories** and click **+ New Story**\n  </Step>\n  <Step title=\"Add Tracks\">\n    Create separate tracks for each voice/speaker\n  </Step>\n  <Step title=\"Add Clips\">\n    - Drag from generation history\n    - Generate new clips inline\n    - Upload audio files\n  </Step>\n  <Step title=\"Arrange & Edit\">\n    - Position clips on timeline\n    - Trim clip edges\n    - Adjust spacing and timing\n  </Step>\n  <Step title=\"Export\">\n    Render the final mixed audio\n  </Step>\n</Steps>\n\n## Use Cases\n\n- **Podcasts**: Multi-host conversations\n- **Audiobooks**: Narrator + character voices\n- **Game Dialogue**: Character interactions\n- **Video Voiceovers**: Multiple speakers\n- **Audio Drama**: Full voice casts\n\n## Coming Soon\n\n- Word-level editing\n- Crossfades and transitions\n- Audio effects (reverb, EQ)\n- Real-time collaboration\n"
  },
  {
    "path": "docs/content/docs/overview/troubleshooting.mdx",
    "content": "---\ntitle: \"Troubleshooting\"\ndescription: \"Common issues and solutions for Voicebox\"\n---\n\nThis guide covers common issues you might encounter when using or developing Voicebox, along with solutions.\n\n## Installation Issues\n\n### macOS: \"App is damaged and can't be opened\"\n\nThis occurs because the app isn't signed with an Apple Developer certificate.\n\n**Solution:**\n```bash\n# Remove the quarantine attribute\nxattr -cr /Applications/Voicebox.app\n```\n\n### Windows: SmartScreen Warning\n\nWindows SmartScreen may warn that the app is unrecognized.\n\n**Solution:**\n- Click \"More info\"\n- Click \"Run anyway\"\n\n<Callout type=\"info\">\n  This is expected for unsigned applications. We're working on code signing for future releases.\n</Callout>\n\n## Server Issues\n\n### Backend Server Won't Start\n\n**Symptoms:**\n- Red status indicator in bottom-left corner\n- \"Failed to connect to server\" error\n\n**Solutions:**\n\n<AccordionGroup>\n  <Accordion title=\"Port Already in Use\">\n    Check if port 17493 is already in use:\n\n    ```bash\n    # macOS/Linux\n    lsof -i :17493\n\n    # Windows\n    powershell -Command \"Get-NetTCPConnection -LocalPort 17493 -State Listen\"\n    ```\n\n    Kill the process using the port:\n    ```bash\n    # macOS/Linux\n    kill -9 <PID>\n\n    # Windows\n    taskkill /PID <PID> /F\n    ```\n  </Accordion>\n\n  <Accordion title=\"Permission Issues\">\n    The server binary might not have execute permissions:\n\n    ```bash\n    # macOS/Linux\n    chmod +x ~/Library/Application\\ Support/com.voicebox.app/backend/voicebox-server\n    ```\n  </Accordion>\n\n  <Accordion title=\"Check Logs\">\n    View server logs for errors:\n\n    **macOS:**\n    ```bash\n    tail -f ~/Library/Application\\ Support/com.voicebox.app/logs/server.log\n    ```\n\n    **Windows:**\n    ```bash\n    type %APPDATA%\\com.voicebox.app\\logs\\server.log\n    ```\n  </Accordion>\n</AccordionGroup>\n\n### Connection Timeout\n\n**Symptoms:**\n- Long loading times\n- \"Connection timeout\" errors\n\n**Solution:**\n- Restart the app\n- Check your firewall settings\n- Ensure localhost is accessible\n\n## Generation Issues\n\n### First Generation is Very Slow\n\n**Symptoms:**\n- First generation takes 2-5 minutes\n- Progress indicator stuck at \"Loading model...\"\n\n**Explanation:**\nThis is expected behavior. The first generation downloads the Qwen3-TTS model (~2-4GB) and initializes it.\n\n**Solution:**\n- Wait for the initial download to complete\n- Subsequent generations will be much faster\n- Check your internet connection\n\n### Poor Voice Quality\n\n**Symptoms:**\n- Robotic or unnatural voice\n- Missing emotion or prosody\n- Pronunciation errors\n\n**Solutions:**\n\n<Steps>\n  <Step title=\"Improve Voice Samples\">\n    - Use 10-30 seconds of clear audio\n    - Avoid background noise\n    - Ensure consistent speaking tone\n    - Add multiple samples from the same speaker\n  </Step>\n\n  <Step title=\"Match Speaking Style\">\n    The generated voice will mimic the tone and style of your samples. If your sample is monotone, the generation will be too.\n  </Step>\n\n  <Step title=\"Adjust Text Formatting\">\n    - Use proper punctuation\n    - Add commas for natural pauses\n    - Capitalize proper nouns\n  </Step>\n</Steps>\n\n### Generation Fails with \"Out of Memory\"\n\n**Symptoms:**\n- Generation crashes\n- \"CUDA out of memory\" or \"RuntimeError: out of memory\"\n\n**Solutions:**\n\n<AccordionGroup>\n  <Accordion title=\"Free GPU Memory\">\n    Close other GPU-intensive applications:\n    - Games\n    - Video editors\n    - Multiple browser tabs with WebGL\n\n    Then restart Voicebox.\n  </Accordion>\n\n  <Accordion title=\"Use CPU Mode\">\n    If your GPU doesn't have enough VRAM (need 6GB+), use CPU mode:\n\n    Settings → Generation → Use CPU instead of GPU\n\n    <Callout type=\"warn\">\n      CPU generation is 5-10x slower but uses system RAM instead of VRAM.\n    </Callout>\n  </Accordion>\n\n  <Accordion title=\"Reduce Batch Size\">\n    For long text, split it into smaller chunks instead of generating all at once.\n  </Accordion>\n</AccordionGroup>\n\n## Audio Issues\n\n### No Audio Playback\n\n**Symptoms:**\n- Generated audio won't play\n- Playback button doesn't respond\n\n**Solutions:**\n- Check system audio settings\n- Ensure audio output device is connected\n- Try exporting and playing in a media player\n\n### Crackling or Distorted Audio\n\n**Symptoms:**\n- Audio has static or distortion\n- Clipping sounds\n\n**Solutions:**\n- Check if your input samples have distortion\n- Reduce playback volume\n- Re-generate with cleaner voice samples\n\n## Development Issues\n\n### Backend Won't Start in Dev Mode\n\n**Symptoms:**\n- `bun run dev:server` fails\n- Import errors or module not found\n\n**Solutions:**\n\n<AccordionGroup>\n  <Accordion title=\"Python Version\">\n    Ensure Python 3.11 or higher:\n\n    ```bash\n    python --version\n    ```\n\n    If not, install Python 3.11+ and recreate the virtual environment.\n  </Accordion>\n\n  <Accordion title=\"Virtual Environment\">\n    Ensure venv is activated:\n\n    ```bash\n    # macOS/Linux\n    source backend/venv/bin/activate\n\n    # Windows\n    backend\\venv\\Scripts\\activate\n    ```\n\n    You should see `(venv)` in your prompt.\n  </Accordion>\n\n  <Accordion title=\"Dependencies\">\n    Reinstall dependencies:\n\n    ```bash\n    cd backend\n    pip install -r requirements.txt\n    pip install git+https://github.com/QwenLM/Qwen3-TTS.git\n    ```\n  </Accordion>\n</AccordionGroup>\n\n### Tauri Build Fails\n\n**Symptoms:**\n- `bun run tauri build` fails\n- Rust compilation errors\n\n**Solutions:**\n\n```bash\n# Clean build artifacts\ncd tauri/src-tauri\ncargo clean\n\n# Update Rust\nrustup update\n\n# Try building again\ncd ../..\nbun run tauri build\n```\n\n### OpenAPI Client Generation Fails\n\n**Symptoms:**\n- `./scripts/generate-api.sh` fails\n- \"Failed to fetch schema\" error\n\n**Solutions:**\n\n<Steps>\n  <Step title=\"Ensure Backend is Running\">\n    ```bash\n    curl http://localhost:17493/openapi.json\n    ```\n\n    Should return JSON. If not, start the backend.\n  </Step>\n\n  <Step title=\"Check Port\">\n    Ensure nothing else is using port 17493\n  </Step>\n\n  <Step title=\"Regenerate Manually\">\n    ```bash\n    cd backend\n    source venv/bin/activate\n    uvicorn main:app --reload --port 17493\n\n    # In another terminal\n    ./scripts/generate-api.sh\n    ```\n  </Step>\n</Steps>\n\n## Database Issues\n\n### \"Database is locked\" Error\n\n**Symptoms:**\n- Profile or generation operations fail\n- SQLite lock errors\n\n**Solutions:**\n- Close all Voicebox instances\n- Delete the lock file:\n  ```bash\n  # macOS\n  rm ~/Library/Application\\ Support/com.voicebox.app/data/voicebox.db-shm\n  rm ~/Library/Application\\ Support/com.voicebox.app/data/voicebox.db-wal\n  ```\n\n### Corrupted Database\n\n**Symptoms:**\n- App crashes on launch\n- Data missing or corrupted\n\n**Solutions:**\n\n<Callout type=\"warn\">\n  This will delete all your voice profiles and generation history. Export important profiles first if possible.\n</Callout>\n\n```bash\n# macOS\nrm ~/Library/Application\\ Support/com.voicebox.app/data/voicebox.db\n\n# Windows\ndel %APPDATA%\\com.voicebox.app\\data\\voicebox.db\n```\n\nRestart the app to create a fresh database.\n\n## Model Issues\n\n### Model Download Fails\n\n**Symptoms:**\n- \"Failed to download model\" error\n- Stuck at \"Downloading...\"\n\n**Solutions:**\n- Check your internet connection\n- Check HuggingFace Hub status\n- Try using a VPN if HuggingFace is blocked in your region\n- Manually download and place in cache directory\n\n### Wrong Model Version\n\n**Symptoms:**\n- Generation quality suddenly degraded\n- Different voice output\n\n**Solutions:**\nClear the model cache and re-download:\n\n```bash\n# macOS\nrm -rf ~/.cache/huggingface/hub/models--Qwen*\n\n# Windows\nrmdir /s %USERPROFILE%\\.cache\\huggingface\\hub\\models--Qwen*\n```\n\n## Performance Issues\n\n### Slow Generation on GPU\n\n**Symptoms:**\n- Generation slower than expected\n- GPU not being utilized\n\n**Solutions:**\n\n<AccordionGroup>\n  <Accordion title=\"Verify CUDA Installation\">\n    ```bash\n    nvidia-smi\n    ```\n\n    Should show your GPU. If not, install CUDA drivers.\n  </Accordion>\n\n  <Accordion title=\"Check GPU Selection\">\n    If you have multiple GPUs, ensure Voicebox is using the right one.\n\n    Settings → Generation → GPU Device\n  </Accordion>\n\n  <Accordion title=\"Update GPU Drivers\">\n    Outdated drivers can cause performance issues. Update to the latest NVIDIA drivers.\n  </Accordion>\n</AccordionGroup>\n\n### High Memory Usage\n\n**Symptoms:**\n- App uses excessive RAM\n- System becomes sluggish\n\n**Solutions:**\n- Close unused voice profiles\n- Clear generation history\n- Restart the app periodically\n\n## Remote Mode Issues\n\n### Can't Connect to Remote Server\n\n**Symptoms:**\n- \"Connection refused\" error\n- Remote server not found\n\n**Solutions:**\n\n<Steps>\n  <Step title=\"Check Server Status\">\n    Ensure the remote server is running:\n\n    ```bash\n    curl http://<server-ip>:17493/health\n    ```\n  </Step>\n\n  <Step title=\"Check Firewall\">\n    Ensure port 17493 is open on the remote server:\n\n    ```bash\n    # Allow port on Ubuntu/Debian\n    sudo ufw allow 17493\n    ```\n  </Step>\n\n  <Step title=\"Verify Network\">\n    - Ensure both machines are on the same network (for local servers)\n    - Use IP address instead of hostname\n    - Try pinging the server: `ping <server-ip>`\n  </Step>\n</Steps>\n\n## Still Having Issues?\n\nIf you're still experiencing problems:\n\n1. **Check GitHub Issues:** [github.com/jamiepine/voicebox/issues](https://github.com/jamiepine/voicebox/issues)\n2. **Open a New Issue:** Provide:\n   - Operating system and version\n   - Voicebox version\n   - Steps to reproduce\n   - Error messages or logs\n3. **Join Discord:** [discord.gg/voicebox](https://discord.gg/voicebox) (coming soon)\n\n## Diagnostic Information\n\nWhen reporting issues, include this information:\n\n```bash\n# Voicebox version\n# Check Help → About in the app\n\n# Operating system\nuname -a  # macOS/Linux\nsysteminfo  # Windows\n\n# Python version (for dev issues)\npython --version\n\n# GPU info (if generation issues)\nnvidia-smi  # NVIDIA GPUs\n```\n\nFor more detailed troubleshooting, see the [TROUBLESHOOTING.md](https://github.com/jamiepine/voicebox/blob/main/docs/TROUBLESHOOTING.md) file in the repository.\n"
  },
  {
    "path": "docs/content/docs/overview/voice-cloning.mdx",
    "content": "---\ntitle: \"Voice Cloning\"\ndescription: \"Clone any voice from just a few seconds of audio\"\n---\n\n## Overview\n\nVoicebox uses **Qwen3-TTS** from Alibaba to achieve near-perfect voice cloning from just a few seconds of audio. The model captures prosody, emotion, and natural cadence.\n\n## How It Works\n\n<Steps>\n  <Step title=\"Upload or Record Sample\">\n    Provide 10-30 seconds of clear speech from the target voice\n  </Step>\n  <Step title=\"Model Analysis\">\n    Qwen3-TTS analyzes vocal characteristics, tone, and speaking patterns\n  </Step>\n  <Step title=\"Voice Profile Created\">\n    The model generates a voice embedding for synthesis\n  </Step>\n  <Step title=\"Generate Speech\">\n    Use the profile to generate any text in the cloned voice\n  </Step>\n</Steps>\n\n## Best Practices\n\n### Sample Quality\n\n<Cards>\n  <Card title=\"Do\">\n    - Use 10-30 seconds of audio\n    - Clear, consistent speaking\n    - Minimal background noise\n    - Natural speaking pace\n  </Card>\n  <Card title=\"Don't\">\n    - Very short clips (< 5 seconds)\n    - Heavy background noise\n    - Music or overlapping voices\n    - Heavily processed audio\n  </Card>\n</Cards>\n\n### Multiple Samples\n\nAdding multiple samples from the same speaker can improve quality:\n\n- Different speaking styles (casual, formal)\n- Different emotions (happy, serious)\n- Different recording conditions\n\n<Callout type=\"info\">\n  The model will learn a more robust representation from diverse samples.\n</Callout>\n\n## Supported Languages\n\nCurrently supported:\n- English\n- Chinese (Mandarin)\n\nMore languages coming soon.\n\n## Limitations\n\n<Callout type=\"warn\">\n  Voice cloning should only be used with consent. Ensure you have permission to clone someone's voice.\n</Callout>\n\n- Quality depends on sample clarity\n- Works best with consistent speaking tone\n- May struggle with extreme accents or speech impediments\n- Background noise reduces quality\n"
  },
  {
    "path": "docs/lib/cn.ts",
    "content": "export { twMerge as cn } from 'tailwind-merge';\n"
  },
  {
    "path": "docs/lib/layout.shared.tsx",
    "content": "import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';\n\nexport function baseOptions(): BaseLayoutProps {\n  return {\n    nav: {\n      title: 'Voicebox',\n    },\n  };\n}\n"
  },
  {
    "path": "docs/lib/openapi.ts",
    "content": "import { createOpenAPI } from 'fumadocs-openapi/server';\n\nexport const openapi = createOpenAPI({\n  input: ['./openapi.json'],\n});\n"
  },
  {
    "path": "docs/lib/source.ts",
    "content": "import { type InferPageType, loader } from 'fumadocs-core/source';\nimport { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';\nimport { docs } from '@/.source';\n\n// See https://fumadocs.dev/docs/headless/source-api for more info\nexport const source = loader({\n  baseUrl: '/',\n  source: docs.toFumadocsSource(),\n  plugins: [lucideIconsPlugin()],\n});\n\nexport function getPageImage(page: InferPageType<typeof source>) {\n  const segments = [...page.slugs, 'image.png'];\n\n  return {\n    segments,\n    url: `/og/docs/${segments.join('/')}`,\n  };\n}\n\nexport async function getLLMText(page: InferPageType<typeof source>) {\n  const processed = await page.data.getText('processed');\n\n  return `# ${page.data.title}\n\n${processed}`;\n}\n"
  },
  {
    "path": "docs/mdx-components.tsx",
    "content": "import { Callout } from 'fumadocs-ui/components/callout';\nimport { Card, Cards } from 'fumadocs-ui/components/card';\nimport { File, Files, Folder } from 'fumadocs-ui/components/files';\nimport { Step, Steps } from 'fumadocs-ui/components/steps';\nimport { Tab, Tabs } from 'fumadocs-ui/components/tabs';\nimport defaultMdxComponents from 'fumadocs-ui/mdx';\nimport type { MDXComponents } from 'mdx/types';\nimport type { ReactNode } from 'react';\nimport { APIPage } from '@/components/api-page';\n\n// Simple accordion using native HTML details/summary\nfunction AccordionGroup({ children }: { children: ReactNode }) {\n  return <div className=\"my-6 space-y-2\">{children}</div>;\n}\n\nfunction Accordion({ title, children }: { title: string; children: ReactNode }) {\n  return (\n    <details className=\"group border rounded-lg p-4\">\n      <summary className=\"cursor-pointer font-semibold list-none\">\n        <span className=\"group-open:rotate-90 transition-transform inline-block mr-2\">▶</span>\n        {title}\n      </summary>\n      <div className=\"mt-4 pl-6\">{children}</div>\n    </details>\n  );\n}\n\nexport function getMDXComponents(components?: MDXComponents): MDXComponents {\n  return {\n    ...defaultMdxComponents,\n    // Layout components\n    Card,\n    Cards,\n    // Files\n    Files,\n    Folder,\n    File,\n    // Callouts\n    Callout,\n    // Tabs\n    Tabs,\n    Tab,\n    // Steps\n    Steps,\n    Step,\n    // Accordion (native HTML-based)\n    AccordionGroup,\n    Accordion,\n    // OpenAPI component\n    APIPage,\n    ...components,\n  };\n}\n"
  },
  {
    "path": "docs/next.config.mjs",
    "content": "import { createMDX } from 'fumadocs-mdx/next';\n\nconst withMDX = createMDX();\n\n/** @type {import('next').NextConfig} */\nconst config = {\n  reactStrictMode: true,\n  async rewrites() {\n    return [\n      {\n        source: '/docs/:path*.mdx',\n        destination: '/llms.mdx/docs/:path*',\n      },\n    ];\n  },\n  webpack: (config) => {\n    config.experiments = {\n      ...config.experiments,\n      topLevelAwait: true,\n    };\n    return config;\n  },\n};\n\nexport default withMDX(config);\n"
  },
  {
    "path": "docs/notes/BACKEND_CODE_REVIEW.md",
    "content": "# Code Review: `backend/` Post-Refactor\n\n**Date:** 2026-03-16\n**Scope:** Full review of `backend/` after major refactor\n\n## Overall Assessment\n\nThe refactor is well-executed. The codebase follows a clean layered architecture (routes -> services -> backends) with good separation of concerns. The code is readable, the module boundaries are sensible, and the migration strategy is pragmatic for a desktop app. Below are findings organized by severity.\n\n---\n\n## Critical Issues\n\n### 1. Double `init_db()` in bundled entry point\n\n**File:** `server.py:261`\n\n`server.py:261` calls `database.init_db()` explicitly, but `app.py:140` also calls `database.init_db()` inside the `startup` event handler. When running via `server.py`, the database gets initialized twice -- once before uvicorn starts and once during the startup event. This is likely benign (idempotent migrations), but the second call recreates the engine and `SessionLocal`, which could cause subtle issues if any sessions were opened between the two calls.\n\n**Recommendation:** Remove the explicit `init_db()` call in `server.py:260-262` and rely solely on the startup event in `app.py`. The same issue exists in `main.py:38`.\n\n### 2. SSE endpoint holds DB session open indefinitely\n\n**File:** `routes/generations.py:179-212`\n\nThe `get_generation_status` SSE endpoint receives a `db` session via `Depends(get_db)` but keeps it open for the lifetime of the SSE stream (polling every 1 second). This ties up a SQLite connection for potentially minutes. With SQLite's single-writer model, this is a contention risk.\n\n**Recommendation:** Open and close a short-lived session on each poll iteration instead of holding one via dependency injection:\n\n```python\nasync def event_stream():\n    while True:\n        db = next(get_db())\n        try:\n            gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n            ...\n        finally:\n            db.close()\n        await asyncio.sleep(1)\n```\n\n---\n\n## High Severity\n\n### 3. `_save_retry` creates no version record\n\n**File:** `services/generation.py:201-214`\n\n`_save_retry` writes the audio file but creates no `GenerationVersion` entry. If the generation previously had versions (from an initial generate that failed mid-effects, for example), the retry result won't appear in the versions list. This creates an inconsistency: some generations have versions, retried ones don't.\n\n**Recommendation:** Create a \"clean\" version in `_save_retry` the same way `_save_generate` does.\n\n### 4. `datetime.utcnow()` is deprecated\n\n**File:** `services/stories.py` and others\n\n`datetime.utcnow()` is deprecated as of Python 3.12 and returns a naive datetime. Used throughout `services/stories.py` (lines 95, 96, 193, 307, 360, 404, 457, 529, 537, 598, 610, 652, 716, 775) and possibly other service files.\n\n**Recommendation:** Replace with `datetime.now(datetime.UTC)` or `datetime.now(timezone.utc)`.\n\n### 5. `list_stories` N+1 query\n\n**File:** `services/stories.py:122-132`\n\n`list_stories` issues one `COUNT(*)` query per story inside a loop. For N stories, that's N+1 queries.\n\n**Recommendation:** Use a subquery or a single aggregated query:\n\n```python\nfrom sqlalchemy import func\ncounts = dict(\n    db.query(DBStoryItem.story_id, func.count(DBStoryItem.id))\n    .group_by(DBStoryItem.story_id)\n    .all()\n)\n```\n\n---\n\n## Medium Severity\n\n### 6. `create_story` queries item count immediately after creation\n\n**File:** `services/stories.py:103`\n\nLine 103 queries the item count for a story that was just created -- it will always be 0. This is wasted I/O.\n\n### 7. Bare `except Exception` with silent `pass`\n\n**File:** `routes/generations.py:69-70`\n\nWhen parsing a profile's stored `effects_chain` JSON, exceptions are silently swallowed. A corrupt JSON blob would result in no effects being applied with no logging.\n\n**Recommendation:** Log the exception at warning level.\n\n### 8. `update_story_item_times` uses `generation_id` as key\n\n**File:** `services/stories.py:643-649`\n\n`item_map` is keyed by `generation_id`, but the same generation can appear in a story multiple times (via split/duplicate). This would cause key collisions, and only the last item per generation_id would be updatable.\n\n**Recommendation:** Key by `item_id` instead, and change the `StoryItemUpdateTime` model to use `item_id`.\n\n### 9. Thread safety gap in `get_stt_backend`\n\n**File:** `backends/__init__.py:499-520`\n\n`get_stt_backend()` uses no locking (unlike `get_tts_backend_for_engine` which uses `_tts_backends_lock`). A race condition could create duplicate STT backend instances.\n\n**Recommendation:** Add a lock or use the same double-checked locking pattern.\n\n### 10. Unused `_tts_backend` global\n\n**File:** `backends/__init__.py:156`\n\n`_tts_backend` is declared but never read or written outside of `reset_backends()`. All TTS access goes through `_tts_backends` dict. Dead code.\n\n### 11. `trim_story_item` returns `None` for validation errors\n\n**File:** `services/stories.py:448`\n\nReturning `None` for \"item not found\" and \"invalid trim values\" is ambiguous. The route handler can't distinguish between a 404 and a 400 response.\n\n**Recommendation:** Raise specific exceptions (e.g., `ValueError` for invalid trim) so the route can return the appropriate HTTP status.\n\n### 12. `load_engine_model` calls different method names\n\n**File:** `backends/__init__.py:340-346`\n\nFor Qwen, it calls `load_model_async(model_size)`. For others, it calls `load_model()` with no arguments. But the `TTSBackend` protocol defines `load_model(self, model_size: str)`. This means the protocol signature doesn't match actual usage for either path.\n\n**Recommendation:** Align the protocol definition with actual backend implementations, or add `load_model_async` to the protocol.\n\n---\n\n## Low Severity / Style\n\n### 13. Inconsistent `async` usage in services\n\nFunctions like `create_story`, `list_stories`, etc. in `services/stories.py` are `async def` but contain no `await` expressions. They do synchronous SQLAlchemy I/O. While this works (the functions are awaitable), it's misleading -- these will block the event loop during DB access.\n\nThis is a known tradeoff with synchronous SQLAlchemy + FastAPI, and acceptable for a single-user desktop app with SQLite, but worth noting for documentation.\n\n### 14. `getattr(item, \"version_id\", None)` pattern\n\n**File:** `services/stories.py:57, 504, 524, etc.`\n\nMultiple places use `getattr(item, \"version_id\", None)` on a DB model that has `version_id` as a declared column (from migrations). After the migration runs, this is always a real attribute. The defensive `getattr` is cargo-culted.\n\n**Recommendation:** Access `item.version_id` directly. If the column is missing, the ORM will raise a clear error.\n\n### 15. `reorder_story_items` ignores trim values\n\n**File:** `services/stories.py:707`\n\nWhen recalculating timecodes, it uses the full `generation.duration` rather than the effective (trimmed) duration. Trimmed items will have larger gaps than intended.\n\n### 16. Module-level `import torch` in `app.py:44`\n\n`import torch` at module level in `app.py` means torch loads on every import of the app module. This is intentional (AMD env vars must be set first), but the comment on line 38 should mention that this is why the import is here and not at the top.\n\n### 17. f-strings in logging in `server.py`\n\n`server.py` uses f-strings in logging calls (e.g., lines 63-66, 252, 256, 264). This evaluates the string even when the log level is filtered out. The rest of the codebase correctly uses `%s` style (e.g., `app.py:131`).\n\n---\n\n## Architecture Observations (Not Issues)\n\n- **Clean layered design**: routes -> services -> backends with Pydantic models as the API contract.\n- **Backend abstraction** with `Protocol` classes and a config registry is a solid pattern.\n- **Serial generation queue** (`task_queue.py`) is simple and effective for single-GPU serialization.\n- **Migration approach** is pragmatic for the use case. The idempotent, check-then-act pattern is reliable.\n- **The `generation.py` refactor** (collapsing three closures into `run_generation` with a mode parameter) is a clear improvement.\n\n---\n\n## Summary\n\n| Severity | Count |\n|----------|-------|\n| Critical | 2 |\n| High | 3 |\n| Medium | 7 |\n| Low/Style | 5 |\n\nThe refactor achieved its goals: clear module boundaries, reduced duplication (especially in `generation.py`), and a well-organized backend abstraction. The critical items (double init_db and SSE session leak) should be addressed first, followed by the version consistency issue in retry and the N+1 query.\n"
  },
  {
    "path": "docs/notes/MIGRATION.md",
    "content": "# Documentation Migration: Mintlify → Fumadocs\n\nThis document summarizes the migration of documentation from `/docs` (Mintlify) to `/docs2` (Fumadocs).\n\n## What Was Done\n\n### 1. Files Copied\n- ✅ All 29 MDX files from `/docs` folders (overview, api, developer, plans)\n- ✅ All 4 root-level markdown files (AUTOUPDATER.md, AUTOUPDATER_QUICKSTART.md, TROUBLESHOOTING.md, README.md)\n- ✅ All images (3 webp files) → `public/images/`\n- ✅ All logo files (2 png files) → `public/logo/`\n\n### 2. Component Migration\nCreated compatibility layer in `components/mintlify-compat.tsx` that maps Mintlify components to Fumadocs equivalents:\n\n- `<Frame>` → Simple div wrapper (images are zoomable by default in Fumadocs)\n- `<CardGroup>` → `<Cards>` (Fumadocs component)\n- `<Card>` → `<Card>` (with icon string → Lucide icon mapping)\n- `<Steps>` / `<Step>` → Direct mapping to Fumadocs components\n- `<Tip>`, `<Note>`, `<Info>` → `<Callout type=\"info\">`\n- `<Warning>` → `<Callout type=\"warn\">`\n- `<Danger>` → `<Callout type=\"error\">`\n- `<AccordionGroup>` / `<Accordion>` → HTML `<details>` / `<summary>` elements\n\n### 3. Navigation Structure\nCreated `meta.json` files for each folder:\n- `content/docs/meta.json` - Root documentation\n- `content/docs/overview/meta.json` - Overview pages\n- `content/docs/api/meta.json` - API reference\n- `content/docs/developer/meta.json` - Developer docs\n- `content/docs/plans/meta.json` - Plans/roadmap\n\n### 4. Link Fixes\n- Fixed incorrect `/guides/...` paths → `/overview/...`\n- All internal links now use correct paths\n\n### 5. Branding\n- Updated `lib/layout.shared.tsx` to use \"Voicebox\" as the nav title\n\n## File Structure\n\n```\ndocs2/\n├── components/\n│   └── mintlify-compat.tsx    # Mintlify → Fumadocs component mappings\n├── content/docs/\n│   ├── meta.json              # Root navigation\n│   ├── overview/              # 12 MDX files\n│   ├── api/                   # 5 MDX files\n│   ├── developer/             # 12 MDX files\n│   ├── plans/                 # 4 MD files\n│   └── *.md                   # 4 root markdown files\n├── public/\n│   ├── images/                # 3 webp files\n│   └── logo/                  # 2 png files\n└── mdx-components.tsx         # MDX component configuration\n```\n\n## Icon Mapping\n\nThe following icon strings are mapped to Lucide icons:\n- `microphone` → Mic\n- `film` → Film\n- `code` → Code\n- `shield` → Shield\n- `download` → Download\n- `rocket` → Rocket\n- `apple` → Apple\n- `windows` → Windows\n- `server` → Server\n- `user` → User\n- `waveform` → Waveform\n\n## Next Steps\n\n1. **Test the build**: Run `npm run build` (requires Node.js >= 20.9.0)\n2. **Start dev server**: Run `npm run dev` to preview\n3. **Customize styling**: Update `app/global.css` if needed\n4. **Add more icons**: Extend `iconMap` in `mintlify-compat.tsx` as needed\n5. **Review navigation**: Adjust `meta.json` files to customize page order\n\n## Notes\n\n- Image paths (`/images/...`) work as-is since Next.js serves from `public/`\n- All Mintlify components are now compatible with Fumadocs\n- Navigation structure follows Fumadocs conventions\n- No breaking changes to content - all MDX files work with compatibility layer\n"
  },
  {
    "path": "docs/notes/PROJECT_STATUS.md",
    "content": "# Voicebox Project Status & Roadmap\n\n> Last updated: 2026-03-18 | Current version: **v0.3.0** | 13.4k stars | ~136 open issues | 9 open PRs\n\n---\n\n## Table of Contents\n\n1. [Architecture Overview](#architecture-overview)\n2. [Current State](#current-state)\n3. [Open PRs — Triage & Analysis](#open-prs--triage--analysis)\n4. [Open Issues — Categorized](#open-issues--categorized)\n5. [Existing Plan Documents — Status](#existing-plan-documents--status)\n6. [New Model Integration — Landscape](#new-model-integration--landscape)\n7. [Architectural Bottlenecks](#architectural-bottlenecks)\n8. [Recommended Priorities](#recommended-priorities)\n\n---\n\n## Architecture Overview\n\n```\n┌─────────────────────────────────────────────────────┐\n│  Tauri Shell (Rust)                                 │\n│  ┌───────────────────────────────────────────────┐  │\n│  │  React Frontend (app/)                        │  │\n│  │  Zustand stores · API client · Generation UI  │  │\n│  │  Stories Editor · Voice Profiles · Model Mgmt │  │\n│  └──────────────────────┬────────────────────────┘  │\n│                         │ HTTP :17493                │\n│  ┌──────────────────────▼────────────────────────┐  │\n│  │  FastAPI Backend (backend/)                   │  │\n│  │  ┌─────────────────────────────────────────┐  │  │\n│  │  │ TTSBackend Protocol                     │  │  │\n│  │  │  ┌──────────┐ ┌───────┐ ┌───────────┐  │  │  │\n│  │  │  │ Qwen3-TTS│ │LuxTTS │ │Chatterbox │  │  │  │\n│  │  │  │(Py/MLX)  │ │       │ │(MTL+Turbo)│  │  │  │\n│  │  │  └──────────┘ └───────┘ └───────────┘  │  │  │\n│  │  │  ┌──────────┐                           │  │  │\n│  │  │  │ TADA     │                           │  │  │\n│  │  │  │(1B / 3B) │                           │  │  │\n│  │  │  └──────────┘                           │  │  │\n│  │  └─────────────────────────────────────────┘  │  │\n│  │  ┌───────────┐  ┌─────────┐                   │  │\n│  │  │ STTBackend│  │ Profiles│                   │  │\n│  │  │ (Whisper) │  │ History │                   │  │\n│  │  └───────────┘  │ Stories │                   │  │\n│  │                  └─────────┘                   │  │\n│  └───────────────────────────────────────────────┘  │\n└─────────────────────────────────────────────────────┘\n```\n\n### Key Files\n\n| Layer | File | Purpose |\n|-------|------|---------|\n| Backend entry | `backend/main.py` | FastAPI app, all API routes (~2850 lines) |\n| TTS protocol | `backend/backends/__init__.py:32-101` | `TTSBackend` Protocol definition |\n| Model registry | `backend/backends/__init__.py:17-29,153-366` | `ModelConfig` dataclass + registry helpers |\n| TTS factory | `backend/backends/__init__.py:382-426` | Thread-safe engine registry (double-checked locking) |\n| PyTorch TTS | `backend/backends/pytorch_backend.py` | Qwen3-TTS via `qwen_tts` package |\n| MLX TTS | `backend/backends/mlx_backend.py` | Qwen3-TTS via `mlx_audio.tts` |\n| LuxTTS | `backend/backends/luxtts_backend.py` | LuxTTS — fast, CPU-friendly |\n| Chatterbox MTL | `backend/backends/chatterbox_backend.py` | Chatterbox Multilingual — 23 languages |\n| Chatterbox Turbo | `backend/backends/chatterbox_turbo_backend.py` | Chatterbox Turbo — English, paralinguistic tags |\n| TADA | `backend/backends/hume_backend.py` | HumeAI TADA — 1B English + 3B Multilingual |\n| Platform detect | `backend/platform_detect.py` | Apple Silicon → MLX, else → PyTorch |\n| API types | `backend/models.py` | Pydantic request/response models |\n| HF progress | `backend/utils/hf_progress.py` | HFProgressTracker (tqdm patching for download progress) |\n| Audio utils | `backend/utils/audio.py` | `trim_tts_output()`, normalize, load/save audio |\n| Frontend API | `app/src/lib/api/client.ts` | Hand-written fetch wrapper |\n| Frontend types | `app/src/lib/api/types.ts` | TypeScript API types |\n| Engine selector | `app/src/components/Generation/EngineModelSelector.tsx` | Shared engine/model dropdown |\n| Generation form | `app/src/components/Generation/GenerationForm.tsx` | TTS generation UI |\n| Floating gen box | `app/src/components/Generation/FloatingGenerateBox.tsx` | Compact generation UI |\n| Model manager | `app/src/components/ServerSettings/ModelManagement.tsx` | Model download/status/progress UI |\n| GPU acceleration | `app/src/components/ServerSettings/GpuAcceleration.tsx` | CUDA backend swap UI |\n| Gen form hook | `app/src/lib/hooks/useGenerationForm.ts` | Form validation + submission |\n| Language constants | `app/src/lib/constants/languages.ts` | Per-engine language maps |\n\n### How TTS Generation Works (Current Flow)\n\n```\nPOST /generate\n  1. Look up voice profile from DB\n  2. Resolve engine from request (qwen | luxtts | chatterbox | chatterbox_turbo | tada)\n  3. Get backend: get_tts_backend_for_engine(engine)  # thread-safe singleton per engine\n  4. Check model cache → if missing, trigger background download, return HTTP 202\n  5. Load model (lazy): tts_backend.load_model(model_size)\n  6. Create voice prompt: profiles.create_voice_prompt_for_profile(engine=engine)\n       → tts_backend.create_voice_prompt(audio_path, reference_text)\n  7. Generate: tts_backend.generate(text, voice_prompt, language, seed, instruct)\n  8. Post-process: trim_tts_output() for Chatterbox engines\n  9. Save WAV → data/generations/{id}.wav\n  10. Insert history record in SQLite\n  11. Return GenerationResponse\n```\n\n---\n\n## Current State\n\n### What's Shipped (v0.3.0)\n\n**Core TTS:**\n- Qwen3-TTS voice cloning (1.7B and 0.6B models)\n- MLX backend for Apple Silicon, PyTorch for everything else\n- Multi-engine TTS architecture with thread-safe backend registry (PR #254)\n- LuxTTS integration — fast, CPU-friendly English TTS (PR #254)\n- Chatterbox Multilingual TTS — 23 languages including Hebrew (PR #257)\n- Chatterbox Turbo — paralinguistic tags, low latency English (PR #258)\n- HumeAI TADA integration — 1B English + 3B Multilingual speech-language model (PR #296)\n- Chunked TTS generation for long text — engine-agnostic, removes ~500 char limit (PR #266)\n- Async generation queue (PR #269)\n- Post-processing audio effects system (PR #271)\n- Centralized model config registry (`ModelConfig` dataclass) — no per-engine dispatch maps\n- Shared `EngineModelSelector` component — engine/model dropdown defined once, used in both generation forms\n\n**Infrastructure:**\n- CUDA backend swap via binary download and restart (PR #252), upgraded to cu128 (PR #316)\n- CUDA backend split into independently versioned server + libs archives (PR #298)\n- Docker + web deployment (PR #161)\n- Backend refactor: modular architecture, style guide, tooling (PR #285)\n- Settings overhaul: routed sub-tabs, server logs, changelog, about page (PR #294)\n- Windows support: CUDA detection, cross-platform justfile, clean server shutdown (PR #272)\n- Voice profiles with multi-sample support\n- Stories editor (multi-track DAW timeline)\n- Whisper transcription (base, small, medium, large variants)\n- Model management UI with inline download progress bars + folder migration (PR #268)\n- Download cancel/clear UI with error panel (PR #238)\n- Generation history with caching\n- Streaming generation endpoint (MLX only)\n- Audio player freeze fix + UX improvements (PR #293)\n- CORS restriction to known local origins (PR #88)\n\n### Abandoned Integrations\n\n| Model | PR | Reason |\n|-------|----|--------|\n| **CosyVoice2/3** | PR #311 | Output quality too poor. Heavy deps, no PyPI, needed 5+ shims. |\n\n### What's In-Flight\n\n| Feature | Branch/PR | Status |\n|---------|-----------|--------|\n| Kokoro 82M TTS engine | WIP | In development — 82M CPU-realtime engine, 8 languages |\n\n### TTS Engine Comparison\n\n| Engine | Model Name | Languages | Size | Key Features | Instruct Support |\n|--------|-----------|-----------|------|-------------|-----------------|\n| Qwen3-TTS 1.7B | `qwen-tts-1.7B` | 10 (zh, en, ja, ko, de, fr, ru, pt, es, it) | ~3.5 GB | Highest quality, voice cloning | None (Base model has no instruct path) |\n| Qwen3-TTS 0.6B | `qwen-tts-0.6B` | 10 | ~1.2 GB | Lighter, faster | None |\n| LuxTTS | `luxtts` | English | ~300 MB | CPU-friendly, 48 kHz, fast | None |\n| Chatterbox | `chatterbox-tts` | 23 (incl. Hebrew, Arabic, Hindi, etc.) | ~3.2 GB | Zero-shot cloning, multilingual | Partial — `exaggeration` float (0-1) for expressiveness |\n| Chatterbox Turbo | `chatterbox-turbo` | English | ~1.5 GB | Paralinguistic tags ([laugh], [cough]), 350M params, low latency | Partial — inline tags only, no separate instruct param |\n| TADA 1B | `tada-1b` | English | ~4 GB | HumeAI speech-language model, 700s+ coherent audio | None |\n| TADA 3B Multilingual | `tada-3b-ml` | 10 (en, ar, zh, de, es, fr, it, ja, pl, pt) | ~8 GB | Multilingual, text-acoustic dual alignment | None |\n| Kokoro 82M | `kokoro` | 8 (en, es, fr, hi, it, pt, ja, zh) | ~350 MB | 82M params, CPU realtime, Apache 2.0, pre-built voices | None |\n\n### Multi-Engine Architecture (Shipped)\n\nThe singleton TTS backend blocker described in the previous version of this doc has been **resolved**. The architecture now supports:\n\n- **Thread-safe backend registry** (`_tts_backends` dict + `_tts_backends_lock`) with double-checked locking\n- **Per-engine backend instances** — each engine gets its own singleton, loaded lazily\n- **Engine field on GenerationRequest** — frontend sends `engine: 'qwen' | 'luxtts' | 'chatterbox' | 'chatterbox_turbo' | 'tada'`\n- **Per-engine language filtering** — `ENGINE_LANGUAGES` map in frontend, backend regex accepts all languages\n- **Per-engine voice prompts** — `create_voice_prompt_for_profile()` dispatches to the correct backend\n- **Trim post-processing** — `trim_tts_output()` for Chatterbox engines (cuts trailing silence/hallucination)\n\n### Known Limitations\n\n- **HF XET progress**: Large files downloaded via `hf-xet` (HuggingFace's new transfer backend) report `n=0` in tqdm updates. Progress bars may appear stuck for large `.safetensors` files even though the download is proceeding. This is a known upstream limitation.\n- **Chatterbox Turbo upstream token bug**: `from_pretrained()` passes `token=os.getenv(\"HF_TOKEN\") or True` which fails without a stored HF token. Our backend works around this by calling `snapshot_download(token=None)` + `from_local()`.\n- **chatterbox-tts must install with `--no-deps`**: It pins `numpy<1.26`, `torch==2.6.0`, `transformers==4.46.3` — all incompatible with our stack (Python 3.12, torch 2.10, transformers 4.57.3). Sub-deps listed explicitly in `requirements.txt`.\n- **Instruct parameter is non-functional** (#224): The UI exposes an instruct text field, but it's silently dropped by every backend. The Qwen3-TTS Base model we ship only supports voice cloning — instruct requires the separate CustomVoice model variant (`Qwen3-TTS-12Hz-1.7B-CustomVoice`), which uses predefined speakers instead of ref audio. The instruct UI should be hidden until a backend with real support is integrated.\n- **Streaming generation** only works for Qwen on MLX. Other engines use the non-streaming `/generate` endpoint.\n- **dicta-onnx** (Hebrew diacritization) not included — upstream Chatterbox bug requires `model_path` arg but calls `Dicta()` with none. Hebrew works fine without it.\n\n---\n\n## Open PRs — Triage & Analysis\n\n### Recently Merged (Since Last Update)\n\n| PR | Title | Merged |\n|----|-------|--------|\n| **#316** | Upgrade CUDA backend from cu126 to cu128, fix GPU settings UI | 2026-03-18 |\n| **#305** | fix: bundle qwen_tts source files in PyInstaller build | 2026-03-17 |\n| **#298** | feat: split CUDA backend into independently versioned server + libs archives | 2026-03-17 |\n| **#296** | Add HumeAI TADA TTS engine (1B English + 3B Multilingual) | 2026-03-17 |\n| **#295** | fix: batch of bug fixes from issue tracker | 2026-03-17 |\n| **#293** | Fix audio player freezing and improve UX | 2026-03-17 |\n| **#294** | Settings overhaul: routed sub-tabs, server logs, changelog, about page | 2026-03-16 |\n| **#288** | Better docs | 2026-03-16 |\n| **#285** | Backend refactor: modular architecture, style guide, tooling | 2026-03-16 |\n| **#274** | Landing page v0.2.0 redesign | 2026-03-15 |\n| **#272** | Windows support: CUDA detection, cross-platform justfile, clean server shutdown | 2026-03-15 |\n| **#271** | Add post-processing audio effects system | 2026-03-14 |\n| **#269** | feat: async generation queue | 2026-03-13 |\n| **#268** | feat: model management improvements and folder migration | 2026-03-13 |\n| **#266** | feat: chunked TTS generation for long text (engine-agnostic) | 2026-03-13 |\n| **#265** | feat: paralinguistic tag autocomplete for Chatterbox Turbo | 2026-03-13 |\n| **#264** | fix: Chatterbox float64 dtype mismatch + model unload button | 2026-03-13 |\n| **#258** | feat: Chatterbox Turbo engine + per-engine language lists | 2026-03-13 |\n| **#230** | docs: fix README grammar | 2026-03-13 |\n| **#161** | feat: Docker + web deployment | 2026-03-13 |\n| **#88** | security: restrict CORS to known local origins | 2026-03-13 |\n\n### Currently Open (9 PRs)\n\n| PR | Title | Status | Notes |\n|----|-------|--------|-------|\n| **#311** | feat: add CosyVoice2/3 TTS engine | **Will close** | Model quality too poor. See Abandoned Integrations. |\n| **#253** | Enhance speech tokenizer with 48kHz version | Community PR | Qwen tokenizer upgrade. Worth reviewing. |\n| **#237** | fix: bundle qwen_tts source files in PyInstaller | Superseded | Our PR #305 shipped this. Can close. |\n| **#227** | fix: harden input validation & file safety | Community PR | Coupled to #225 (custom models). |\n| **#225** | feat: custom HuggingFace model support | Community PR | Needs rework for multi-engine arch. |\n| **#218** | fix: unify qwen tts cache dir on Windows | Community PR | Windows-specific path fix. Still relevant. |\n| **#195** | feat: per-profile LoRA fine-tuning | Draft | Complex. 15 new endpoints. |\n| **#154** | feat: Audiobook tab | Community PR | Chunked generation now shipped (#266). |\n| **#91** | fix: CoreAudio device enumeration | Draft | macOS audio device handling. |\n\n---\n\n## Open Issues — Categorized\n\n### GPU / Hardware Detection (19 issues)\n\nThe single most reported category. Users on Windows with NVIDIA GPUs frequently report \"GPU not detected.\"\n\n**Root causes (likely):**\n- PyInstaller binary doesn't bundle CUDA correctly → falls back to CPU\n- DirectML/Vulkan path not implemented (AMD on Windows)\n- Binary size limit means CUDA can't ship in the main release\n\n**Key issues:** #239, #222, #220, #217, #208, #198, #192, #167, #164, #141, #130, #127\n\n**Fix path:** PR #252 (CUDA backend swap) is now merged. Users can download the CUDA binary separately from the GPU acceleration settings. Many of these issues may now be resolvable — needs triage to confirm.\n\n### Model Downloads (20 issues)\n\nSecond most reported. Users get stuck downloads, can't resume, no offline fallback.\n\n**Key issues:** #249, #240, #221, #216, #212, #181, #180, #159, #150, #149, #145, #143, #135, #134\n\n**Fix path:** PR #238 (cancel/clear UI) is now merged. PR #152 (offline crash fix) still open. Inline progress bars now show for all engines. Resume support not yet addressed.\n\n### Language Requests (18 issues)\n\nStrong demand for: Hindi (#245), Indonesian (#247), Dutch (#236), Hebrew (#199), Greek (#188), Portuguese (#183), Persian (#162), and many more.\n\n**Key issues:** #247, #245, #236, #211, #205, #199, #189, #188, #187, #183, #179, #162\n\n**Fix path:** Chatterbox Multilingual (merged via #257) now supports 23 languages including many of the requested ones: Arabic, Danish, German, Greek, Finnish, Hebrew, Hindi, Dutch, Norwegian, Polish, Swedish, Swahili, Turkish. Per-engine language filtering (PR #258) ensures the UI shows correct options. Several of these issues may be closeable.\n\n### New Model Requests (5 explicit issues)\n\n| Issue | Model Requested |\n|-------|----------------|\n| #226 | GGUF support |\n| #172 | VibeVoice |\n| #138 | Export to ONNX/Piper format |\n| #132 | LavaSR (transcription) |\n| #76 | (General model expansion) |\n\nCommunity also requests: XTTS-v2, Fish Speech, Kokoro. CosyVoice was tried and abandoned. The multi-engine architecture is in place, making new model integration straightforward.\n\n### Long-Form / Chunking (5 issues)\n\nUsers hitting the ~500 character practical limit.\n\n**Key issues:** #234 (queue system), #203 (500 char limit), #191 (auto-split), #111, #69\n\n**Fix path:** **Mostly resolved.** PR #266 (engine-agnostic chunked TTS) and PR #269 (async generation queue) are both merged. PR #154 (Audiobook tab) is still open.\n\n### Feature Requests (23 issues)\n\nNotable requests:\n- **#234** — Queue system for batch generation\n- **#182** — Concurrent/multi-thread generation\n- **#173** — Vocal intonation/inflection control\n- **#165** — Audiobook mode\n- **#144** — Copy text to clipboard\n- **#184** — Cancel button for progress bar\n- **#242** — Seed value pinning for consistency\n- **#228** — Always use 0.6B option\n- **#233** — Transcribe audio API improvements\n- **#235** — Finetuned Qwen3-TTS tokenizer\n\n### Bugs (19 issues)\n\n| Category | Issues |\n|----------|--------|\n| Generation failures | #248 (broken pipe), #219 (unsupported scalarType), #202 (clipping error), #170 (load failed) |\n| UI bugs | #231 (history not updating), #190 (mobile landing), #169 (blank interface) |\n| File operations | #207 (transcribe file error), #168 (no such file), #142 (download audio fail) |\n| Server lifecycle | #166 (server processes remain), #164 (no auto-update) |\n| Database | #174 (sqlite3 IntegrityError) |\n| Dependency | #131 (numpy ABI mismatch), #209 (import error) |\n\n---\n\n## Existing Plan Documents — Status\n\n| Document | Target Version | Status | Relevance |\n|----------|---------------|--------|-----------|\n| `TTS_PROVIDER_ARCHITECTURE.md` | v0.1.13 | **Partially superseded** by multi-engine arch + CUDA swap | Core concepts implemented differently than planned |\n| `CUDA_BACKEND_SWAP.md` | — | **Shipped** (PR #252) | CUDA binary download + backend restart |\n| `CUDA_BACKEND_SWAP_FINAL.md` | — | **Shipped** (PR #252) | Final implementation plan |\n| `EXTERNAL_PROVIDERS.md` | v0.2.0 | **Not started** | Remote server support |\n| `MLX_AUDIO.md` | — | **Shipped** | MLX backend is live |\n| `DOCKER_DEPLOYMENT.md` | v0.2.0 | **Shipped** (PR #161) | Docker + web deployment |\n| `OPENAI_SUPPORT.md` | v0.2.0 | **Not started** | OpenAI-compatible API layer |\n| `PR33_CUDA_PROVIDER_REVIEW.md` | — | **Reference** | Analysis of the original provider approach |\n\n---\n\n## New Model Integration — Landscape\n\n### Models Worth Supporting (2026 SOTA — updated March 18)\n\n| Model | Cloning | Speed | Sample Rate | Languages | VRAM | Instruct Support | Integration Ease | Status |\n|-------|---------|-------|-------------|-----------|------|-----------------|-----------------|--------|\n| **Qwen3-TTS** | 10s zero-shot | Medium | 24 kHz | 10 | Medium | None (Base); Yes (CustomVoice variant, predefined speakers only) | **Shipped** | v0.1.13 |\n| **LuxTTS** | 3s zero-shot | 150x RT, CPU ok | 48 kHz | English | <1 GB | None | **Shipped** | PR #254 |\n| **Chatterbox MTL** | 5s zero-shot | Medium | 24 kHz | 23 | Medium | Partial — `exaggeration` float | **Shipped** | PR #257 |\n| **Chatterbox Turbo** | 5s zero-shot | Fast | 24 kHz | English | Low | Partial — inline tags only | **Shipped** | PR #258 |\n| **HumeAI TADA 1B/3B** | Zero-shot | 5x faster than LLM-TTS | 24 kHz | EN (1B), Multilingual (3B) | Medium | Partial — automatic prosody | **Shipped** | PR #296 |\n| **Kokoro-82M** | Pre-built voices | CPU realtime | 24 kHz | 8 | Tiny (82M) | None | **In progress** | Apache 2.0, pip install, ~350MB |\n| ~~**CosyVoice2-0.5B**~~ | 3-10s zero-shot | Very fast | 24 kHz | Multilingual | Low | Yes — `inference_instruct2()` | **Abandoned** | PR #311 — poor output quality |\n| **Fish Speech** | 10-30s few-shot | Real-time | 24-44 kHz | 50+ | Medium | **Yes** — inline text descriptions, word-level control | Ready | Needs license clarification |\n| **XTTS-v2** | 6s zero-shot | Mid-GPU | 24 kHz | 17+ | Medium | Partial — style transfer from ref audio only | Ready | Mature pip package |\n| **Pocket TTS** | Zero-shot + streaming | >1x RT on CPU | — | English | ~100M params, CPU-first | None | Ready | MIT, Kyutai Labs |\n| **MOSS-TTS Family** | Zero-shot | — | — | Multilingual | Medium | **Yes** — text prompts for style + timbre design | Needs vetting | Apache 2.0 |\n| **VoxCPM 1.5** | Zero-shot (seconds) | ~0.15 RTF streaming | — | Bilingual (EN/ZH) | Medium | Partial — automatic context-aware prosody | Needs vetting | Apache 2.0 |\n\n#### Notes on Candidates (March 2026)\n\n- **CosyVoice2-0.5B** — **Tried and abandoned** (PR #311). Despite having the best instruct API, output quality was poor. No PyPI package, needed 5+ shims, heavy deps. Not worth it.\n- **HumeAI TADA** — **Shipped** (PR #296). 700+ seconds coherent audio. [GitHub: HumeAI/tada](https://github.com/HumeAI/tada)\n- **Kokoro-82M** — **In progress.** 82M params, CPU realtime, Apache 2.0, clean `pip install kokoro`. Uses pre-built voice styles (not zero-shot cloning from arbitrary audio). [GitHub: hexgrad/kokoro](https://github.com/hexgrad/kokoro)\n- **Fish Speech** — Word-level fine-grained control. License needs clarification. [fish.audio blog](https://fish.audio/blog/fish-audio-s2-fine-grained-ai-voice-control-at-the-word-level)\n- **XTTS-v2** — Coqui's multilingual cloning. 17+ languages, pip-installable. [GitHub: coqui-ai/TTS](https://github.com/coqui-ai/TTS)\n- **Pocket TTS** — 100M param CPU-first model from Kyutai Labs. [GitHub: kyutai-labs/pocket-tts](https://github.com/kyutai-labs/pocket-tts)\n- **Watch list:** MioTTS-2.6B (fast LLM-based EN/JP, vLLM compatible), Oolel-Voices (Soynade Research, expressive modular control)\n\n### Adding a New Engine (Now Straightforward)\n\nWith the model config registry and shared `EngineModelSelector` component, adding a new TTS engine requires:\n\n1. **Create `backend/backends/<engine>_backend.py`** — implement `TTSBackend` protocol (~200-300 lines)\n2. **Register in `backend/backends/__init__.py`** — add `ModelConfig` entry + `TTS_ENGINES` entry + factory elif\n3. **Update `backend/models.py`** — add engine name to regex\n4. **Update frontend** — add to engine union type, `EngineModelSelector` options, form schema, language map (4 files)\n\n`main.py` requires **zero changes** — the registry handles all dispatch automatically.\n\nTotal effort: **~1 day** for a well-documented model with a PyPI package. See `docs/plans/ADDING_TTS_ENGINES.md` for the full guide.\n\n---\n\n## Architectural Bottlenecks\n\n### ~~1. Single Backend Singleton~~ — RESOLVED\n\nThe singleton TTS backend was replaced with a thread-safe per-engine registry in PR #254. Multiple engines can now be loaded simultaneously.\n\n### ~~2. `main.py` Dispatch Point Duplication~~ — RESOLVED\n\nPreviously, each engine required updates to 6+ hardcoded dispatch maps across `main.py` (~320 lines of if/elif chains). A model config registry in `backend/backends/__init__.py` now centralizes all model metadata (`ModelConfig` dataclass) with helper functions (`load_engine_model()`, `check_model_loaded()`, `engine_needs_trim()`, etc.). Adding a new engine requires zero changes to `main.py`.\n\n### ~~3. Model Config is Scattered~~ — RESOLVED\n\nModel identifiers, HF repo IDs, display names, and engine metadata are now consolidated in the `ModelConfig` registry. Backend-aware branching (e.g. MLX vs PyTorch Qwen repo IDs) happens inside the registry. Frontend model options are centralized in `EngineModelSelector.tsx`.\n\n### 4. Voice Prompt Cache Assumes PyTorch Tensors\n\n`backend/utils/cache.py` uses `torch.save()` / `torch.load()`. LuxTTS and Chatterbox backends work around this by storing reference audio paths instead of tensors in their voice prompt dicts. Not ideal but functional.\n\n### 5. ~~Frontend Assumes Qwen Model Sizes~~ — RESOLVED\n\nThe generation form now uses a flat model dropdown with engine-based routing. Per-engine language filtering is in place. Model size is only sent for Qwen.\n\n---\n\n## Recommended Priorities\n\n### Tier 1 — Ship Now\n\n| Priority | PR/Item | Impact | Effort |\n|----------|---------|--------|--------|\n| 1 | **Kokoro 82M** — finish integration | New engine, CPU-friendly, 8 langs | Low (nearly done) |\n| 2 | Close PR #311 (CosyVoice) and #237 (superseded by #305) | Housekeeping | None |\n| 3 | **#218** — Windows HF cache dir fix | Windows-specific pain | Low |\n| 4 | **#253** — 48kHz speech tokenizer | Quality improvement for Qwen | Medium |\n\n### Tier 2 — Feature Work\n\n| Priority | Item | Impact | Effort |\n|----------|------|--------|--------|\n| 1 | **#154** — Audiobook tab | Long-form users. Chunking + queue now shipped. | Medium |\n| 2 | **#225** — Custom HuggingFace models | User-supplied models. Needs rework. | High |\n| 3 | OpenAI-compatible API (plan doc exists) | Low effort once API is stable | Low |\n| 4 | LoRA fine-tuning (PR #195) | Complex, needs rework for multi-engine | Very High |\n| 5 | Streaming for non-MLX engines | Currently MLX-only | Medium |\n\n### Tier 3 — Future Engines\n\n| Priority | Item | Notes |\n|----------|------|-------|\n| 1 | **Fish Speech** | 50+ langs, word-level instruct. License TBD. |\n| 2 | **XTTS-v2** | 17+ langs, mature pip package. Best multilingual cloning. |\n| 3 | **Pocket TTS** (Kyutai) | CPU-first 100M model. MIT. |\n| 4 | **MOSS-TTS** | Text-to-voice design. Multi-speaker dialogue for Stories. |\n| 5 | **VoxCPM 1.5** | Tokenizer-free streaming. Uncertain integration surface. |\n\n### ~~Previously Prioritized — Now Done~~\n\n- ~~#258 — Chatterbox Turbo~~ **Merged**\n- ~~#99 — Chunked TTS~~ **Superseded by #266, merged**\n- ~~#88 — CORS restriction~~ **Merged**\n- ~~#161 — Docker deployment~~ **Merged**\n- ~~#234 — Queue system~~ **Addressed by #269, merged**\n- ~~HumeAI TADA~~ **Shipped** (PR #296)\n- ~~Kokoro-82M~~ **In progress**\n\n---\n\n## Branch Inventory\n\n| Branch | PR | Status | Notes |\n|--------|-----|--------|-------|\n| `feat/cosyvoice-engine` | #311 | Open — closing | CosyVoice2/3 — abandoned, poor quality |\n| `feat/chatterbox-turbo` | #258 | **Merged** | Chatterbox Turbo + per-engine languages |\n| `feat/chatterbox` | #257 | **Merged** | Chatterbox Multilingual |\n| `feat/luxtts` | #254 | **Merged** | LuxTTS + multi-engine arch |\n\n---\n\n## Quick Reference: API Endpoints\n\n<details>\n<summary>All current endpoints</summary>\n\n| Endpoint | Method | Purpose |\n|----------|--------|---------|\n| `/health` | GET | Health check, model/GPU status |\n| `/profiles` | POST, GET | Create/list voice profiles |\n| `/profiles/{id}` | GET, PUT, DELETE | Profile CRUD |\n| `/profiles/{id}/samples` | POST, GET | Add/list voice samples |\n| `/profiles/{id}/avatar` | POST, GET, DELETE | Avatar management |\n| `/profiles/{id}/export` | GET | Export profile as ZIP |\n| `/profiles/import` | POST | Import profile from ZIP |\n| `/generate` | POST | Generate speech (engine param selects TTS backend) |\n| `/generate/stream` | POST | Stream speech (MLX only) |\n| `/history` | GET | List generation history |\n| `/history/{id}` | GET, DELETE | Get/delete generation |\n| `/history/{id}/export` | GET | Export generation ZIP |\n| `/history/{id}/export-audio` | GET | Export audio only |\n| `/transcribe` | POST | Transcribe audio (Whisper) |\n| `/models/status` | GET | All model statuses (Qwen, LuxTTS, Chatterbox, Chatterbox Turbo, TADA, Whisper) |\n| `/models/download` | POST | Trigger model download |\n| `/models/download/cancel` | POST | Cancel/dismiss download |\n| `/models/{name}` | DELETE | Delete downloaded model |\n| `/models/load` | POST | Load model into memory |\n| `/models/unload` | POST | Unload model |\n| `/models/progress/{name}` | GET | SSE download progress |\n| `/tasks/active` | GET | Active downloads/generations (with inline progress) |\n| `/stories` | POST, GET | Create/list stories |\n| `/stories/{id}` | GET, PUT, DELETE | Story CRUD |\n| `/stories/{id}/items` | POST, GET | Story items CRUD |\n| `/stories/{id}/export` | GET | Export story audio |\n| `/channels` | POST, GET | Audio channel CRUD |\n| `/channels/{id}` | PUT, DELETE | Channel update/delete |\n| `/cache/clear` | POST | Clear voice prompt cache |\n| `/server/cuda/status` | GET | CUDA binary availability |\n| `/server/cuda/download` | POST | Download CUDA binary |\n| `/server/cuda/switch` | POST | Switch to CUDA backend |\n\n</details>\n"
  },
  {
    "path": "docs/notes/RELEASE_v0.2.0.md",
    "content": "# Voicebox v0.2.0 -- Release Notes\n\n## The story\n\nVoicebox v0.1.x shipped as a single-engine voice cloning app built around Qwen3-TTS. It worked, but it was limited: one model family, 10 languages, English-centric emotion, a synchronous generation pipeline that locked the UI, and a hard ceiling on how much text you could generate at once.\n\nv0.2.0 is a ground-up rethink. Voicebox is now a **multi-engine voice cloning platform**. Four TTS engines. 23 languages. Expressive paralinguistic controls. A full post-processing effects pipeline. Unlimited generation length. Asynchronous everything. And it runs on every major GPU vendor -- NVIDIA, AMD, Intel Arc, Apple Silicon -- plus Docker for headless deployment.\n\nThis is the release where Voicebox stops being a proof of concept and starts being a real tool.\n\n---\n\n## Major New Features\n\n### Multi-Engine Architecture\nVoicebox now supports **four TTS engines**, each with different strengths. Switch between them per-generation from a single unified interface:\n\n| Engine | Languages | Strengths |\n|--------|-----------|-----------|\n| **Qwen3-TTS** (0.6B / 1.7B) | 10 | High-quality multilingual cloning, delivery instructions (\"speak slowly\", \"whisper\") |\n| **LuxTTS** | English | Lightweight (~1GB VRAM), 48kHz output, 150x realtime on CPU |\n| **Chatterbox Multilingual** | 23 | Broadest language coverage -- Arabic, Danish, Finnish, Greek, Hebrew, Hindi, Malay, Norwegian, Polish, Swahili, Swedish, Turkish and more |\n| **Chatterbox Turbo** | English | Fast 350M model with paralinguistic emotion/sound tags |\n\n### Emotions and Paralinguistic Tags (Chatterbox Turbo)\nType `/` in the text input to open an autocomplete for **9 expressive tags** that the model synthesizes inline with speech:\n\n`[laugh]` `[chuckle]` `[gasp]` `[cough]` `[sigh]` `[groan]` `[sniff]` `[shush]` `[clear throat]`\n\nTags render as inline badges in a rich text editor and serialize cleanly to the API. This makes generated speech sound natural and expressive in a way that plain TTS can't.\n\n### 23 Languages via Chatterbox Multilingual\nThe Chatterbox Multilingual engine brings zero-shot voice cloning to **23 languages**: Arabic, Chinese, Danish, Dutch, English, Finnish, French, German, Greek, Hebrew, Hindi, Italian, Japanese, Korean, Malay, Norwegian, Polish, Portuguese, Russian, Spanish, Swahili, Swedish, and Turkish. The language dropdown dynamically filters to show only languages supported by the selected engine.\n\n### Unlimited Generation Length (Auto-Chunking)\nPreviously, long text would hit model context limits and degrade. Now, text is **automatically split at sentence boundaries** and each chunk is generated independently, then crossfaded back together. This is fully engine-agnostic and works with all four engines.\n\n- **Auto-chunking limit slider** (100-5,000 chars, default 800) -- controls when text gets split\n- **Crossfade slider** (0-200ms, default 50ms) -- blends chunk boundaries smoothly, or set to 0 for a hard cut\n- **Max text length raised to 50,000 characters** -- generate entire scripts, chapters, or articles in one go\n- Smart splitting respects abbreviations (Dr., e.g., a.m.), CJK punctuation, and never breaks inside paralinguistic `[tags]`\n\n### Asynchronous Generation Queue\nGeneration is now fully **non-blocking**. Submit a generation and immediately start typing the next one -- no more frozen UI waiting for inference to complete.\n\n- Serial execution queue prevents GPU contention across all backends\n- Real-time SSE status streaming (`generating` -> `completed` / `failed`)\n- Failed generations can be retried without re-entering text\n- Stale generations from crashes are auto-recovered on startup\n- Generating status pill shown inline in the story editor\n\n### Post-Processing Effects Pipeline\nA full audio effects system powered by Spotify's `pedalboard` library. Apply effects after generation, preview them in real time, and build reusable presets -- all without leaving the app.\n\n**8 effects available:**\n\n| Effect | What it does |\n|--------|-------------|\n| **Pitch Shift** | Shift pitch up or down by up to 12 semitones |\n| **Reverb** | Room reverb with configurable size, damping, and wet/dry mix |\n| **Delay** | Echo with adjustable delay time, feedback, and mix |\n| **Chorus / Flanger** | Modulated delay -- short for metallic flanger, longer for lush chorus |\n| **Compressor** | Dynamic range compression with threshold, ratio, attack, and release |\n| **Gain** | Volume adjustment from -40 to +40 dB |\n| **High-Pass Filter** | Remove low frequencies below a configurable cutoff |\n| **Low-Pass Filter** | Remove high frequencies above a configurable cutoff |\n\n**Effects presets** -- Four built-in presets ship out of the box (Robotic, Radio, Echo Chamber, Deep Voice), and you can create unlimited custom presets. Presets are drag-and-drop chains of effects with per-parameter sliders.\n\n**Per-profile default effects** -- Assign an effects chain to a voice profile and it applies automatically to every generation with that voice. Override per-generation from the generate box.\n\n**Live preview** -- Audition any effects chain against an existing generation before committing. The preview streams processed audio without saving anything.\n\n### Generation Versions\nEvery generation now supports **multiple versions** with full provenance tracking:\n\n- **Original** -- the clean, unprocessed TTS output (always preserved)\n- **Effects versions** -- apply different effects chains to create new versions from any source version\n- **Takes** -- regenerate with the same text and voice but a new seed for variation\n- **Source tracking** -- each version records which version it was derived from\n- **Version pinning in stories** -- pin a specific version to a track clip in the story editor, independent of the generation's default\n- **Favorites** -- star generations to mark them for quick access\n\n---\n\n## New Platform Support\n\n### Linux (Native)\nFull Linux support with `.deb` and `.rpm` packages. Includes PulseAudio/PipeWire audio capture for voice sample recording.\n\n### AMD ROCm GPU Acceleration\nAMD GPU users now get hardware-accelerated inference via ROCm, with automatic `HSA_OVERRIDE_GFX_VERSION` configuration for GPUs not officially in the ROCm compatibility list (e.g., RX 6600).\n\n### NVIDIA CUDA Backend Swap\nThe CPU-only release can download and swap in a CUDA-accelerated backend binary from within the app -- no reinstall required. Handles GitHub's 2GB asset limit by downloading split parts and verifying SHA-256 checksums.\n\n### Intel Arc (XPU) and DirectML\nPyTorch backend also supports Intel Arc GPUs via IPEX/XPU and Windows any-GPU via DirectML.\n\n### Docker + Web Deployment\nRun Voicebox headless as a Docker container with the full web UI:\n```bash\ndocker compose up\n```\n3-stage build, non-root runtime, health checks, persistent model cache across rebuilds. Binds to localhost only by default.\n\n---\n\n## Model Management\n- **Per-model unload** -- free GPU memory without deleting downloaded models\n- **Custom models directory** -- set `VOICEBOX_MODELS_DIR` to store models anywhere\n- **Model folder migration** -- move all models to a new location with progress tracking\n- **Whisper Turbo** -- added `openai/whisper-large-v3-turbo` as a transcription model option\n- **Download cancel/clear UI** -- cancel in-progress downloads, VS Code-style problems panel for errors\n\n---\n\n## Security\n- **CORS hardening** -- replaced wildcard `*` with an explicit allowlist of local origins; extensible via `VOICEBOX_CORS_ORIGINS` env var\n- **Network access toggle** -- fully disable outbound network requests for air-gapped deployments\n\n## Accessibility\n- Comprehensive screen reader support (tested with NVDA/Narrator) across all major UI surfaces\n- Keyboard navigation for voice cards, history rows, model management, and story editor\n- State-aware `aria-label` attributes on all interactive controls\n\n## Reliability\n- **Atomic audio saves** -- two-phase write prevents corrupted files on crash/interrupt\n- **Filesystem health endpoint** -- proactive disk space and directory writability checks\n- **Errno-specific error messages** -- clear feedback for permission denied, disk full, missing directory\n\n## UX Polish\n- Responsive layout with horizontal-scroll voice cards on mobile\n- App version shown in sidebar\n- Voice card heights normalized\n- Audio player title hidden at narrow widths to prevent overflow\n\n---\n\n## Installation\n\n| Platform | Download |\n|----------|----------|\n| **macOS (Apple Silicon)** | `Voicebox_0.2.0_aarch64.dmg` |\n| **macOS (Intel)** | `Voicebox_0.2.0_x64.dmg` |\n| **Windows** | `Voicebox_0.2.0_x64_en-US.msi` or `x64-setup.exe` |\n| **Linux** | `.deb` / `.rpm` packages |\n| **Docker** | `docker compose up` |\n\nThe app includes automatic updates -- future patches will be installed automatically.\n\n---\n\n## Video Script Beats\n\nFor the marketing video, focus on these six beats:\n\n1. **\"Four engines, one app\"** -- show the engine dropdown switching between Qwen, LuxTTS, Chatterbox, and Turbo\n2. **\"23 languages\"** -- generate the same voice clone in Arabic, Japanese, Hindi, etc.\n3. **\"Make it expressive\"** -- type `/laugh` and `/sigh` with Chatterbox Turbo, play back the result\n4. **\"Shape your sound\"** -- apply the Robotic or Deep Voice preset, preview it live, then build a custom effects chain with drag-and-drop\n5. **\"No limits\"** -- paste a long script, show it auto-chunk and generate seamlessly\n6. **\"Queue and go\"** -- fire off multiple generations back-to-back without waiting\n"
  },
  {
    "path": "docs/notes/issue-pain-points.md",
    "content": "# Voicebox Issue Pain Points (Snapshot)\n\n## Scope\n\n- Dataset: **128 total issues** (**107 open**, **21 closed**)\n- Source: GitHub issues in `jamiepine/voicebox`\n- Classification: keyword/theme clustering\n- Note: counts below are **non-exclusive** (one issue can belong to multiple pain points)\n\n## Most Common Pain Points (Open Issues)\n\n| Rank | Pain Point | Open Issues | What users are reporting |\n|---|---|---:|---|\n| 1 | Model download & offline reliability | **32** | Downloads failing/stalling, cache/offline behavior inconsistent, wrong model size selected, Errno issues |\n| 2 | GPU/backend compatibility | **22** | GPU not detected, backend fallback surprises, platform-specific runtime failures (Windows/Mac) |\n| 3 | Export/save/file persistence | **15** | Export fails, \"failed to fetch/download audio\", samples/profiles not saving |\n| 4 | Language/accent quality & coverage | **14** | Missing language support, accent mismatch, robotic outputs |\n| 5 | Update/restart safety + long-op controls | **4** | Auto-restart without warning, update confusion, lack of cancel/pause controls |\n\n## Representative Issues by Pain Point\n\n### 1) Model download & offline reliability (32)\n\n- [#159](https://github.com/jamiepine/voicebox/issues/159) - Qwen download fails with Errno 22\n- [#151](https://github.com/jamiepine/voicebox/issues/151) - Model loading hangs / server crashes\n- [#150](https://github.com/jamiepine/voicebox/issues/150) - Internet required despite downloaded models\n- [#149](https://github.com/jamiepine/voicebox/issues/149) - Cancel/pause controls for large downloads\n- [#96](https://github.com/jamiepine/voicebox/issues/96) - 0.6B selection still uses/downloads 1.7B\n\n### 2) GPU/backend compatibility (22)\n\n- [#164](https://github.com/jamiepine/voicebox/issues/164) - Windows: no GPU usage + multiple breakages\n- [#141](https://github.com/jamiepine/voicebox/issues/141) - Using CPU only, GPU not used\n- [#131](https://github.com/jamiepine/voicebox/issues/131) - Numpy ABI mismatch in bundled app\n- [#130](https://github.com/jamiepine/voicebox/issues/130) - Intel Mac tensor/padding generation error\n- [#127](https://github.com/jamiepine/voicebox/issues/127) - GPU not found\n\n### 3) Export/save/file persistence (15)\n\n- [#148](https://github.com/jamiepine/voicebox/issues/148) - Japanese export fails on 0.1.12\n- [#143](https://github.com/jamiepine/voicebox/issues/143) - Samples not saving\n- [#134](https://github.com/jamiepine/voicebox/issues/134) - Can't save profile\n- [#105](https://github.com/jamiepine/voicebox/issues/105) - Export audio fails (failed to fetch)\n- [#49](https://github.com/jamiepine/voicebox/issues/49) - Export filename/location ignored on Windows\n\n### 4) Language/accent quality & coverage (14)\n\n- [#162](https://github.com/jamiepine/voicebox/issues/162) - Persian audio request/problem\n- [#117](https://github.com/jamiepine/voicebox/issues/117) - Arabic language support\n- [#113](https://github.com/jamiepine/voicebox/issues/113) - Polish language support\n- [#109](https://github.com/jamiepine/voicebox/issues/109) - Ukrainian support\n- [#100](https://github.com/jamiepine/voicebox/issues/100) - Non-US accent quality issues\n\n### 5) Update/restart safety + controls (4)\n\n- [#164](https://github.com/jamiepine/voicebox/issues/164) - Update behavior + usability failures\n- [#136](https://github.com/jamiepine/voicebox/issues/136) - Auto-restart without warning\n- [#86](https://github.com/jamiepine/voicebox/issues/86) - Unexpected restart with no confirmation\n- [#149](https://github.com/jamiepine/voicebox/issues/149) - Need pause/cancel and pre-download confirmation\n\n## Additional Signal\n\n- There is also a large **feature-request/misc** bucket (**36 open**) that is competing with stability triage (audiobook, Linux build, additional ASR/TTS models, integrations).\n\n## Takeaway\n\nMost user pain is concentrated in four stability areas: **download/offline path**, **GPU/backend detection**, **save/export reliability**, and **language/accent correctness**. Addressing those first should reduce the majority of current support friction.\n"
  },
  {
    "path": "docs/openapi.json",
    "content": "{\n  \"openapi\": \"3.1.0\",\n  \"info\": {\n    \"title\": \"voicebox API\",\n    \"description\": \"Production-quality Qwen3-TTS voice cloning API\",\n    \"version\": \"0.1.0\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://localhost:8000\",\n      \"description\": \"Local development server\"\n    }\n  ],\n  \"paths\": {\n    \"/\": {\n      \"get\": {\n        \"summary\": \"Root\",\n        \"description\": \"Root endpoint.\",\n        \"operationId\": \"root__get\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"General\"\n        ]\n      }\n    },\n    \"/health\": {\n      \"get\": {\n        \"summary\": \"Health\",\n        \"description\": \"Health check endpoint.\",\n        \"operationId\": \"health_health_get\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HealthResponse\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"General\"\n        ]\n      }\n    },\n    \"/profiles\": {\n      \"get\": {\n        \"summary\": \"List Profiles\",\n        \"description\": \"List all voice profiles.\",\n        \"operationId\": \"list_profiles_profiles_get\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/VoiceProfileResponse\"\n                  },\n                  \"type\": \"array\",\n                  \"title\": \"Response List Profiles Profiles Get\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Profiles\"\n        ]\n      },\n      \"post\": {\n        \"summary\": \"Create Profile\",\n        \"description\": \"Create a new voice profile.\",\n        \"operationId\": \"create_profile_profiles_post\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/VoiceProfileCreate\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/VoiceProfileResponse\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Profiles\"\n        ]\n      }\n    },\n    \"/profiles/{profile_id}\": {\n      \"get\": {\n        \"summary\": \"Get Profile\",\n        \"description\": \"Get a voice profile by ID.\",\n        \"operationId\": \"get_profile_profiles__profile_id__get\",\n        \"parameters\": [\n          {\n            \"name\": \"profile_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Profile Id\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/VoiceProfileResponse\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Profiles\"\n        ]\n      },\n      \"put\": {\n        \"summary\": \"Update Profile\",\n        \"description\": \"Update a voice profile.\",\n        \"operationId\": \"update_profile_profiles__profile_id__put\",\n        \"parameters\": [\n          {\n            \"name\": \"profile_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Profile Id\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/VoiceProfileCreate\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/VoiceProfileResponse\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Profiles\"\n        ]\n      },\n      \"delete\": {\n        \"summary\": \"Delete Profile\",\n        \"description\": \"Delete a voice profile.\",\n        \"operationId\": \"delete_profile_profiles__profile_id__delete\",\n        \"parameters\": [\n          {\n            \"name\": \"profile_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Profile Id\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Profiles\"\n        ]\n      }\n    },\n    \"/profiles/{profile_id}/samples\": {\n      \"post\": {\n        \"summary\": \"Add Profile Sample\",\n        \"description\": \"Add a sample to a voice profile.\",\n        \"operationId\": \"add_profile_sample_profiles__profile_id__samples_post\",\n        \"parameters\": [\n          {\n            \"name\": \"profile_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Profile Id\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Body_add_profile_sample_profiles__profile_id__samples_post\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ProfileSampleResponse\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Profiles\"\n        ]\n      },\n      \"get\": {\n        \"summary\": \"Get Profile Samples\",\n        \"description\": \"Get all samples for a profile.\",\n        \"operationId\": \"get_profile_samples_profiles__profile_id__samples_get\",\n        \"parameters\": [\n          {\n            \"name\": \"profile_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Profile Id\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"$ref\": \"#/components/schemas/ProfileSampleResponse\"\n                  },\n                  \"title\": \"Response Get Profile Samples Profiles  Profile Id  Samples Get\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Profiles\"\n        ]\n      }\n    },\n    \"/profiles/samples/{sample_id}\": {\n      \"delete\": {\n        \"summary\": \"Delete Profile Sample\",\n        \"description\": \"Delete a profile sample.\",\n        \"operationId\": \"delete_profile_sample_profiles_samples__sample_id__delete\",\n        \"parameters\": [\n          {\n            \"name\": \"sample_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Sample Id\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Profiles\"\n        ]\n      }\n    },\n    \"/generate\": {\n      \"post\": {\n        \"summary\": \"Generate Speech\",\n        \"description\": \"Generate speech from text using a voice profile.\",\n        \"operationId\": \"generate_speech_generate_post\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/GenerationRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/GenerationResponse\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Generation\"\n        ]\n      }\n    },\n    \"/history\": {\n      \"get\": {\n        \"summary\": \"List History\",\n        \"description\": \"List generation history with optional filters.\",\n        \"operationId\": \"list_history_history_get\",\n        \"parameters\": [\n          {\n            \"name\": \"profile_id\",\n            \"in\": \"query\",\n            \"required\": false,\n            \"schema\": {\n              \"anyOf\": [\n                {\n                  \"type\": \"string\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ],\n              \"title\": \"Profile Id\"\n            }\n          },\n          {\n            \"name\": \"search\",\n            \"in\": \"query\",\n            \"required\": false,\n            \"schema\": {\n              \"anyOf\": [\n                {\n                  \"type\": \"string\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ],\n              \"title\": \"Search\"\n            }\n          },\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"integer\",\n              \"default\": 50,\n              \"title\": \"Limit\"\n            }\n          },\n          {\n            \"name\": \"offset\",\n            \"in\": \"query\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"integer\",\n              \"default\": 0,\n              \"title\": \"Offset\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HistoryListResponse\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"History\"\n        ]\n      }\n    },\n    \"/history/{generation_id}\": {\n      \"get\": {\n        \"summary\": \"Get Generation\",\n        \"description\": \"Get a generation by ID.\",\n        \"operationId\": \"get_generation_history__generation_id__get\",\n        \"parameters\": [\n          {\n            \"name\": \"generation_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Generation Id\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HistoryResponse\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"History\"\n        ]\n      },\n      \"delete\": {\n        \"summary\": \"Delete Generation\",\n        \"description\": \"Delete a generation.\",\n        \"operationId\": \"delete_generation_history__generation_id__delete\",\n        \"parameters\": [\n          {\n            \"name\": \"generation_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Generation Id\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"History\"\n        ]\n      }\n    },\n    \"/history/stats\": {\n      \"get\": {\n        \"summary\": \"Get Stats\",\n        \"description\": \"Get generation statistics.\",\n        \"operationId\": \"get_stats_history_stats_get\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"History\"\n        ]\n      }\n    },\n    \"/transcribe\": {\n      \"post\": {\n        \"summary\": \"Transcribe Audio\",\n        \"description\": \"Transcribe audio file to text.\",\n        \"operationId\": \"transcribe_audio_transcribe_post\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/Body_transcribe_audio_transcribe_post\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/TranscriptionResponse\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Generation\"\n        ]\n      }\n    },\n    \"/audio/{generation_id}\": {\n      \"get\": {\n        \"summary\": \"Get Audio\",\n        \"description\": \"Serve generated audio file.\",\n        \"operationId\": \"get_audio_audio__generation_id__get\",\n        \"parameters\": [\n          {\n            \"name\": \"generation_id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Generation Id\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Generation\"\n        ]\n      }\n    },\n    \"/models/load\": {\n      \"post\": {\n        \"summary\": \"Load Model\",\n        \"description\": \"Manually load TTS model.\",\n        \"operationId\": \"load_model_models_load_post\",\n        \"parameters\": [\n          {\n            \"name\": \"model_size\",\n            \"in\": \"query\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"string\",\n              \"default\": \"1.7B\",\n              \"title\": \"Model Size\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Models\"\n        ]\n      }\n    },\n    \"/models/unload\": {\n      \"post\": {\n        \"summary\": \"Unload Model\",\n        \"description\": \"Unload TTS model to free memory.\",\n        \"operationId\": \"unload_model_models_unload_post\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Models\"\n        ]\n      }\n    },\n    \"/models/progress/{model_name}\": {\n      \"get\": {\n        \"summary\": \"Get Model Progress\",\n        \"description\": \"Get model download progress via Server-Sent Events.\",\n        \"operationId\": \"get_model_progress_models_progress__model_name__get\",\n        \"parameters\": [\n          {\n            \"name\": \"model_name\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\",\n              \"title\": \"Model Name\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Models\"\n        ]\n      }\n    },\n    \"/models/status\": {\n      \"get\": {\n        \"summary\": \"Get Model Status\",\n        \"description\": \"Get status of all available models.\",\n        \"operationId\": \"get_model_status_models_status_get\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ModelStatusListResponse\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Models\"\n        ]\n      }\n    },\n    \"/models/download\": {\n      \"post\": {\n        \"summary\": \"Trigger Model Download\",\n        \"description\": \"Trigger download of a specific model.\",\n        \"operationId\": \"trigger_model_download_models_download_post\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ModelDownloadRequest\"\n              }\n            }\n          },\n          \"required\": true\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {}\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Validation Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/HTTPValidationError\"\n                }\n              }\n            }\n          }\n        },\n        \"tags\": [\n          \"Models\"\n        ]\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"Body_add_profile_sample_profiles__profile_id__samples_post\": {\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\",\n            \"title\": \"File\"\n          },\n          \"reference_text\": {\n            \"type\": \"string\",\n            \"title\": \"Reference Text\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"file\",\n          \"reference_text\"\n        ],\n        \"title\": \"Body_add_profile_sample_profiles__profile_id__samples_post\"\n      },\n      \"Body_transcribe_audio_transcribe_post\": {\n        \"properties\": {\n          \"file\": {\n            \"type\": \"string\",\n            \"format\": \"binary\",\n            \"title\": \"File\"\n          },\n          \"language\": {\n            \"anyOf\": [\n              {\n                \"type\": \"string\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Language\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"file\"\n        ],\n        \"title\": \"Body_transcribe_audio_transcribe_post\"\n      },\n      \"GenerationRequest\": {\n        \"properties\": {\n          \"profile_id\": {\n            \"type\": \"string\",\n            \"title\": \"Profile Id\"\n          },\n          \"text\": {\n            \"type\": \"string\",\n            \"maxLength\": 5000,\n            \"minLength\": 1,\n            \"title\": \"Text\"\n          },\n          \"language\": {\n            \"type\": \"string\",\n            \"pattern\": \"^(en|zh)$\",\n            \"title\": \"Language\",\n            \"default\": \"en\"\n          },\n          \"seed\": {\n            \"anyOf\": [\n              {\n                \"type\": \"integer\",\n                \"minimum\": 0.0\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Seed\"\n          },\n          \"model_size\": {\n            \"anyOf\": [\n              {\n                \"type\": \"string\",\n                \"pattern\": \"^(1\\\\.7B|0\\\\.6B)$\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Model Size\",\n            \"default\": \"1.7B\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"profile_id\",\n          \"text\"\n        ],\n        \"title\": \"GenerationRequest\",\n        \"description\": \"Request model for voice generation.\"\n      },\n      \"GenerationResponse\": {\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"title\": \"Id\"\n          },\n          \"profile_id\": {\n            \"type\": \"string\",\n            \"title\": \"Profile Id\"\n          },\n          \"text\": {\n            \"type\": \"string\",\n            \"title\": \"Text\"\n          },\n          \"language\": {\n            \"type\": \"string\",\n            \"title\": \"Language\"\n          },\n          \"audio_path\": {\n            \"type\": \"string\",\n            \"title\": \"Audio Path\"\n          },\n          \"duration\": {\n            \"type\": \"number\",\n            \"title\": \"Duration\"\n          },\n          \"seed\": {\n            \"anyOf\": [\n              {\n                \"type\": \"integer\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Seed\"\n          },\n          \"created_at\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"title\": \"Created At\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"id\",\n          \"profile_id\",\n          \"text\",\n          \"language\",\n          \"audio_path\",\n          \"duration\",\n          \"seed\",\n          \"created_at\"\n        ],\n        \"title\": \"GenerationResponse\",\n        \"description\": \"Response model for voice generation.\"\n      },\n      \"HTTPValidationError\": {\n        \"properties\": {\n          \"detail\": {\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ValidationError\"\n            },\n            \"type\": \"array\",\n            \"title\": \"Detail\"\n          }\n        },\n        \"type\": \"object\",\n        \"title\": \"HTTPValidationError\"\n      },\n      \"HealthResponse\": {\n        \"properties\": {\n          \"status\": {\n            \"type\": \"string\",\n            \"title\": \"Status\"\n          },\n          \"model_loaded\": {\n            \"type\": \"boolean\",\n            \"title\": \"Model Loaded\"\n          },\n          \"model_downloaded\": {\n            \"anyOf\": [\n              {\n                \"type\": \"boolean\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Model Downloaded\"\n          },\n          \"model_size\": {\n            \"anyOf\": [\n              {\n                \"type\": \"string\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Model Size\"\n          },\n          \"gpu_available\": {\n            \"type\": \"boolean\",\n            \"title\": \"Gpu Available\"\n          },\n          \"vram_used_mb\": {\n            \"anyOf\": [\n              {\n                \"type\": \"number\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Vram Used Mb\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"status\",\n          \"model_loaded\",\n          \"gpu_available\"\n        ],\n        \"title\": \"HealthResponse\",\n        \"description\": \"Response model for health check.\"\n      },\n      \"HistoryListResponse\": {\n        \"properties\": {\n          \"items\": {\n            \"items\": {\n              \"$ref\": \"#/components/schemas/HistoryResponse\"\n            },\n            \"type\": \"array\",\n            \"title\": \"Items\"\n          },\n          \"total\": {\n            \"type\": \"integer\",\n            \"title\": \"Total\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"items\",\n          \"total\"\n        ],\n        \"title\": \"HistoryListResponse\",\n        \"description\": \"Response model for history list.\"\n      },\n      \"HistoryResponse\": {\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"title\": \"Id\"\n          },\n          \"profile_id\": {\n            \"type\": \"string\",\n            \"title\": \"Profile Id\"\n          },\n          \"profile_name\": {\n            \"type\": \"string\",\n            \"title\": \"Profile Name\"\n          },\n          \"text\": {\n            \"type\": \"string\",\n            \"title\": \"Text\"\n          },\n          \"language\": {\n            \"type\": \"string\",\n            \"title\": \"Language\"\n          },\n          \"audio_path\": {\n            \"type\": \"string\",\n            \"title\": \"Audio Path\"\n          },\n          \"duration\": {\n            \"type\": \"number\",\n            \"title\": \"Duration\"\n          },\n          \"seed\": {\n            \"anyOf\": [\n              {\n                \"type\": \"integer\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Seed\"\n          },\n          \"created_at\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"title\": \"Created At\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"id\",\n          \"profile_id\",\n          \"profile_name\",\n          \"text\",\n          \"language\",\n          \"audio_path\",\n          \"duration\",\n          \"seed\",\n          \"created_at\"\n        ],\n        \"title\": \"HistoryResponse\",\n        \"description\": \"Response model for history entry (includes profile name).\"\n      },\n      \"ModelDownloadRequest\": {\n        \"properties\": {\n          \"model_name\": {\n            \"type\": \"string\",\n            \"title\": \"Model Name\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"model_name\"\n        ],\n        \"title\": \"ModelDownloadRequest\",\n        \"description\": \"Request model for triggering model download.\"\n      },\n      \"ModelStatus\": {\n        \"properties\": {\n          \"model_name\": {\n            \"type\": \"string\",\n            \"title\": \"Model Name\"\n          },\n          \"display_name\": {\n            \"type\": \"string\",\n            \"title\": \"Display Name\"\n          },\n          \"downloaded\": {\n            \"type\": \"boolean\",\n            \"title\": \"Downloaded\"\n          },\n          \"size_mb\": {\n            \"anyOf\": [\n              {\n                \"type\": \"number\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Size Mb\"\n          },\n          \"loaded\": {\n            \"type\": \"boolean\",\n            \"title\": \"Loaded\",\n            \"default\": false\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"model_name\",\n          \"display_name\",\n          \"downloaded\"\n        ],\n        \"title\": \"ModelStatus\",\n        \"description\": \"Response model for model status.\"\n      },\n      \"ModelStatusListResponse\": {\n        \"properties\": {\n          \"models\": {\n            \"items\": {\n              \"$ref\": \"#/components/schemas/ModelStatus\"\n            },\n            \"type\": \"array\",\n            \"title\": \"Models\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"models\"\n        ],\n        \"title\": \"ModelStatusListResponse\",\n        \"description\": \"Response model for model status list.\"\n      },\n      \"ProfileSampleResponse\": {\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"title\": \"Id\"\n          },\n          \"profile_id\": {\n            \"type\": \"string\",\n            \"title\": \"Profile Id\"\n          },\n          \"audio_path\": {\n            \"type\": \"string\",\n            \"title\": \"Audio Path\"\n          },\n          \"reference_text\": {\n            \"type\": \"string\",\n            \"title\": \"Reference Text\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"id\",\n          \"profile_id\",\n          \"audio_path\",\n          \"reference_text\"\n        ],\n        \"title\": \"ProfileSampleResponse\",\n        \"description\": \"Response model for profile sample.\"\n      },\n      \"TranscriptionResponse\": {\n        \"properties\": {\n          \"text\": {\n            \"type\": \"string\",\n            \"title\": \"Text\"\n          },\n          \"duration\": {\n            \"type\": \"number\",\n            \"title\": \"Duration\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"text\",\n          \"duration\"\n        ],\n        \"title\": \"TranscriptionResponse\",\n        \"description\": \"Response model for transcription.\"\n      },\n      \"ValidationError\": {\n        \"properties\": {\n          \"loc\": {\n            \"items\": {\n              \"anyOf\": [\n                {\n                  \"type\": \"string\"\n                },\n                {\n                  \"type\": \"integer\"\n                }\n              ]\n            },\n            \"type\": \"array\",\n            \"title\": \"Location\"\n          },\n          \"msg\": {\n            \"type\": \"string\",\n            \"title\": \"Message\"\n          },\n          \"type\": {\n            \"type\": \"string\",\n            \"title\": \"Error Type\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"loc\",\n          \"msg\",\n          \"type\"\n        ],\n        \"title\": \"ValidationError\"\n      },\n      \"VoiceProfileCreate\": {\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"maxLength\": 100,\n            \"minLength\": 1,\n            \"title\": \"Name\"\n          },\n          \"description\": {\n            \"anyOf\": [\n              {\n                \"type\": \"string\",\n                \"maxLength\": 500\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Description\"\n          },\n          \"language\": {\n            \"type\": \"string\",\n            \"pattern\": \"^(en|zh)$\",\n            \"title\": \"Language\",\n            \"default\": \"en\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"name\"\n        ],\n        \"title\": \"VoiceProfileCreate\",\n        \"description\": \"Request model for creating a voice profile.\"\n      },\n      \"VoiceProfileResponse\": {\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"title\": \"Id\"\n          },\n          \"name\": {\n            \"type\": \"string\",\n            \"title\": \"Name\"\n          },\n          \"description\": {\n            \"anyOf\": [\n              {\n                \"type\": \"string\"\n              },\n              {\n                \"type\": \"null\"\n              }\n            ],\n            \"title\": \"Description\"\n          },\n          \"language\": {\n            \"type\": \"string\",\n            \"title\": \"Language\"\n          },\n          \"created_at\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"title\": \"Created At\"\n          },\n          \"updated_at\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"title\": \"Updated At\"\n          }\n        },\n        \"type\": \"object\",\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"description\",\n          \"language\",\n          \"created_at\",\n          \"updated_at\"\n        ],\n        \"title\": \"VoiceProfileResponse\",\n        \"description\": \"Response model for voice profile.\"\n      }\n    }\n  },\n  \"tags\": [\n    {\n      \"name\": \"General\",\n      \"description\": \"Root and health check endpoints\"\n    },\n    {\n      \"name\": \"Profiles\",\n      \"description\": \"Voice profile management\"\n    },\n    {\n      \"name\": \"Generation\",\n      \"description\": \"Speech generation, transcription, and audio retrieval\"\n    },\n    {\n      \"name\": \"History\",\n      \"description\": \"Generation history and statistics\"\n    },\n    {\n      \"name\": \"Models\",\n      \"description\": \"Model loading, unloading, and status management\"\n    }\n  ]\n}"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"example-next-mdx\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"fumadocs-mdx && next build\",\n    \"dev\": \"fumadocs-mdx && next dev\",\n    \"start\": \"next start\",\n    \"postinstall\": \"fumadocs-mdx\"\n  },\n  \"dependencies\": {\n    \"@radix-ui/react-popover\": \"^1.1.15\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"fumadocs-core\": \"^16.4.11\",\n    \"fumadocs-mdx\": \"13\",\n    \"fumadocs-openapi\": \"^10.2.7\",\n    \"fumadocs-ui\": \"^16.4.11\",\n    \"lucide-react\": \"^0.546.0\",\n    \"next\": \"^16.1.6\",\n    \"react\": \"^19.2.0\",\n    \"react-dom\": \"^19.2.0\",\n    \"shiki\": \"^3.22.0\",\n    \"tailwind-merge\": \"^3.5.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/postcss\": \"^4.1.15\",\n    \"@types/mdx\": \"^2.0.13\",\n    \"@types/node\": \"^24.9.1\",\n    \"@types/react\": \"^19.2.2\",\n    \"@types/react-dom\": \"^19.2.2\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.1.15\",\n    \"typescript\": \"^5.9.3\"\n  }\n}\n"
  },
  {
    "path": "docs/plans/API_REFACTOR_PLAN.md",
    "content": "# Voicebox API Refactor Plan\n\nDate: 2026-03-19\nStatus: Proposed\nScope: Backend HTTP API structure, schemas, docs, and compatibility strategy\n\n## Goals\n\n- Make the API easier to understand and automate against.\n- Improve endpoint consistency without breaking the desktop app or existing local integrations.\n- Align generated docs and checked-in OpenAPI artifacts with the actual backend.\n- Separate app-facing resources from internal or operational actions.\n- Create a migration path toward a cleaner `v2` resource model while preserving `v1` routes during transition.\n\n## Non-Goals\n\n- Rewriting backend business logic or generation internals.\n- Introducing authentication for all deployment modes in the first pass.\n- Changing storage models or database schema unless required for API correctness.\n- Removing current routes immediately.\n\n## Current Pain Points\n\n- Mixed endpoint styles: resource-oriented (`/profiles`) and command-oriented (`/generate`, `/tasks/clear`) coexist.\n- Related generation resources are split across multiple namespaces: `/generate`, `/history`, `/audio`, `/effects`, and `/generations/.../versions`.\n- Response payloads vary widely: typed models, raw dicts with `message`, booleans, and `HTTPException(detail=...)` payloads.\n- Some async flows use exception-shaped `202` responses instead of first-class task contracts.\n- Checked-in OpenAPI output can drift from actual backend models.\n- Operational endpoints such as `/shutdown` are exposed in the same surface as user workflows.\n\n## Guiding Principles\n\n1. Prefer additive changes before destructive changes.\n2. Keep `v1` behavior working until the app and docs fully migrate.\n3. Add compatibility shims close to the routing layer, not deep in services.\n4. Treat OpenAPI as a release artifact that must be kept in sync.\n5. Standardize public contracts before renaming everything.\n\n## Target API Shape\n\nThis is the intended end state, not the immediate first milestone.\n\n### Core Resources\n\n- `/profiles`\n- `/profiles/{profile_id}/samples`\n- `/profiles/{profile_id}/avatar`\n- `/profiles/{profile_id}/effects`\n- `/generations`\n- `/generations/{generation_id}`\n- `/generations/{generation_id}/status`\n- `/generations/{generation_id}/audio`\n- `/generations/{generation_id}/versions`\n- `/generations/{generation_id}/versions/{version_id}`\n- `/generations/{generation_id}/versions/{version_id}/audio`\n- `/stories`\n- `/stories/{story_id}/items`\n- `/effects/presets`\n- `/models`\n- `/models/{model_name}`\n- `/tasks`\n\n### Operational or Internal Endpoints\n\nMove under an explicit namespace and disable where appropriate:\n\n- `/admin/shutdown`\n- `/admin/watchdog/disable`\n- `/admin/cache/clear`\n- `/admin/tasks/clear`\n\n### Response Contract Direction\n\n- Resource reads and writes return typed resource models.\n- Delete and action endpoints return small typed action result models.\n- Errors use a consistent structure.\n- Async actions return explicit task metadata instead of overloading `detail`.\n\n## Migration Strategy Overview\n\nThe refactor is split into six phases. Phases 1-3 are the highest impact and safest to ship first.\n\n| Phase | Focus | Est. Duration | Risk | Backward Compatibility |\n| --- | --- | --- | --- | --- |\n| 1 | Documentation and contract correctness | 2-3 days | Low | Full |\n| 2 | Response and error consistency | 3-5 days | Low-Medium | Full |\n| 3 | Router structure and internal organization | 3-4 days | Low | Full |\n| 4 | Additive `v2` resource endpoints | 1-2 weeks | Medium | Full |\n| 5 | Client migration and deprecation rollout | 1 week | Medium | Full during rollout |\n| 6 | Cleanup and optional removals | 1-2 releases | Medium-High | Partial after notice |\n\n## Phase 1: Fix Contract Drift First\n\nPriority: Highest\nOutcome: The documented API matches the running backend.\n\n### Problems Addressed\n\n- `docs/openapi.json` can become stale.\n- Generated API reference pages may describe outdated request bodies.\n- App metadata still frames the backend too narrowly.\n\n### Implementation Steps\n\n1. Update FastAPI app metadata in `backend/app.py`.\n   - Replace the old Qwen-specific description with a multi-engine Voicebox API description.\n   - Add tags metadata for major domains if desired.\n2. Regenerate OpenAPI from the running app using the existing docs script flow.\n3. Compare `backend/models.py` to the checked-in schema.\n   - Verify `GenerationRequest`, effects endpoints, stories endpoints, and model endpoints.\n4. Regenerate or refresh API reference pages under `docs/content/docs/api-reference/`.\n5. Add a CI check that fails if `docs/openapi.json` is out of date.\n6. Add a short maintainer note describing when schema regeneration is required.\n\n### Backward Compatibility\n\n- No route changes.\n- No payload changes.\n- Safe to release immediately.\n\n### Success Criteria\n\n- `docs/openapi.json` matches the live app.\n- Generated docs include all currently supported generate parameters.\n- No frontend code changes required.\n\n## Phase 2: Standardize Responses and Errors\n\nPriority: High\nOutcome: Clients can handle responses predictably.\n\n### Problems Addressed\n\n- Delete endpoints return ad hoc message dicts.\n- Toggle endpoints return special one-off payloads.\n- `202` async responses are encoded as `HTTPException(detail=...)` in some places.\n\n### Implementation Steps\n\n1. Add shared response models in `backend/models.py`.\n   - `ActionResult`\n   - `DeleteResult`\n   - `ToggleFavoriteResponse`\n   - `AcceptedTaskResponse`\n   - `ApiError`\n2. Convert routes that currently return raw dicts to explicit `response_model`s.\n   - `DELETE /profiles/{profile_id}`\n   - `DELETE /history/{generation_id}`\n   - `DELETE /stories/{story_id}`\n   - `POST /tasks/clear`\n   - `POST /cache/clear`\n   - similar endpoints across routes\n3. Replace exception-shaped `202` responses in `transcription.py` with an explicit accepted response body.\n   - Return `JSONResponse(status_code=202, content=...)` or typed FastAPI response model.\n4. Add a global exception handler for known API errors if helpful.\n   - Normalize `ValueError` to `400` with a consistent error body.\n   - Preserve FastAPI validation errors for now, or wrap them in a consistent top-level shape in a later pass.\n5. Document the stable error contract in the docs.\n\n### Migration Strategy\n\n- Keep field names inside successful payloads compatible where possible.\n- For existing dict responses, preserve the current keys while introducing typed models with the same shape.\n- For `202` flows, support both old and new client handling for one release if needed.\n\n### Timeline Estimate\n\n- 3-5 engineering days including tests and docs refresh.\n\n### Success Criteria\n\n- All mutation endpoints declare response models.\n- Clients can programmatically distinguish success, accepted, and error cases without special casing `detail` payloads.\n\n## Phase 3: Normalize Router Structure Internally\n\nPriority: High\nOutcome: The backend becomes easier to maintain before public path changes begin.\n\n### Problems Addressed\n\n- Route files hardcode full paths and are all mounted at root.\n- There is no consistent use of router prefixes or tags.\n- Route grouping in code does not cleanly express the public API shape.\n\n### Implementation Steps\n\n1. Add prefixes and tags to routers.\n   - `profiles`: `prefix=\"/profiles\"`\n   - `generations`: `prefix=\"/generate\"` for now or split additive aliases carefully\n   - `history`: `prefix=\"/history\"`\n   - `effects`: `prefix=\"/effects\"`\n   - and so on\n2. Convert route declarations to relative paths within each router.\n3. Introduce a small route compatibility layer for routes that are likely to move later.\n   - Example: helper functions that can be mounted under both old and new paths.\n4. Add explicit route tags so Swagger/OpenAPI groups are coherent.\n5. Document the intended public ownership of each namespace.\n\n### Backward Compatibility\n\n- No public path changes yet if existing paths are preserved through prefixes and aliases.\n- Mostly internal refactoring.\n\n### Timeline Estimate\n\n- 3-4 engineering days.\n\n### Success Criteria\n\n- All route modules use prefixes and tags.\n- Route registration in `backend/routes/__init__.py` becomes simpler.\n- OpenAPI groups read cleanly by domain.\n\n## Phase 4: Introduce Additive `v2` Resource Endpoints\n\nPriority: High\nOutcome: A cleaner API exists without breaking the current one.\n\n### Problems Addressed\n\n- Generation-related resources are fragmented.\n- Sample and audio endpoints are not consistently modeled as resources.\n- Command-style naming makes the API harder to reason about.\n\n### New Endpoints to Add\n\nThese should be introduced alongside current endpoints, not as replacements.\n\n- `POST /generations` -> alias for current `/generate`\n- `GET /generations` -> alias for current `/history`\n- `GET /generations/{id}` -> alias for current `/history/{id}`\n- `POST /generations/{id}/retry` -> alias for current `/generate/{id}/retry`\n- `POST /generations/{id}/regenerate` -> alias for current `/generate/{id}/regenerate`\n- `GET /generations/{id}/status` -> alias for current `/generate/{id}/status`\n- `POST /generations/stream` -> alias for current `/generate/stream`\n- `GET /generations/{id}/audio` -> alias for current `/audio/{generation_id}`\n- `GET /generations/{id}/export` -> alias for current `/history/{generation_id}/export`\n- `GET /generations/{id}/export-audio` -> alias for current `/history/{generation_id}/export-audio`\n- `GET /profiles/{profile_id}/samples/{sample_id}` or `GET /samples/{sample_id}` as a consciously chosen model\n- `PUT /profiles/{profile_id}/samples/{sample_id}` -> alias for current sample update route\n- `DELETE /profiles/{profile_id}/samples/{sample_id}` -> alias for current sample delete route\n\n### Implementation Steps\n\n1. Create new handler entry points that call the existing service functions.\n2. Keep old handlers in place, but mark them deprecated in OpenAPI.\n3. Add `summary` and `description` text clarifying preferred routes.\n4. Update frontend and docs examples to use new endpoints first.\n5. Add tests proving both old and new paths return equivalent responses.\n\n### Migration Strategy\n\n- Old paths remain functional for at least one stable release cycle.\n- New docs and client examples use `v2-style` resource routes immediately.\n- Include deprecation headers where feasible, for example:\n  - `Deprecation: true`\n  - `Sunset: <date>`\n  - `Link: <new-doc-url>; rel=\"successor-version\"`\n\n### Timeline Estimate\n\n- 1-2 weeks depending on test coverage and frontend updates.\n\n### Success Criteria\n\n- All major generation workflows are accessible through resource-oriented routes.\n- Old routes still work unchanged.\n\n## Phase 5: Migrate First-Party Clients and Publish Deprecations\n\nPriority: Medium\nOutcome: Voicebox itself stops depending on legacy paths.\n\n### Problems Addressed\n\n- The desktop app and docs may continue to reinforce old route shapes.\n- Third-party consumers need a visible migration path.\n\n### Implementation Steps\n\n1. Update `app/src/lib/api/client.ts` to use the new preferred endpoints.\n2. Regenerate or refresh any generated API clients.\n3. Update docs examples, tutorials, and code snippets to use preferred routes only.\n4. Add a changelog entry describing the migration path.\n5. Add runtime deprecation logging for legacy route usage in development mode.\n6. If feasible, expose a small `/health` or `/meta` field showing API version and deprecation window.\n\n### Migration Strategy\n\n- Keep old endpoints available but clearly documented as legacy.\n- Publish a mapping table from old route to new route.\n- Do not change request or response payloads during the same phase unless necessary.\n\n### Timeline Estimate\n\n- About 1 week including docs and app verification.\n\n### Success Criteria\n\n- First-party app no longer depends on legacy route names.\n- Docs do not advertise deprecated paths as the primary interface.\n\n## Phase 6: Cleanup, Namespace Hardening, and Optional Breaking Changes\n\nPriority: Medium\nOutcome: The API surface is cleaner and safer for remote or Docker use.\n\n### Problems Addressed\n\n- Internal/admin endpoints are mixed into the public API.\n- Legacy aliases increase maintenance cost forever if never retired.\n\n### Implementation Steps\n\n1. Move operational endpoints under `/admin` or `/internal`.\n   - `/shutdown`\n   - `/watchdog/disable`\n   - `/tasks/clear`\n   - `/cache/clear`\n2. Gate these endpoints behind configuration for non-local deployments.\n   - Example: `VOICEBOX_ENABLE_ADMIN_API=true`\n3. Decide whether to remove or keep legacy aliases.\n   - If removing, do so only after a published deprecation window.\n4. Remove deprecated docs pages and old examples.\n5. Tighten route-level tests to prevent accidental reintroduction of legacy patterns.\n\n### Migration Strategy\n\n- For desktop-only local use, aliases may remain indefinitely if removal cost outweighs benefit.\n- For published remote API guidance, hide admin endpoints from default docs even if they still exist.\n\n### Timeline Estimate\n\n- 1-2 releases after the additive migration is complete.\n\n### Success Criteria\n\n- Public docs expose a coherent resource API.\n- Operational endpoints are clearly separate or disabled in remote contexts.\n\n## Cross-Cutting Work Items\n\nThese should happen throughout the migration, not only in a single phase.\n\n### Testing\n\n- Add route equivalence tests for old and new paths.\n- Add schema snapshot tests for OpenAPI generation.\n- Add response-shape tests for common mutations and async workflows.\n- Add contract tests for `202 Accepted` flows.\n\n### Documentation\n\n- Maintain an old-to-new endpoint mapping table.\n- Add per-endpoint examples for create profile, generate, apply effects, transcribe, and stories operations.\n- Explicitly document which endpoints are app-facing vs admin-facing.\n\n### Observability\n\n- Add warning logs when deprecated endpoints are used.\n- Track usage counts in development or optional telemetry-free local logs.\n\n### Release Management\n\n- Mention API changes in `CHANGELOG.md`.\n- Ensure docs and app updates ship in the same release as new preferred routes.\n\n## Recommended Execution Order\n\nIf engineering time is limited, implement in this exact order:\n\n1. Fix OpenAPI and docs drift.\n2. Standardize response models and accepted-task responses.\n3. Add router prefixes and tags internally.\n4. Add `/generations` aliases and sample path aliases.\n5. Migrate the first-party app to preferred routes.\n6. Deprecate or hide legacy/admin routes.\n\n## Old-to-New Route Mapping\n\n| Current Route | Preferred Route |\n| --- | --- |\n| `POST /generate` | `POST /generations` |\n| `POST /generate/stream` | `POST /generations/stream` |\n| `POST /generate/{id}/retry` | `POST /generations/{id}/retry` |\n| `POST /generate/{id}/regenerate` | `POST /generations/{id}/regenerate` |\n| `GET /generate/{id}/status` | `GET /generations/{id}/status` |\n| `GET /history` | `GET /generations` |\n| `GET /history/{id}` | `GET /generations/{id}` |\n| `GET /audio/{id}` | `GET /generations/{id}/audio` |\n| `GET /history/{id}/export` | `GET /generations/{id}/export` |\n| `GET /history/{id}/export-audio` | `GET /generations/{id}/export-audio` |\n| `PUT /profiles/samples/{sample_id}` | `PUT /profiles/{profile_id}/samples/{sample_id}` |\n| `DELETE /profiles/samples/{sample_id}` | `DELETE /profiles/{profile_id}/samples/{sample_id}` |\n| `POST /tasks/clear` | `POST /admin/tasks/clear` |\n| `POST /cache/clear` | `POST /admin/cache/clear` |\n| `POST /shutdown` | `POST /admin/shutdown` |\n| `POST /watchdog/disable` | `POST /admin/watchdog/disable` |\n\n## Risks and Mitigations\n\n### Risk: App regressions during endpoint migration\n\n- Mitigation: Add new routes before changing client usage.\n- Mitigation: Keep payloads identical while paths change.\n\n### Risk: Docs still drift after cleanup\n\n- Mitigation: Add CI enforcement and a release checklist step.\n\n### Risk: Third-party local scripts break on removal\n\n- Mitigation: Prefer indefinite aliases for one-person local workflows unless maintenance becomes painful.\n\n### Risk: Admin endpoints remain dangerous in remote mode\n\n- Mitigation: Hide and gate them before promoting remote deployment more broadly.\n\n## Definition of Done\n\nThe refactor can be considered complete when all of the following are true:\n\n- OpenAPI, checked-in docs, and backend models match.\n- The preferred public API is resource-oriented and documented consistently.\n- The Voicebox app uses preferred routes exclusively.\n- Legacy routes are either deprecated with a timeline or intentionally retained as compatibility aliases.\n- Operational endpoints are clearly separated from the public app API.\n"
  },
  {
    "path": "docs/plans/CUDA_LIBS_ADDON.md",
    "content": "# CUDA Libs as a Bolt-On Addon\n\n## Problem\n\nEvery time we bump `__version__` (even for a UI tweak or bugfix), the exact-match version check in both `main.rs:222` and `cuda.py:237` invalidates the user's ~2.4GB CUDA binary, forcing a full redownload. The CUDA binary is the entire server rebuilt with NVIDIA libs included -- there's no separation between app logic and the CUDA runtime.\n\n## Why This Is Hard With `--onefile`\n\nThe core tension is PyInstaller `--onefile` mode (`build_binary.py:39`). In onefile mode, everything -- Python code, all dependencies, torch, the NVIDIA `.dll`/`.so` files -- gets packed into a single self-extracting archive. There's no concept of \"swap out one part.\" The binary IS the server.\n\n## Options\n\n### Option A: Switch to `--onedir` for the CUDA Build (Recommended)\n\nInstead of `--onefile`, build the CUDA variant as a directory (a folder with the exe + all the shared libs alongside it). Then split the distribution into two archives:\n\n1. **`voicebox-server-cuda` executable + non-NVIDIA deps** (~200-400MB) -- versioned with the app, redownloaded on every app update.\n2. **`cuda-libs-cu126.tar.gz`** (~2GB) -- the `nvidia.*` packages (cublas, cudnn, cuda_runtime, etc.), versioned independently (e.g., `cuda-libs-cu126-v1`). Only redownloaded when we bump the CUDA toolkit version or torch's CUDA dependency changes.\n\n#### How it would work at runtime\n\n- Tauri downloads the server binary archive and extracts it to `{data_dir}/backends/cuda/`\n- On first CUDA setup (or when cuda-libs version bumps), downloads and extracts the libs archive into the same directory\n- The CUDA server exe finds the `.dll`/`.so` files next to it (standard PyInstaller onedir behavior)\n- Version check becomes two checks: server version + cuda-libs version\n\n#### Independent versioning\n\nAdd a `cuda-libs.json` manifest:\n\n```json\n{\"version\": \"cu126-v1\", \"torch_compat\": \">=2.6.0,<2.8.0\"}\n```\n\nThe server checks this on startup. The Tauri side checks it before launching. Only bump `cu126-v1` -> `cu126-v2` when we actually change the CUDA toolkit or torch major version.\n\n#### Build pipeline changes\n\nThe CI `build-cuda-windows` job would build with `--onedir`, then separate the output into two archives. The CUDA libs archive could be built less frequently (only when torch/CUDA version changes) and stored as a pinned release asset.\n\n#### Download experience\n\n- First-time CUDA setup: ~2.4GB total (same as today)\n- Subsequent app updates: ~200-400MB for the server, CUDA libs stay cached\n- CUDA toolkit bump: ~2GB for just the libs\n\n#### Pros\n\n- PyInstaller `--onedir` natively produces this structure -- NVIDIA DLLs end up as discrete files in the output directory\n- The separation is natural: PyInstaller puts torch's NVIDIA deps in predictable paths (`nvidia/cublas/lib/`, etc.)\n- CUDA libs are highly stable -- only rebundle when changing CUDA toolkit version (e.g., cu126 -> cu128) or major torch version\n- Server updates become ~200-400MB instead of ~2.4GB\n- No library path hacking needed -- torch finds NVIDIA DLLs because they're in the same directory tree\n\n#### Cons\n\n- Onedir means a folder with hundreds of files instead of a single exe -- more complex to manage, extract, and clean up\n- Need to modify download/assembly logic in `cuda.py` to handle two separate archives\n- The Tauri side (`main.rs`) needs to point at an exe inside a directory rather than a standalone binary\n- Users who manually manage the file may find the folder structure confusing\n\n#### TTS engine compatibility\n\nNo issues. The TTS engines are pure Python + torch. They don't care whether NVIDIA libs are inside the binary or sitting next to it -- torch's dynamic loader finds them either way.\n\n---\n\n### Option B: Keep `--onefile` but Externalize CUDA Libs via Library Path\n\nKeep the server as a single `--onefile` binary (with NVIDIA packages excluded, same as the CPU build). Ship the CUDA libs as a separate download that gets extracted to `{data_dir}/backends/cuda-libs/`. Before launching, set the library search path to include that directory.\n\n**Important caveat:** The CPU torch wheel (`whl/cpu`) doesn't have CUDA kernels compiled in -- it's a fundamentally different build. So the binary would need to be built with CUDA-compiled torch but with the NVIDIA runtime libraries excluded. The runtime libs (cublas, cudnn, etc.) would be provided externally.\n\n#### How it would work\n\n- Build ONE \"CUDA-ready\" server binary with CUDA-compiled torch but NVIDIA runtime packages excluded\n- Ship `cuda-libs-cu126-v1.tar.gz` separately (~2GB of `.dll`/`.so` files)\n- When launching, Tauri sets `PATH` (Windows) or `LD_LIBRARY_PATH` (Linux) to include the cuda-libs directory\n\n#### Pros\n\n- Single server binary for both CPU and CUDA users -- simplifies build pipeline enormously\n- True bolt-on CUDA libs with fully independent versioning\n- Server updates are always small (~150MB for the onefile binary)\n\n#### Cons\n\n- **Fragile on Windows.** PyInstaller `--onefile` extracts to a temp directory at runtime and the internal torch may not find externally-placed NVIDIA libs. DLL resolution on Windows is notoriously unreliable in this scenario.\n- `os.add_dll_directory()` only affects `LoadLibraryEx` with `LOAD_LIBRARY_SEARCH_USER_DIRS` flag -- not all DLL loads go through this path\n- PyInstaller's onefile bootloader may configure DLL search paths before Python code runs\n- Could work on Linux but is fragile on Windows\n\n---\n\n### Option C: Hybrid -- `--onefile` Server + Dynamic CUDA Lib Loading at Runtime\n\nBuild the server as `--onefile` with CUDA-compiled torch but with NVIDIA packages excluded. At startup, before torch initializes CUDA, explicitly load the NVIDIA shared libraries using `ctypes.CDLL` or `os.add_dll_directory()`.\n\nIn `server.py`, before any torch imports:\n\n```python\ncuda_libs_dir = os.environ.get(\"VOICEBOX_CUDA_LIBS\")\nif cuda_libs_dir and os.path.isdir(cuda_libs_dir):\n    if sys.platform == \"win32\":\n        os.add_dll_directory(cuda_libs_dir)\n        os.environ[\"PATH\"] = cuda_libs_dir + os.pathsep + os.environ.get(\"PATH\", \"\")\n    else:\n        os.environ[\"LD_LIBRARY_PATH\"] = cuda_libs_dir + \":\" + os.environ.get(\"LD_LIBRARY_PATH\", \"\")\n```\n\n#### Pros\n\n- Single server binary, true bolt-on CUDA libs\n- Clean separation of concerns\n- Independent versioning\n\n#### Cons\n\n- Needs careful testing with each torch version -- CUDA initialization happens deep in C++ extension layer\n- On Windows, `os.add_dll_directory()` may not cover all DLL load paths\n- PyInstaller's onefile bootloader may have already configured DLL search paths before Python code runs\n- Most complex to get right and maintain\n\n## Recommendation\n\n**Option A (`--onedir` with split archives)** is the most reliable path:\n\n1. **It actually works.** `--onedir` puts all files on disk as regular files. Torch finds NVIDIA DLLs because they're in the same directory tree, exactly as they would be in a normal pip install.\n2. **Natural separation.** PyInstaller's `--onedir` output already separates the NVIDIA `.dll`/`.so` files into `nvidia/` subdirectories. We can split the output directory into \"core\" and \"nvidia-libs\" archives after building.\n3. **Independent versioning is straightforward.** A `cuda-libs.json` manifest controls when redownloads are needed.\n4. **Build pipeline simplification.** Build CUDA libs archive less frequently, store as a pinned release asset.\n\nThe main cost is managing a directory instead of a single file, but we already have sophisticated download/assembly infrastructure in `cuda.py` with manifests and split parts. Extending that to handle two archives is incremental work.\n\n## Tauri Compatibility (Validated)\n\nTauri handles PyInstaller `--onedir` with no issues. The key insight is that we're **not** using a static sidecar for CUDA -- we're downloading and extracting at runtime (the existing `cuda.py` + `main.rs` flow). For runtime-launched processes, Tauri's `tauri::shell::Command` supports arbitrary directories natively.\n\n### The critical change in `main.rs`\n\nThe only Tauri-side change needed is adding `.current_dir()` when spawning the CUDA backend:\n\n```rust\nlet cuda_dir = data_dir.join(\"backends/cuda\");\nlet exe_path = cuda_dir.join(\"voicebox-server-cuda.exe\");\n\nlet mut cmd = app.shell().command(exe_path.to_str().unwrap());\ncmd = cmd.current_dir(&cuda_dir);  // PyInstaller finds all DLLs relative to exe\ncmd = cmd.args([\"--data-dir\", &data_dir_str, \"--port\", &port_str, \"--parent-pid\", &parent_pid_str]);\n```\n\n`.current_dir()` tells the PyInstaller bootloader that everything (DLLs, `nvidia/cublas/lib/`, `_internal/`, torch extensions, etc.) lives relative to the exe. Torch finds the NVIDIA libs exactly as it does in a normal `pip install` or dev environment -- no `LD_LIBRARY_PATH` hacks, no `os.add_dll_directory` gymnastics.\n\n### Community evidence\n\n- Multiple Tauri users run this exact pattern: Nuitka folders (exe + pythonXX.dll + supporting files), multi-file .NET apps, and PyInstaller onedir backends (GitHub issues #5719, discussion #5206).\n- The shell plugin explicitly supports `cwd` in both Rust and JS APIs.\n- No reports of torch/CUDA-specific breakage -- the onedir layout is identical to what PyInstaller produces in normal usage.\n\n### Known gotcha: process termination on Windows\n\nPyInstaller onedir creates a parent bootloader + child Python process on Windows. `child.kill()` only hits the outer process in some cases (Tauri issue #11686). Mitigation: keep a reference to the parent PID or use `taskkill /F /T` for clean shutdown. This is not a blocker -- our existing `--parent-pid` watchdog mechanism in `server.py` already handles orphan cleanup.\n\n## Next Steps\n\n1. Prototype: Build the current CUDA binary with `--onedir` and verify torch CUDA works from the output directory\n2. Measure the size split: how much is NVIDIA libs vs everything else\n3. Design the two-archive download flow and dual version checking\n4. Update `cuda.py` for dual-archive extraction (server core + cuda-libs)\n5. Update `main.rs`: change launch path to `backends/cuda/` dir + add `.current_dir()`\n6. Add `ensure_cuda_structure()` helper in Rust to verify exe + nvidia/ subdirs exist before spawning\n7. Update CI pipeline: `build-cuda-windows` produces two archives instead of split parts\n8. ~~Update `split_binary.py` or replace with archive-based distribution~~ Done: replaced with `package_cuda.py`\n"
  },
  {
    "path": "docs/plans/DOCKER_DEPLOYMENT.md",
    "content": "# Docker Deployment Guide\n\n**Status:** In Development for v0.2.0\n**Requested By:** Reddit community ([thread](https://reddit.com/r/LocalLLaMA/...))\n\n## Overview\n\nDocker support makes Voicebox easier to deploy, especially for:\n\n- **Consistent Environments**: Same setup across dev/staging/prod\n- **GPU Passthrough**: Easy NVIDIA/AMD GPU access\n- **Server Deployments**: Run on headless Linux servers\n- **Multi-User Setups**: Isolate instances per user/team\n- **Cloud Platforms**: Deploy to AWS, GCP, Azure, DigitalOcean\n\n## Quick Start\n\n### Using Pre-Built Images (Recommended)\n\n```bash\n# CPU-only version\ndocker run -p 8000:8000 -v voicebox-data:/app/data \\\n  ghcr.io/jamiepine/voicebox:latest\n\n# NVIDIA GPU version\ndocker run --gpus all -p 8000:8000 -v voicebox-data:/app/data \\\n  ghcr.io/jamiepine/voicebox:latest-cuda\n\n# AMD GPU version (experimental)\ndocker run --device=/dev/kfd --device=/dev/dri -p 8000:8000 \\\n  -v voicebox-data:/app/data \\\n  ghcr.io/jamiepine/voicebox:latest-rocm\n```\n\nThen open: `http://localhost:8000`\n\n### Using Docker Compose (Easiest)\n\nCreate `docker-compose.yml`:\n\n```yaml\nversion: '3.8'\n\nservices:\n  voicebox:\n    image: ghcr.io/jamiepine/voicebox:latest-cuda\n    ports:\n      - \"8000:8000\"\n    volumes:\n      - voicebox-data:/app/data\n      - huggingface-cache:/root/.cache/huggingface\n    environment:\n      - GPU_MEMORY_FRACTION=0.8  # Use 80% of GPU memory\n      - TTS_MODE=local\n      - WHISPER_MODE=local\n    deploy:\n      resources:\n        reservations:\n          devices:\n            - driver: nvidia\n              count: 1\n              capabilities: [gpu]\n\nvolumes:\n  voicebox-data:\n  huggingface-cache:\n```\n\nRun:\n```bash\ndocker compose up -d\n```\n\n## Building From Source\n\n### Basic Dockerfile\n\n```dockerfile\n# Dockerfile\nFROM python:3.11-slim\n\nWORKDIR /app\n\n# Install system dependencies\nRUN apt-get update && apt-get install -y \\\n    git \\\n    build-essential \\\n    ffmpeg \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy application\nCOPY backend/ /app/backend/\nCOPY requirements.txt /app/\n\n# Install Python dependencies\nRUN pip install --no-cache-dir -r requirements.txt\nRUN pip install --no-cache-dir git+https://github.com/QwenLM/Qwen3-TTS.git\n\n# Create data directory\nRUN mkdir -p /app/data\n\n# Expose port\nEXPOSE 8000\n\n# Run server\nCMD [\"uvicorn\", \"backend.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n```\n\nBuild and run:\n```bash\ndocker build -t voicebox .\ndocker run -p 8000:8000 -v $(pwd)/data:/app/data voicebox\n```\n\n### Multi-Stage Build (Optimized)\n\nSmaller image size by separating build and runtime:\n\n```dockerfile\n# Dockerfile.optimized\n# Stage 1: Build dependencies\nFROM python:3.11-slim AS builder\n\nWORKDIR /build\n\nRUN apt-get update && apt-get install -y \\\n    git build-essential && \\\n    rm -rf /var/lib/apt/lists/*\n\nCOPY backend/requirements.txt .\nRUN pip install --no-cache-dir --target=/build/packages \\\n    -r requirements.txt\n\nRUN pip install --no-cache-dir --target=/build/packages \\\n    git+https://github.com/QwenLM/Qwen3-TTS.git\n\n# Stage 2: Runtime\nFROM python:3.11-slim\n\nWORKDIR /app\n\n# Install only runtime dependencies\nRUN apt-get update && apt-get install -y \\\n    ffmpeg \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy installed packages from builder\nCOPY --from=builder /build/packages /usr/local/lib/python3.11/site-packages/\n\n# Copy application code\nCOPY backend/ /app/backend/\n\n# Create data directory\nRUN mkdir -p /app/data\n\nEXPOSE 8000\n\nCMD [\"uvicorn\", \"backend.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n```\n\nBuild:\n```bash\ndocker build -f Dockerfile.optimized -t voicebox:slim .\n```\n\n## GPU Support\n\n### NVIDIA GPUs (CUDA)\n\n**Dockerfile:**\n```dockerfile\nFROM nvidia/cuda:12.1.0-runtime-ubuntu22.04\n\n# Install Python\nRUN apt-get update && apt-get install -y \\\n    python3.11 python3-pip git ffmpeg && \\\n    rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\n\n# Install PyTorch with CUDA support\nCOPY backend/requirements.txt .\nRUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121\n\n# Install other dependencies\nRUN pip3 install -r requirements.txt\nRUN pip3 install git+https://github.com/QwenLM/Qwen3-TTS.git\n\nCOPY backend/ /app/backend/\n\nEXPOSE 8000\nCMD [\"uvicorn\", \"backend.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n```\n\n**Run with GPU:**\n```bash\ndocker run --gpus all -p 8000:8000 \\\n  -v voicebox-data:/app/data \\\n  voicebox:cuda\n```\n\n**Docker Compose with GPU:**\n```yaml\nservices:\n  voicebox:\n    image: voicebox:cuda\n    deploy:\n      resources:\n        reservations:\n          devices:\n            - driver: nvidia\n              count: all\n              capabilities: [gpu]\n```\n\n### AMD GPUs (ROCm) - Experimental\n\n**Dockerfile:**\n```dockerfile\nFROM rocm/dev-ubuntu-22.04:6.0\n\n# Install Python\nRUN apt-get update && apt-get install -y \\\n    python3.11 python3-pip git ffmpeg && \\\n    rm -rf /var/lib/apt/lists/*\n\nWORKDIR /app\n\n# Install PyTorch with ROCm support\nCOPY backend/requirements.txt .\nRUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm6.0\n\n# Install other dependencies\nRUN pip3 install -r requirements.txt\nRUN pip3 install git+https://github.com/QwenLM/Qwen3-TTS.git\n\n# Set ROCm environment variables\nENV HSA_OVERRIDE_GFX_VERSION=10.3.0\nENV ROCM_PATH=/opt/rocm\n\nCOPY backend/ /app/backend/\n\nEXPOSE 8000\nCMD [\"uvicorn\", \"backend.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n```\n\n**Run with AMD GPU:**\n```bash\ndocker run --device=/dev/kfd --device=/dev/dri \\\n  --group-add video --ipc=host --cap-add=SYS_PTRACE \\\n  --security-opt seccomp=unconfined \\\n  -p 8000:8000 -v voicebox-data:/app/data \\\n  voicebox:rocm\n```\n\n**Note:** ROCm support varies by GPU model. Works best on Linux. See [AMD ROCm docs](https://rocm.docs.amd.com) for compatibility.\n\n## Volume Mounts\n\n### Essential Volumes\n\n```bash\ndocker run -v voicebox-data:/app/data \\           # Profiles, generations, history\n           -v huggingface-cache:/root/.cache/huggingface \\  # Downloaded models\n           -p 8000:8000 voicebox\n```\n\n### Development Volume Mounts\n\nFor development with hot-reload:\n\n```bash\ndocker run -v $(pwd)/backend:/app/backend \\       # Live code changes\n           -v voicebox-data:/app/data \\\n           -e RELOAD=true \\\n           -p 8000:8000 voicebox\n```\n\n### Custom Model Storage\n\nUse external model directory:\n\n```bash\ndocker run -v /path/to/models:/models \\\n           -e MODELS_DIR=/models \\\n           -v voicebox-data:/app/data \\\n           -p 8000:8000 voicebox\n```\n\n## Environment Variables\n\nConfigure Voicebox via environment variables:\n\n```bash\ndocker run -e TTS_MODE=local \\\n           -e WHISPER_MODE=openai-api \\\n           -e OPENAI_API_KEY=sk-... \\\n           -e GPU_MEMORY_FRACTION=0.8 \\\n           -e LOG_LEVEL=info \\\n           -p 8000:8000 voicebox\n```\n\n### Available Variables\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `TTS_MODE` | `local` | TTS provider: `local`, `remote` |\n| `TTS_REMOTE_URL` | - | URL for remote TTS server |\n| `WHISPER_MODE` | `local` | Whisper provider: `local`, `openai-api`, `remote` |\n| `WHISPER_REMOTE_URL` | - | URL for remote Whisper server |\n| `OPENAI_API_KEY` | - | OpenAI API key (if using OpenAI Whisper) |\n| `GPU_MEMORY_FRACTION` | `0.9` | Fraction of GPU memory to use (0.0-1.0) |\n| `DATA_DIR` | `/app/data` | Directory for profiles/generations |\n| `MODELS_DIR` | `/app/models` | Directory for local models |\n| `LOG_LEVEL` | `info` | Logging level: `debug`, `info`, `warning`, `error` |\n| `RELOAD` | `false` | Enable hot-reload for development |\n\n## Complete Docker Compose Examples\n\n### Production Deployment\n\n```yaml\n# docker-compose.prod.yml\nversion: '3.8'\n\nservices:\n  voicebox:\n    image: ghcr.io/jamiepine/voicebox:latest-cuda\n    container_name: voicebox\n    restart: unless-stopped\n    ports:\n      - \"8000:8000\"\n    volumes:\n      - voicebox-data:/app/data\n      - huggingface-cache:/root/.cache/huggingface\n    environment:\n      - TTS_MODE=local\n      - WHISPER_MODE=local\n      - GPU_MEMORY_FRACTION=0.8\n      - LOG_LEVEL=info\n    deploy:\n      resources:\n        reservations:\n          devices:\n            - driver: nvidia\n              count: 1\n              capabilities: [gpu]\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8000/health\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 40s\n\nvolumes:\n  voicebox-data:\n    driver: local\n  huggingface-cache:\n    driver: local\n```\n\nRun:\n```bash\ndocker compose -f docker-compose.prod.yml up -d\n```\n\n### Development Setup\n\n```yaml\n# docker-compose.dev.yml\nversion: '3.8'\n\nservices:\n  voicebox:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - \"8000:8000\"\n    volumes:\n      - ./backend:/app/backend:ro\n      - voicebox-data:/app/data\n      - huggingface-cache:/root/.cache/huggingface\n    environment:\n      - RELOAD=true\n      - LOG_LEVEL=debug\n      - TTS_MODE=local\n    command: uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload\n\nvolumes:\n  voicebox-data:\n  huggingface-cache:\n```\n\n### Multi-Service Stack\n\nFull stack with reverse proxy and monitoring:\n\n```yaml\n# docker-compose.stack.yml\nversion: '3.8'\n\nservices:\n  # Main Voicebox app\n  voicebox:\n    image: ghcr.io/jamiepine/voicebox:latest-cuda\n    restart: unless-stopped\n    volumes:\n      - voicebox-data:/app/data\n      - huggingface-cache:/root/.cache/huggingface\n    environment:\n      - TTS_MODE=local\n      - WHISPER_MODE=local\n    deploy:\n      resources:\n        reservations:\n          devices:\n            - driver: nvidia\n              count: 1\n              capabilities: [gpu]\n\n  # Nginx reverse proxy\n  nginx:\n    image: nginx:alpine\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - ./nginx.conf:/etc/nginx/nginx.conf:ro\n      - ./ssl:/etc/nginx/ssl:ro\n    depends_on:\n      - voicebox\n\n  # Prometheus monitoring (optional)\n  prometheus:\n    image: prom/prometheus\n    ports:\n      - \"9090:9090\"\n    volumes:\n      - ./prometheus.yml:/etc/prometheus/prometheus.yml\n      - prometheus-data:/prometheus\n\nvolumes:\n  voicebox-data:\n  huggingface-cache:\n  prometheus-data:\n```\n\n## Cloud Deployment\n\n### AWS EC2\n\n1. **Launch GPU Instance** (g4dn.xlarge or p3.2xlarge)\n2. **Install Docker + nvidia-docker:**\n   ```bash\n   # Amazon Linux 2\n   sudo yum install -y docker\n   sudo systemctl start docker\n   distribution=$(. /etc/os-release;echo $ID$VERSION_ID)\n   curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -\n   curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | \\\n     sudo tee /etc/apt/sources.list.d/nvidia-docker.list\n   sudo apt-get update && sudo apt-get install -y nvidia-docker2\n   sudo systemctl restart docker\n   ```\n3. **Deploy:**\n   ```bash\n   docker run --gpus all -d -p 80:8000 \\\n     -v voicebox-data:/app/data \\\n     --restart unless-stopped \\\n     ghcr.io/jamiepine/voicebox:latest-cuda\n   ```\n\n### DigitalOcean\n\nUse GPU Droplet + Docker:\n\n```bash\n# Create droplet via CLI\ndoctl compute droplet create voicebox \\\n  --size gpu-h100x1-80gb \\\n  --image ubuntu-22-04-x64 \\\n  --region nyc3\n\n# SSH and deploy\nssh root@<droplet-ip>\ncurl -fsSL https://get.docker.com -o get-docker.sh\nsh get-docker.sh\ndocker run --gpus all -d -p 80:8000 voicebox:cuda\n```\n\n### Google Cloud Run (CPU-only)\n\n```bash\n# Build and push\ndocker build -t gcr.io/your-project/voicebox .\ndocker push gcr.io/your-project/voicebox\n\n# Deploy to Cloud Run\ngcloud run deploy voicebox \\\n  --image gcr.io/your-project/voicebox \\\n  --platform managed \\\n  --region us-central1 \\\n  --memory 4Gi \\\n  --cpu 2 \\\n  --port 8000\n```\n\n### Fly.io\n\nCreate `fly.toml`:\n```toml\napp = \"voicebox\"\n\n[build]\n  image = \"ghcr.io/jamiepine/voicebox:latest\"\n\n[[services]]\n  http_checks = []\n  internal_port = 8000\n  protocol = \"tcp\"\n\n  [[services.ports]]\n    port = 80\n    handlers = [\"http\"]\n\n  [[services.ports]]\n    port = 443\n    handlers = [\"tls\", \"http\"]\n\n[mounts]\n  source = \"voicebox_data\"\n  destination = \"/app/data\"\n```\n\nDeploy:\n```bash\nfly launch\nfly deploy\n```\n\n## Troubleshooting\n\n### GPU Not Detected\n\n**Check NVIDIA Docker:**\n```bash\ndocker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi\n```\n\nIf this fails, reinstall nvidia-docker2.\n\n**Check AMD ROCm:**\n```bash\ndocker run --rm --device=/dev/kfd --device=/dev/dri rocm/dev-ubuntu-22.04:6.0 rocminfo\n```\n\n### Permission Errors\n\nContainer can't write to volumes:\n```bash\n# Fix permissions\ndocker run --user $(id -u):$(id -g) -v $(pwd)/data:/app/data voicebox\n```\n\n### Out of Memory\n\nReduce GPU memory usage:\n```bash\ndocker run -e GPU_MEMORY_FRACTION=0.5 voicebox\n```\n\nOr use CPU-only:\n```bash\ndocker run -e DEVICE=cpu voicebox\n```\n\n### Model Download Fails\n\nEnsure HuggingFace cache is writable:\n```bash\ndocker run -v huggingface-cache:/root/.cache/huggingface voicebox\n```\n\nOr use host cache:\n```bash\ndocker run -v ~/.cache/huggingface:/root/.cache/huggingface voicebox\n```\n\n### Port Already in Use\n\nChange host port:\n```bash\ndocker run -p 8080:8000 voicebox  # Use port 8080 instead\n```\n\n## Security Best Practices\n\n### 1. Don't Run as Root\n\nCreate non-root user in Dockerfile:\n```dockerfile\nRUN useradd -m -u 1000 voicebox\nUSER voicebox\n```\n\n### 2. Use Secrets for API Keys\n\nDon't put API keys in docker-compose.yml:\n\n```bash\n# Use Docker secrets\necho \"sk-your-key\" | docker secret create openai_key -\n\ndocker service create \\\n  --secret openai_key \\\n  -e OPENAI_API_KEY_FILE=/run/secrets/openai_key \\\n  voicebox\n```\n\n### 3. Network Isolation\n\nUse internal networks for multi-container setups:\n\n```yaml\nservices:\n  voicebox:\n    networks:\n      - internal\n  nginx:\n    networks:\n      - internal\n      - external\n    ports:\n      - \"80:80\"\n\nnetworks:\n  internal:\n    internal: true\n  external:\n```\n\n### 4. Resource Limits\n\nPrevent resource exhaustion:\n\n```yaml\nservices:\n  voicebox:\n    deploy:\n      resources:\n        limits:\n          cpus: '4'\n          memory: 8G\n        reservations:\n          cpus: '2'\n          memory: 4G\n```\n\n## Performance Tuning\n\n### GPU Memory Management\n\n```bash\n# Use 80% of GPU (default 90%)\ndocker run -e GPU_MEMORY_FRACTION=0.8 voicebox\n\n# Allow GPU memory growth (prevents OOM)\ndocker run -e TF_FORCE_GPU_ALLOW_GROWTH=true voicebox\n```\n\n### Model Caching\n\nPre-download models to volume:\n\n```bash\n# Download models first\ndocker run --rm -v huggingface-cache:/root/.cache/huggingface \\\n  voicebox python -c \"\nfrom transformers import WhisperProcessor, WhisperForConditionalGeneration\nWhisperProcessor.from_pretrained('openai/whisper-base')\nWhisperForConditionalGeneration.from_pretrained('openai/whisper-base')\n\"\n\n# Then run normally\ndocker run -v huggingface-cache:/root/.cache/huggingface voicebox\n```\n\n### Multi-Worker Setup\n\nUse uvicorn workers for better throughput:\n\n```dockerfile\nCMD [\"uvicorn\", \"backend.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\", \"--workers\", \"4\"]\n```\n\n## Monitoring\n\n### Health Checks\n\nBuilt-in health endpoint:\n```bash\ncurl http://localhost:8000/health\n```\n\nDocker health check:\n```yaml\nhealthcheck:\n  test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8000/health\"]\n  interval: 30s\n  timeout: 10s\n  retries: 3\n```\n\n### Prometheus Metrics\n\nAdd metrics exporter:\n```python\n# backend/main.py\nfrom prometheus_fastapi_instrumentator import Instrumentator\n\nInstrumentator().instrument(app).expose(app)\n```\n\nThen scrape `/metrics` with Prometheus.\n\n### Logs\n\nView container logs:\n```bash\ndocker logs -f voicebox\n\n# Or with compose\ndocker compose logs -f voicebox\n```\n\n## Next Steps\n\n- [ ] Publish official images to GitHub Container Registry\n- [ ] Add Kubernetes Helm charts\n- [ ] Create Docker Desktop extension\n- [ ] Add automated vulnerability scanning\n- [ ] Support ARM64 builds for Raspberry Pi / Apple Silicon\n\n## Contributing\n\nHelp improve Docker support:\n1. Test on different platforms (AMD GPU, ARM64, etc.)\n2. Submit Dockerfile optimizations\n3. Share deployment configurations\n4. Report issues: [GitHub Issues](https://github.com/jamiepine/voicebox/issues)\n\n## Resources\n\n- [Docker Documentation](https://docs.docker.com)\n- [NVIDIA Container Toolkit](https://github.com/NVIDIA/nvidia-docker)\n- [AMD ROCm Docker](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html)\n- [Docker Compose Reference](https://docs.docker.com/compose/compose-file/)\n"
  },
  {
    "path": "docs/plans/OPENAI_SUPPORT.md",
    "content": "# OpenAI API Compatibility\n\n**Status:** Planned for v0.2.0\n\n**Issue:** [#10 OpenAI API compatibility](https://github.com/jamiepine/voicebox/issues/10)\n\n## Overview\n\nThis feature exposes OpenAI-compatible endpoints from Voicebox, allowing any tool, library, or application that speaks the OpenAI Audio API to use Voicebox as a drop-in local replacement.\n\n```mermaid\nflowchart LR\n    subgraph clients [External Clients]\n        SDK[OpenAI SDK]\n        Curl[curl / HTTP]\n        Apps[Third-party Apps]\n    end\n    \n    subgraph voicebox [Voicebox Server]\n        OpenAI[\"/v1/audio/* endpoints\"]\n        TTS[TTSModel]\n        Whisper[WhisperModel]\n        Profiles[Voice Profiles]\n    end\n    \n    SDK --> OpenAI\n    Curl --> OpenAI\n    Apps --> OpenAI\n    OpenAI --> TTS\n    OpenAI --> Whisper\n    OpenAI --> Profiles\n```\n\n## Use Cases\n\n- **OpenAI SDK users**: `openai.audio.speech.create()` works with Voicebox\n- **LLM frameworks**: LangChain, AutoGen, etc. can use Voicebox for TTS\n- **Shell scripts**: `curl` commands copy-pasted from OpenAI docs work\n- **Existing integrations**: Any tool expecting OpenAI's API works without code changes\n\n## Endpoints to Implement\n\n### 1. `POST /v1/audio/speech` (TTS)\n\nOpenAI spec: https://platform.openai.com/docs/api-reference/audio/createSpeech\n\n**Request:**\n\n```json\n{\n  \"model\": \"tts-1\",\n  \"input\": \"Hello world!\",\n  \"voice\": \"alloy\",\n  \"response_format\": \"mp3\",\n  \"speed\": 1.0\n}\n```\n\n**Response:** Audio file (mp3, wav, opus, aac, flac, pcm)\n\n**Voice Mapping Strategy:**\n\n- `voice` parameter maps to Voicebox profile names (case-insensitive)\n- If no match, use a configurable default profile\n- Support special syntax: `voice: \"profile:uuid\"` for explicit profile ID\n\n### 2. `POST /v1/audio/transcriptions` (Whisper)\n\nOpenAI spec: https://platform.openai.com/docs/api-reference/audio/createTranscription\n\n**Request:** (multipart/form-data)\n\n- `file`: Audio file\n- `model`: \"whisper-1\"\n- `language`: Optional language hint\n- `response_format`: json, text, srt, verbose_json, vtt\n\n**Response:**\n\n```json\n{\n  \"text\": \"Hello world!\"\n}\n```\n\n## Implementation Details\n\n### New File: `backend/openai_compat.py`\n\nCreate a dedicated module with an APIRouter for OpenAI-compatible endpoints:\n\n```python\nfrom fastapi import APIRouter, UploadFile, File, Form, HTTPException\nfrom fastapi.responses import StreamingResponse\nfrom pydantic import BaseModel\nfrom typing import Literal, Optional\n\nrouter = APIRouter(prefix=\"/v1/audio\", tags=[\"OpenAI Compatible\"])\n\nclass SpeechRequest(BaseModel):\n    model: str = \"tts-1\"\n    input: str\n    voice: str = \"alloy\"\n    response_format: Literal[\"mp3\", \"wav\", \"opus\", \"aac\", \"flac\", \"pcm\"] = \"mp3\"\n    speed: float = 1.0\n\n@router.post(\"/speech\")\nasync def create_speech(request: SpeechRequest, db: Session = Depends(get_db)):\n    # 1. Map voice name to profile\n    # 2. Generate audio using existing TTSModel\n    # 3. Convert to requested format\n    # 4. Return audio stream\n    ...\n\n@router.post(\"/transcriptions\")\nasync def create_transcription(\n    file: UploadFile = File(...),\n    model: str = Form(\"whisper-1\"),\n    language: Optional[str] = Form(None),\n    response_format: str = Form(\"json\"),\n):\n    # 1. Save uploaded file\n    # 2. Transcribe using existing WhisperModel\n    # 3. Return in requested format\n    ...\n```\n\n### Voice Profile Resolution\n\nAdd helper in [backend/profiles.py](backend/profiles.py):\n\n```python\nasync def resolve_voice_for_openai(voice: str, db: Session) -> Optional[VoiceProfile]:\n    \"\"\"\n    Resolve OpenAI voice parameter to a Voicebox profile.\n    \n    Priority:\n    1. Exact profile name match (case-insensitive)\n    2. Profile ID match (if voice starts with \"profile:\")\n    3. Default profile from config\n    4. First available profile\n    \"\"\"\n    ...\n```\n\n### Audio Format Conversion\n\nAdd conversion utilities in [backend/utils/audio.py](backend/utils/audio.py):\n\n```python\ndef convert_audio_format(\n    audio: np.ndarray,\n    sample_rate: int,\n    target_format: str,  # mp3, wav, opus, aac, flac, pcm\n) -> bytes:\n    \"\"\"Convert audio to target format using ffmpeg or pydub.\"\"\"\n    ...\n```\n\n### Configuration\n\nAdd to [backend/config.py](backend/config.py):\n\n```python\n# OpenAI API Compatibility\nOPENAI_COMPAT_ENABLED = True\nOPENAI_COMPAT_DEFAULT_VOICE = None  # Profile ID or name for default voice\nOPENAI_COMPAT_REQUIRE_AUTH = False  # Require API key validation\nOPENAI_COMPAT_API_KEY = None        # If set, validate against this\n```\n\n### Integration with main.py\n\nIn [backend/main.py](backend/main.py), include the router:\n\n```python\nfrom . import openai_compat\n\n# Add OpenAI-compatible routes\nif config.OPENAI_COMPAT_ENABLED:\n    app.include_router(openai_compat.router)\n```\n\n## Streaming Support (Future Enhancement)\n\nInitial implementation returns complete audio. Streaming can be added later:\n\n```python\n@router.post(\"/speech\")\nasync def create_speech(request: SpeechRequest):\n    if request.stream:\n        return StreamingResponse(\n            generate_audio_chunks(request),\n            media_type=f\"audio/{request.response_format}\"\n        )\n    ...\n```\n\n## Testing\n\nExample usage after implementation:\n\n```bash\n# TTS with curl\ncurl http://localhost:8000/v1/audio/speech \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\": \"tts-1\", \"input\": \"Hello!\", \"voice\": \"MyProfile\"}' \\\n  --output speech.mp3\n\n# With OpenAI Python SDK\nfrom openai import OpenAI\nclient = OpenAI(base_url=\"http://localhost:8000/v1\", api_key=\"unused\")\nresponse = client.audio.speech.create(\n    model=\"tts-1\",\n    voice=\"MyProfile\",\n    input=\"Hello world!\"\n)\nresponse.stream_to_file(\"output.mp3\")\n\n# Transcription\ncurl http://localhost:8000/v1/audio/transcriptions \\\n  -F file=@audio.mp3 \\\n  -F model=\"whisper-1\"\n```\n\n## Security Considerations\n\n- Optional API key validation (for shared deployments)\n- Rate limiting on endpoints\n- Input length limits (same as existing `/generate` endpoint)\n\n## Dependencies\n\n- `pydub` or `ffmpeg-python` for audio format conversion (mp3, opus, etc.)\n- No changes to existing TTS/Whisper model code"
  },
  {
    "path": "docs/postcss.config.mjs",
    "content": "export default {\n  plugins: {\n    '@tailwindcss/postcss': {},\n  },\n};\n"
  },
  {
    "path": "docs/scripts/generate-openapi.ts",
    "content": "import { generateFiles } from 'fumadocs-openapi';\nimport { openapi } from '../lib/openapi';\n\nawait generateFiles({\n  input: openapi,\n  output: 'content/docs/api-reference',\n  groupBy: 'tag',\n});\n\nconsole.log('✓ OpenAPI documentation generated in content/docs/api-reference/');\n"
  },
  {
    "path": "docs/source.config.ts",
    "content": "import {\n  defineConfig,\n  defineDocs,\n  frontmatterSchema,\n  metaSchema,\n} from 'fumadocs-mdx/config';\n\n// You can customise Zod schemas for frontmatter and `meta.json` here\n// see https://fumadocs.dev/docs/mdx/collections\nexport const docs = defineDocs({\n  dir: 'content/docs',\n  docs: {\n    schema: frontmatterSchema,\n    postprocess: {\n      includeProcessedMarkdown: true,\n    },\n  },\n  meta: {\n    schema: metaSchema,\n  },\n});\n\nexport default defineConfig({\n  mdxOptions: {\n    // MDX options\n  },\n});\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ESNext\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"paths\": {\n      \"@/*\": [\n        \"./*\"\n      ],\n      \"@/.source\": [\n        \".source\"\n      ]\n    },\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "justfile",
    "content": "# Voicebox development commands\n# Install: brew install just (or cargo install just)\n# Usage: just --list\n\n# Directories\nbackend_dir := \"backend\"\ntauri_dir := \"tauri\"\napp_dir := \"app\"\nweb_dir := \"web\"\nvenv := backend_dir / \"venv\"\n\n# Platform-aware paths\nvenv_bin := if os() == \"windows\" { venv / \"Scripts\" } else { venv / \"bin\" }\npython := if os() == \"windows\" { venv_bin / \"python.exe\" } else { venv_bin / \"python\" }\npip := if os() == \"windows\" { venv_bin / \"pip.exe\" } else { venv_bin / \"pip\" }\n\n# Shell selection: use powershell on Windows, bash elsewhere\nset windows-shell := [\"powershell\", \"-NoProfile\", \"-Command\"]\n\n# Detect best python for venv creation (platform-aware)\nsystem_python := if os() == \"windows\" { \"python\" } else { `command -v python3.12 2>/dev/null || command -v python3.13 2>/dev/null || echo python3` }\n\n# ─── Setup ────────────────────────────────────────────────────────────\n\n# Full project setup (python venv + JS deps + dev sidecar)\nsetup: setup-python setup-js\n    @echo \"\"\n    @echo \"Setup complete! Run: just dev\"\n\n# Create venv and install Python dependencies\n[unix]\nsetup-python:\n    #!/usr/bin/env bash\n    set -euo pipefail\n    if [ ! -d \"{{ venv }}\" ]; then\n        echo \"Creating Python virtual environment...\"\n        PY_MINOR=$({{ system_python }} -c \"import sys; print(sys.version_info[1])\")\n        if [ \"$PY_MINOR\" -gt 13 ]; then\n            echo \"Warning: Python 3.$PY_MINOR detected. ML packages may not be compatible.\"\n            echo \"Recommended: brew install python@3.12\"\n        fi\n        {{ system_python }} -m venv {{ venv }}\n    fi\n    echo \"Installing Python dependencies...\"\n    {{ pip }} install --upgrade pip -q\n    {{ pip }} install -r {{ backend_dir }}/requirements.txt\n    # Chatterbox pins numpy<1.26 / torch==2.6 which break on Python 3.12+\n    {{ pip }} install --no-deps chatterbox-tts\n    # HumeAI TADA pins torch>=2.7,<2.8 which conflicts with our torch>=2.1\n    {{ pip }} install --no-deps hume-tada\n    # Apple Silicon: install MLX backend\n    if [ \"$(uname -m)\" = \"arm64\" ] && [ \"$(uname)\" = \"Darwin\" ]; then\n        echo \"Detected Apple Silicon — installing MLX dependencies...\"\n        {{ pip }} install -r {{ backend_dir }}/requirements-mlx.txt\n    fi\n    {{ pip }} install git+https://github.com/QwenLM/Qwen3-TTS.git\n    {{ pip }} install pyinstaller ruff pytest pytest-asyncio -q\n    echo \"Python environment ready.\"\n\n[windows]\nsetup-python:\n    if (-not (Test-Path \"{{ venv }}\")) { \\\n        Write-Host \"Creating Python virtual environment...\"; \\\n        $pyMinor = & {{ system_python }} -c \"import sys; print(sys.version_info[1])\"; \\\n        if ([int]$pyMinor -gt 13) { \\\n            Write-Host \"Warning: Python 3.$pyMinor detected. ML packages may not be compatible.\"; \\\n        }; \\\n        & {{ system_python }} -m venv {{ venv }}; \\\n    }\n    Write-Host \"Installing Python dependencies...\"\n    & \"{{ python }}\" -m pip install --upgrade pip -q\n    $hasNvidia = $null -ne (Get-WmiObject Win32_VideoController | Where-Object { $_.Name -match 'NVIDIA' })\n    if ($hasNvidia) { \\\n        Write-Host \"NVIDIA GPU detected — installing PyTorch with CUDA support...\"; \\\n        & \"{{ pip }}\" install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128; \\\n    }\n    & \"{{ pip }}\" install -r {{ backend_dir }}/requirements.txt\n    & \"{{ pip }}\" install --no-deps chatterbox-tts\n    & \"{{ pip }}\" install --no-deps hume-tada\n    & \"{{ pip }}\" install git+https://github.com/QwenLM/Qwen3-TTS.git\n    & \"{{ pip }}\" install pyinstaller ruff pytest pytest-asyncio -q\n    Write-Host \"Python environment ready.\"\n\n# Install JavaScript dependencies\nsetup-js:\n    bun install\n\n# ─── Development ──────────────────────────────────────────────────────\n\n# Start backend (if not already running) + frontend for development\n[unix]\ndev: _ensure-venv _ensure-sidecar\n    #!/usr/bin/env bash\n    set -euo pipefail\n\n    backend_pid=\"\"\n    if curl -sf http://127.0.0.1:17493/health > /dev/null 2>&1; then\n        echo \"Backend already running on http://localhost:17493\"\n    else\n        echo \"Starting backend on http://localhost:17493 ...\"\n        {{ venv_bin }}/uvicorn backend.main:app --reload --port 17493 &\n        backend_pid=$!\n        sleep 2\n    fi\n\n    trap '[ -n \"$backend_pid\" ] && kill \"$backend_pid\" 2>/dev/null; wait' EXIT\n\n    echo \"Starting Tauri desktop app...\"\n    cd {{ tauri_dir }} && bun run tauri dev\n\n[windows]\ndev: _ensure-venv _ensure-sidecar\n    $backendJob = $null; \\\n    try { $null = Invoke-WebRequest -Uri \"http://127.0.0.1:17493/health\" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop; Write-Host \"Backend already running on http://localhost:17493\" } catch { \\\n        Write-Host \"Starting backend on http://localhost:17493 ...\"; \\\n        $backendJob = Start-Process -PassThru -NoNewWindow -FilePath \"{{ python }}\" -ArgumentList \"-m\",\"uvicorn\",\"backend.main:app\",\"--reload\",\"--port\",\"17493\"; \\\n        Start-Sleep -Seconds 2; \\\n    }; \\\n    Write-Host \"Starting Tauri desktop app...\"; \\\n    try { Set-Location \"{{ tauri_dir }}\"; bun run tauri dev } finally { if ($backendJob) { taskkill /PID $backendJob.Id /T /F 2>$null | Out-Null } }\n\n# Start backend only\n[unix]\ndev-backend: _ensure-venv\n    {{ venv_bin }}/uvicorn backend.main:app --reload --port 17493\n\n[windows]\ndev-backend: _ensure-venv\n    & \"{{ python }}\" -m uvicorn backend.main:app --reload --port 17493\n\n# Start Tauri desktop app only (backend must be running separately)\n[unix]\ndev-frontend: _ensure-sidecar\n    cd {{ tauri_dir }} && bun run tauri dev\n\n[windows]\ndev-frontend: _ensure-sidecar\n    Set-Location \"{{ tauri_dir }}\"; bun run tauri dev\n\n# Start backend (if not already running) + web app (no Tauri)\n[unix]\ndev-web: _ensure-venv\n    #!/usr/bin/env bash\n    set -euo pipefail\n\n    backend_pid=\"\"\n    if curl -sf http://127.0.0.1:17493/health > /dev/null 2>&1; then\n        echo \"Backend already running on http://localhost:17493\"\n    else\n        echo \"Starting backend on http://localhost:17493 ...\"\n        {{ venv_bin }}/uvicorn backend.main:app --reload --port 17493 &\n        backend_pid=$!\n        sleep 2\n    fi\n\n    trap '[ -n \"$backend_pid\" ] && kill \"$backend_pid\" 2>/dev/null; wait' EXIT\n\n    cd {{ web_dir }} && bun run dev\n\n[windows]\ndev-web: _ensure-venv\n    $backendJob = $null; \\\n    try { $null = Invoke-WebRequest -Uri \"http://127.0.0.1:17493/health\" -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop; Write-Host \"Backend already running on http://localhost:17493\" } catch { \\\n        Write-Host \"Starting backend on http://localhost:17493 ...\"; \\\n        $backendJob = Start-Process -PassThru -NoNewWindow -FilePath \"{{ python }}\" -ArgumentList \"-m\",\"uvicorn\",\"backend.main:app\",\"--reload\",\"--port\",\"17493\"; \\\n        Start-Sleep -Seconds 2; \\\n    }; \\\n    Write-Host \"Starting web app...\"; \\\n    try { Set-Location \"{{ web_dir }}\"; bun run dev } finally { if ($backendJob) { taskkill /PID $backendJob.Id /T /F 2>$null | Out-Null } }\n\n# Kill all dev processes\n[unix]\nkill:\n    -pkill -f \"uvicorn backend.main:app\" 2>/dev/null || true\n    -pkill -f \"vite\" 2>/dev/null || true\n    @echo \"Dev processes killed.\"\n\n[windows]\nkill:\n    Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*uvicorn*backend.main*' -or $_.CommandLine -like '*vite*' } | Stop-Process -Force -ErrorAction SilentlyContinue\n    Write-Host \"Dev processes killed.\"\n\n# ─── Build ────────────────────────────────────────────────────────────\n\n# Build everything (server binary + desktop app)\nbuild: build-server build-tauri\n\n# Build Python server binary (CPU)\n[unix]\nbuild-server: _ensure-venv\n    PATH=\"{{ venv_bin }}:$PATH\" ./scripts/build-server.sh\n\n[windows]\nbuild-server: _ensure-venv\n    $ErrorActionPreference = \"Stop\"; \\\n    $env:PATH = \"{{ venv_bin }};$env:PATH\"; \\\n    & \"{{ python }}\" backend/build_binary.py; \\\n    if ($LASTEXITCODE -ne 0) { throw \"build_binary.py failed with exit code $LASTEXITCODE\" }; \\\n    $triple = (rustc --print host-tuple); \\\n    New-Item -ItemType Directory -Path \"{{ tauri_dir }}/src-tauri/binaries\" -Force | Out-Null; \\\n    Copy-Item \"backend/dist/voicebox-server.exe\" \"{{ tauri_dir }}/src-tauri/binaries/voicebox-server-$triple.exe\" -Force; \\\n    Write-Host \"Copied sidecar: voicebox-server-$triple.exe\"\n\n# Build CUDA server binary and place in app data dir for local testing\n[windows]\nbuild-server-cuda: _ensure-venv\n    $ErrorActionPreference = \"Stop\"; \\\n    $env:PATH = \"{{ venv_bin }};$env:PATH\"; \\\n    & \"{{ python }}\" backend/build_binary.py --cuda; \\\n    if ($LASTEXITCODE -ne 0) { throw \"build_binary.py --cuda failed with exit code $LASTEXITCODE\" }; \\\n    $dest = \"$env:APPDATA/sh.voicebox.app/backends/cuda\"; \\\n    if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }; \\\n    New-Item -ItemType Directory -Path $dest -Force | Out-Null; \\\n    Copy-Item \"backend/dist/voicebox-server-cuda/*\" $dest -Recurse -Force; \\\n    Write-Host \"Copied CUDA backend to $dest\"\n\n# Build everything locally: CPU server + CUDA server + installable Tauri app\n[windows]\nbuild-local: build-server build-server-cuda build-tauri\n\n# Build Tauri desktop app\n[unix]\nbuild-tauri:\n    cd {{ tauri_dir }} && bun run tauri build\n\n[windows]\nbuild-tauri:\n    Set-Location \"{{ tauri_dir }}\"; bun run tauri build\n\n# Build web app\n[unix]\nbuild-web:\n    cd {{ web_dir }} && bun run build\n\n[windows]\nbuild-web:\n    Set-Location \"{{ web_dir }}\"; bun run build\n\n# ─── Code Quality ────────────────────────────────────────────────────\n\n# Run all checks (JS + Python lint + format)\ncheck: check-js check-python\n\n# JS/TS: lint + format + typecheck (Biome)\ncheck-js:\n    bun run check\n\n# Python: lint + format check (ruff)\ncheck-python: _ensure-venv\n    {{ venv_bin }}/ruff check {{ backend_dir }}\n    {{ venv_bin }}/ruff format --check {{ backend_dir }}\n\n# Lint with Biome (JS) + ruff (Python)\nlint: _ensure-venv\n    bun run lint\n    {{ venv_bin }}/ruff check {{ backend_dir }}\n\n# Format with Biome (JS) + ruff (Python)\nformat: _ensure-venv\n    bun run format\n    {{ venv_bin }}/ruff format {{ backend_dir }}\n\n# Fix lint + format issues (JS + Python)\nfix: _ensure-venv\n    bun run check:fix\n    {{ venv_bin }}/ruff check {{ backend_dir }} --fix\n    {{ venv_bin }}/ruff format {{ backend_dir }}\n\n# Python lint only\nlint-python: _ensure-venv\n    {{ venv_bin }}/ruff check {{ backend_dir }}\n\n# Python format only\nformat-python: _ensure-venv\n    {{ venv_bin }}/ruff format {{ backend_dir }}\n\n# Python auto-fix lint issues\nfix-python: _ensure-venv\n    {{ venv_bin }}/ruff check {{ backend_dir }} --fix\n    {{ venv_bin }}/ruff format {{ backend_dir }}\n\n# Run Python tests\ntest: _ensure-venv\n    {{ venv_bin }}/python -m pytest {{ backend_dir }}/tests -v\n\n# ─── Database ─────────────────────────────────────────────────────────\n\n# Initialize SQLite database\n[unix]\ndb-init: _ensure-venv\n    {{ python }} -c \"from backend.database import init_db; init_db()\"\n\n[windows]\ndb-init: _ensure-venv\n    & \"{{ python }}\" -c \"from backend.database import init_db; init_db()\"\n\n# Reset database (delete + reinit)\n[unix]\ndb-reset:\n    rm -f {{ backend_dir }}/data/voicebox.db\n    just db-init\n\n[windows]\ndb-reset:\n    if (Test-Path \"{{ backend_dir }}/data/voicebox.db\") { Remove-Item -Force \"{{ backend_dir }}/data/voicebox.db\" }\n    just db-init\n\n# ─── Utilities ────────────────────────────────────────────────────────\n\n# Generate TypeScript API client (backend must be running)\n[unix]\ngenerate-api:\n    ./scripts/generate-api.sh\n\n[windows]\ngenerate-api:\n    bash scripts/generate-api.sh\n\n# Open API docs in browser\n[unix]\ndocs:\n    open http://localhost:17493/docs 2>/dev/null || xdg-open http://localhost:17493/docs\n\n[windows]\ndocs:\n    Start-Process \"http://localhost:17493/docs\"\n\n# Tail backend logs\n[unix]\nlogs:\n    tail -f {{ backend_dir }}/logs/*.log 2>/dev/null || echo \"No log files found\"\n\n[windows]\nlogs:\n    Get-ChildItem {{ backend_dir }}/logs/*.log -ErrorAction SilentlyContinue | ForEach-Object { Get-Content $_.FullName -Tail 50 -Wait } ; if (-not $?) { Write-Host \"No log files found\" }\n\n# ─── Clean ────────────────────────────────────────────────────────────\n\n# Clean build artifacts\n[unix]\nclean:\n    rm -rf {{ tauri_dir }}/src-tauri/target/release\n    rm -rf {{ web_dir }}/dist\n    rm -rf {{ app_dir }}/dist\n\n[windows]\nclean:\n    if (Test-Path \"{{ tauri_dir }}/src-tauri/target/release\") { Remove-Item -Recurse -Force \"{{ tauri_dir }}/src-tauri/target/release\" }\n    if (Test-Path \"{{ web_dir }}/dist\") { Remove-Item -Recurse -Force \"{{ web_dir }}/dist\" }\n    if (Test-Path \"{{ app_dir }}/dist\") { Remove-Item -Recurse -Force \"{{ app_dir }}/dist\" }\n\n# Clean Python venv and cache\n[unix]\nclean-python:\n    rm -rf {{ venv }}\n    find {{ backend_dir }} -type d -name \"__pycache__\" -exec rm -rf {} + 2>/dev/null || true\n\n[windows]\nclean-python:\n    if (Test-Path \"{{ venv }}\") { Remove-Item -Recurse -Force \"{{ venv }}\" }\n    Get-ChildItem -Path \"{{ backend_dir }}\" -Directory -Recurse -Filter \"__pycache__\" -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force\n\n# Nuclear clean (everything including node_modules)\n[unix]\nclean-all: clean clean-python\n    rm -rf node_modules\n    rm -rf {{ app_dir }}/node_modules\n    rm -rf {{ tauri_dir }}/node_modules\n    rm -rf {{ web_dir }}/node_modules\n    cd {{ tauri_dir }}/src-tauri && cargo clean\n\n[windows]\nclean-all: clean clean-python\n    if (Test-Path \"node_modules\") { Remove-Item -Recurse -Force \"node_modules\" }\n    if (Test-Path \"{{ app_dir }}/node_modules\") { Remove-Item -Recurse -Force \"{{ app_dir }}/node_modules\" }\n    if (Test-Path \"{{ tauri_dir }}/node_modules\") { Remove-Item -Recurse -Force \"{{ tauri_dir }}/node_modules\" }\n    if (Test-Path \"{{ web_dir }}/node_modules\") { Remove-Item -Recurse -Force \"{{ web_dir }}/node_modules\" }\n    Push-Location \"{{ tauri_dir }}/src-tauri\"; cargo clean; Pop-Location\n\n# ─── Internal ─────────────────────────────────────────────────────────\n\n# Ensure venv exists (prompt to run setup if not)\n[private, unix]\n_ensure-venv:\n    #!/usr/bin/env bash\n    if [ ! -d \"{{ venv }}\" ]; then\n        echo \"Python venv not found. Run: just setup\"\n        exit 1\n    fi\n\n[private, windows]\n_ensure-venv:\n    if (-not (Test-Path \"{{ venv }}\")) { Write-Host \"Python venv not found. Run: just setup\"; exit 1 }\n\n# Ensure Tauri dev sidecar placeholder exists\n[private]\n_ensure-sidecar:\n    bun run setup:dev\n"
  },
  {
    "path": "landing/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts"
  },
  {
    "path": "landing/README.md",
    "content": "# Voicebox Landing Page\n\nLanding page for voicebox.sh - a modern Next.js 16 application.\n\n## Tech Stack\n\n- **Next.js 16** with App Router\n- **Bun** for package management\n- **Tailwind CSS** with shadcn/ui components\n- **TypeScript** with strict mode\n- **Railway** deployment ready\n\n## Getting Started\n\n### Prerequisites\n\n- Bun installed ([bun.sh](https://bun.sh))\n\n### Installation\n\n```bash\ncd landing\nbun install\n```\n\n### Development\n\n```bash\nbun run dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) to view the landing page.\n\n### Build\n\n```bash\nbun run build\n```\n\n### Production\n\n```bash\nbun run start\n```\n\n## Configuration\n\n### Update Download Links\n\nEdit `src/lib/constants.ts` to update:\n- `LATEST_VERSION` - Current release version\n- `DOWNLOAD_LINKS` - GitHub release download URLs\n- `GITHUB_REPO` - Repository URL\n\n### Update GitHub Username\n\nReplace `USERNAME` in `src/lib/constants.ts` with your actual GitHub username.\n\n## Deployment to Railway\n\n1. Connect your GitHub repository to Railway\n2. Railway will auto-detect `nixpacks.toml`\n3. Set root directory to `landing/`\n4. Railway will automatically:\n   - Install dependencies with `bun install`\n   - Build with `bun run build`\n   - Start with `bun run start`\n5. Configure custom domain `voicebox.sh` in Railway settings\n\n## Project Structure\n\n```\nlanding/\n├── src/\n│   ├── app/\n│   │   ├── layout.tsx      # Root layout with metadata\n│   │   ├── page.tsx        # Landing page\n│   │   └── globals.css     # Global styles\n│   ├── components/\n│   │   ├── Header.tsx      # Top navigation\n│   │   ├── Footer.tsx      # Footer\n│   │   ├── DownloadSection.tsx  # Download buttons\n│   │   └── ui/             # shadcn/ui components\n│   └── lib/\n│       ├── utils.ts        # Utility functions\n│       └── constants.ts    # App constants\n├── public/\n│   └── voicebox-logo.png   # Logo asset\n└── nixpacks.toml          # Railway deployment config\n```\n\n## Features\n\n- Responsive design (mobile-first)\n- Dark mode by default\n- SEO optimized metadata\n- Download links for Mac, Windows, Linux\n- Feature showcase\n- Platform highlights\n- GitHub integration\n"
  },
  {
    "path": "landing/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/app/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\"\n  }\n}\n"
  },
  {
    "path": "landing/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  output: 'standalone',\n  images: {\n    unoptimized: false,\n    formats: ['image/avif', 'image/webp'],\n  },\n  turbopack: {},\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "landing/nixpacks.toml",
    "content": "[phases.setup]\nnixPkgs = [\"nodejs_20\", \"bun\"]\n\n[phases.install]\ncmds = [\"bun install\"]\n\n[phases.build]\ncmds = [\"bun run build\"]\n\n[start]\ncmd = \"bun run start\"\n"
  },
  {
    "path": "landing/package.json",
    "content": "{\n  \"name\": \"@voicebox/landing\",\n  \"version\": \"0.3.1\",\n  \"description\": \"Landing page for voicebox.sh\",\n  \"scripts\": {\n    \"dev\": \"bun --bun next dev --turbo\",\n    \"build\": \"bun --bun next build\",\n    \"start\": \"bun --bun next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@fontsource/space-grotesk\": \"^5.2.10\",\n    \"@radix-ui/react-separator\": \"^1.1.8\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"autoprefixer\": \"^10.4.17\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"framer-motion\": \"^12.36.0\",\n    \"lucide-react\": \"^0.316.0\",\n    \"next\": \"^16.1.3\",\n    \"postcss\": \"^8.4.33\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"tailwind-merge\": \"^3.4.0\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"wavesurfer.js\": \"^7.12.2\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20.11.5\",\n    \"@types/react\": \"^18.2.48\",\n    \"@types/react-dom\": \"^18.2.18\",\n    \"typescript\": \"^5.3.3\"\n  }\n}\n"
  },
  {
    "path": "landing/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "landing/src/app/api/releases/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { getLatestRelease } from '@/lib/releases';\n\nexport const dynamic = 'force-dynamic';\n\nexport async function GET() {\n  try {\n    const releaseInfo = await getLatestRelease();\n    return NextResponse.json(releaseInfo);\n  } catch (error) {\n    console.error('Error fetching release info:', error);\n    return NextResponse.json({ error: 'Failed to fetch release information' }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "landing/src/app/api/stars/route.ts",
    "content": "import { NextResponse } from 'next/server';\nimport { getStarCount } from '@/lib/releases';\n\nexport const dynamic = 'force-dynamic';\nexport const revalidate = 600;\n\nexport async function GET() {\n  try {\n    const count = await getStarCount();\n    return NextResponse.json({ count });\n  } catch (error) {\n    console.error('Error fetching star count:', error);\n    return NextResponse.json({ error: 'Failed to fetch star count' }, { status: 500 });\n  }\n}\n"
  },
  {
    "path": "landing/src/app/download/[platform]/route.ts",
    "content": "import { type NextRequest, NextResponse } from 'next/server';\nimport { getLatestRelease } from '@/lib/releases';\n\nexport const dynamic = 'force-dynamic';\n\nconst PLATFORM_MAP: Record<\n  string,\n  keyof Awaited<ReturnType<typeof getLatestRelease>>['downloadLinks']\n> = {\n  'mac-arm': 'macArm',\n  'mac-intel': 'macIntel',\n  windows: 'windows',\n  linux: 'linux',\n};\n\nexport async function GET(\n  _request: NextRequest,\n  { params }: { params: Promise<{ platform: string }> },\n) {\n  const { platform } = await params;\n  const key = PLATFORM_MAP[platform];\n\n  if (!key) {\n    return NextResponse.json(\n      { error: `Unknown platform: ${platform}. Use: ${Object.keys(PLATFORM_MAP).join(', ')}` },\n      { status: 404 },\n    );\n  }\n\n  try {\n    const release = await getLatestRelease();\n    const url = release.downloadLinks[key];\n\n    if (!url) {\n      return NextResponse.json({ error: `No download available for ${platform}` }, { status: 404 });\n    }\n\n    return NextResponse.redirect(url);\n  } catch {\n    return NextResponse.redirect(`https://github.com/jamiepine/voicebox/releases/latest`);\n  }\n}\n"
  },
  {
    "path": "landing/src/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 0 0% 0%;\n    --card: 0 0% 100%;\n    --card-foreground: 0 0% 0%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 0 0% 0%;\n    --primary: 0 0% 0%;\n    --primary-foreground: 0 0% 100%;\n    --secondary: 0 0% 96%;\n    --secondary-foreground: 0 0% 0%;\n    --muted: 0 0% 96%;\n    --muted-foreground: 0 0% 45%;\n    --accent: 43 50% 50%;\n    --accent-foreground: 0 0% 0%;\n    --destructive: 0 0% 0%;\n    --destructive-foreground: 0 0% 100%;\n    --border: 0 0% 90%;\n    --input: 0 0% 90%;\n    --ring: 0 0% 0%;\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    /* Surfaces -- slightly warm-tinted darks */\n    --background: 30 4% 4%;\n    --foreground: 30 10% 94%;\n    --card: 30 4% 7%;\n    --card-foreground: 30 10% 94%;\n    --popover: 30 4% 7%;\n    --popover-foreground: 30 10% 94%;\n    --primary: 30 10% 94%;\n    --primary-foreground: 30 4% 7%;\n    --secondary: 30 4% 10%;\n    --secondary-foreground: 30 10% 94%;\n    --muted: 30 3% 12%;\n    --muted-foreground: 30 5% 55%;\n    --accent: 43 50% 45%;\n    --accent-foreground: 30 10% 94%;\n    --destructive: 0 62% 50%;\n    --destructive-foreground: 30 10% 94%;\n    --border: 30 4% 13%;\n    --input: 30 4% 13%;\n    --ring: 30 10% 94% / 0.2;\n    --radius: 0.75rem;\n\n    /* App-specific surface tokens */\n    --app: 30 4% 4%;\n    --app-box: 30 4% 7%;\n    --app-dark-box: 30 4% 5%;\n    --app-darker-box: 30 4% 3%;\n    --app-light-box: 30 4% 14%;\n    --app-line: 30 4% 13%;\n    --app-button: 30 4% 11%;\n    --app-hover: 30 4% 15%;\n    --app-selected: 30 4% 17%;\n\n    /* Text hierarchy */\n    --ink: 30 10% 94%;\n    --ink-dull: 30 5% 55%;\n    --ink-faint: 30 3% 38%;\n\n    /* Accent shades */\n    --accent-faint: 43 45% 55%;\n    --accent-deep: 43 55% 35%;\n    --accent-glow: 43 60% 50%;\n\n    /* Sidebar */\n    --sidebar: 30 4% 3%;\n    --sidebar-line: 30 4% 10%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  html {\n    overflow-x: hidden;\n  }\n  body {\n    @apply bg-background text-foreground antialiased;\n    overflow-x: hidden;\n    font-family:\n      ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n      \"Helvetica Neue\", Arial, sans-serif;\n  }\n}\n\n@layer utilities {\n  .text-balance {\n    text-wrap: balance;\n  }\n}\n\n/* Staggered fade-in animation for hero elements */\n@keyframes fadeUp {\n  from {\n    opacity: 0;\n    transform: translateY(16px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.fade-in {\n  opacity: 0;\n  animation: fadeUp 0.6s ease-out forwards;\n}\n\n.hero-glow-fade {\n  opacity: 0;\n  animation: fadeIn 2s ease-out 0.3s forwards;\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n/* Noise texture overlay for hero glow */\n/* .hero-glow::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  z-index: 5;\n  pointer-events: none;\n  background: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2048' height='2048'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.5' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\") center / 100% 100% no-repeat;\n  opacity: 0.35;\n  mix-blend-mode: overlay;\n  will-change: transform;\n} */\n\n/* Scrollbar hiding */\n::-webkit-scrollbar {\n  display: none;\n}\n\n* {\n  scrollbar-width: none;\n}\n"
  },
  {
    "path": "landing/src/app/layout.tsx",
    "content": "import type { Metadata } from 'next';\nimport './globals.css';\n\nexport const metadata: Metadata = {\n  metadataBase: new URL('https://voicebox.sh'),\n  title: 'Voicebox - Open Source Voice Cloning Desktop App',\n  description:\n    'Near-perfect voice cloning with multiple TTS engines. Desktop app for Mac, Windows, and Linux. Multi-sample support, smart caching, local or remote inference.',\n  keywords: [\n    'voice cloning',\n    'TTS',\n    'multi-engine',\n    'desktop app',\n    'AI voice',\n    'open source',\n    'text to speech',\n  ],\n  icons: {\n    icon: [\n      { url: '/favicon.png', type: 'image/png' },\n      { url: '/favicon.ico', sizes: 'any' },\n    ],\n    apple: [{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' }],\n  },\n  openGraph: {\n    title: 'Voicebox',\n    description: 'Open source voice cloning. Local-first. Free forever.',\n    type: 'website',\n    url: 'https://voicebox.sh',\n    images: [{ url: '/og.webp', width: 1200, height: 630 }],\n  },\n  twitter: {\n    card: 'summary_large_image',\n    title: 'Voicebox',\n    description: 'Open source voice cloning. Local-first. Free forever.',\n    images: ['/og.webp'],\n  },\n};\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning className=\"dark\">\n      <head>\n        <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n        <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossOrigin=\"anonymous\" />\n        <link\n          href=\"https://fonts.googleapis.com/css2?family=Caveat:wght@400;500&display=swap\"\n          rel=\"stylesheet\"\n        />\n      </head>\n      <body>\n        <div className=\"relative min-h-screen bg-background font-sans\">{children}</div>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "landing/src/app/linux-install/page.tsx",
    "content": "import type { Metadata } from 'next';\nimport { Footer } from '@/components/Footer';\nimport { Navbar } from '@/components/Navbar';\nimport { GITHUB_REPO } from '@/lib/constants';\n\nexport const metadata: Metadata = {\n  title: 'Linux Install - Voicebox',\n  description: 'Build Voicebox from source on Linux. Clone, setup, and build in three commands.',\n};\n\nexport default function LinuxInstall() {\n  return (\n    <>\n      <Navbar />\n\n      <section className=\"relative pt-32 pb-24\">\n        <div className=\"mx-auto max-w-2xl px-6\">\n          <h1 className=\"text-3xl font-bold tracking-tight text-foreground\">Install on Linux</h1>\n\n          <p className=\"mt-4 text-muted-foreground\">\n            We&apos;re currently working through CI issues that prevent us from shipping a reliable\n            pre-built binary for Linux. In the meantime, building from source is straightforward and\n            takes just a few minutes.\n          </p>\n\n          <div className=\"mt-10 space-y-6\">\n            {/* Prerequisites */}\n            <div>\n              <h2 className=\"text-sm font-medium text-muted-foreground uppercase tracking-wider mb-3\">\n                Prerequisites\n              </h2>\n              <ul className=\"list-disc list-inside text-sm text-muted-foreground space-y-1\">\n                <li>\n                  <a\n                    href=\"https://git-scm.com\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"text-foreground hover:underline\"\n                  >\n                    Git\n                  </a>\n                </li>\n                <li>\n                  <a\n                    href=\"https://www.rust-lang.org/tools/install\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"text-foreground hover:underline\"\n                  >\n                    Rust\n                  </a>\n                </li>\n                <li>\n                  <a\n                    href=\"https://github.com/casey/just#installation\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"text-foreground hover:underline\"\n                  >\n                    just\n                  </a>{' '}\n                  — install via{' '}\n                  <code className=\"text-xs bg-muted px-1.5 py-0.5 rounded\">cargo install just</code>\n                </li>\n                <li>\n                  <a\n                    href=\"https://bun.sh\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"text-foreground hover:underline\"\n                  >\n                    Bun\n                  </a>\n                </li>\n                <li>\n                  Tauri system deps —{' '}\n                  <a\n                    href=\"https://v2.tauri.app/start/prerequisites/#linux\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"text-foreground hover:underline\"\n                  >\n                    see Tauri docs\n                  </a>\n                </li>\n              </ul>\n            </div>\n\n            {/* Steps */}\n            <div>\n              <h2 className=\"text-sm font-medium text-muted-foreground uppercase tracking-wider mb-3\">\n                Build from source\n              </h2>\n              <div className=\"space-y-3\">\n                <div className=\"rounded-lg border border-border bg-card/60 p-4 font-mono text-sm\">\n                  <div className=\"text-muted-foreground select-none\"># Clone the repo</div>\n                  <div>git clone https://github.com/jamiepine/voicebox.git</div>\n                  <div>cd voicebox</div>\n                </div>\n\n                <div className=\"rounded-lg border border-border bg-card/60 p-4 font-mono text-sm\">\n                  <div className=\"text-muted-foreground select-none\">\n                    # Install all dependencies (Python venv, JS deps, etc.)\n                  </div>\n                  <div>just setup</div>\n                </div>\n\n                <div className=\"rounded-lg border border-border bg-card/60 p-4 font-mono text-sm\">\n                  <div className=\"text-muted-foreground select-none\"># Build the app</div>\n                  <div>just build</div>\n                </div>\n              </div>\n\n              <p className=\"mt-4 text-sm text-muted-foreground\">\n                The built app will be in{' '}\n                <code className=\"text-xs bg-muted px-1.5 py-0.5 rounded\">\n                  tauri/src-tauri/target/release/bundle/\n                </code>\n              </p>\n            </div>\n\n            {/* Dev mode */}\n            <div>\n              <h2 className=\"text-sm font-medium text-muted-foreground uppercase tracking-wider mb-3\">\n                Or run in dev mode\n              </h2>\n              <div className=\"rounded-lg border border-border bg-card/60 p-4 font-mono text-sm\">\n                <div className=\"text-muted-foreground select-none\">\n                  # Start the dev server with hot reload\n                </div>\n                <div>just dev</div>\n              </div>\n            </div>\n          </div>\n\n          {/* Links */}\n          <div className=\"mt-12 pt-8 border-t border-border flex flex-wrap gap-4 text-sm\">\n            <a\n              href={GITHUB_REPO}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              GitHub Repo\n            </a>\n            <a\n              href={`${GITHUB_REPO}/issues`}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              Report an issue\n            </a>\n            <a\n              href={`${GITHUB_REPO}/blob/main/CONTRIBUTING.md`}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              Contributing guide\n            </a>\n          </div>\n        </div>\n      </section>\n\n      <Footer />\n    </>\n  );\n}\n"
  },
  {
    "path": "landing/src/app/og/page.tsx",
    "content": "'use client';\n\nexport default function OgPreview() {\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-[#0a0a09] p-10\">\n      {/* The card — standard OG image dimensions */}\n      <div\n        id=\"og\"\n        className=\"relative flex items-center overflow-hidden\"\n        style={{\n          width: 1200,\n          height: 630,\n          background:\n            'radial-gradient(ellipse 80% 70% at 50% 45%, hsla(43,60%,50%,0.12) 0%, hsla(43,60%,50%,0.04) 40%, transparent 70%), linear-gradient(180deg, hsl(30,4%,6%) 0%, hsl(30,4%,4%) 100%)',\n        }}\n      >\n        {/* Logo + text — left-justified, horizontal */}\n        <div className=\"relative z-10 flex items-center\" style={{ paddingLeft: 20 }}>\n          {/* Glow behind logo */}\n          <div\n            className=\"pointer-events-none absolute rounded-full blur-[100px]\"\n            style={{\n              width: 300,\n              height: 300,\n              top: '50%',\n              left: 0,\n              transform: 'translateY(-50%)',\n              background: 'hsla(43, 60%, 50%, 0.15)',\n            }}\n          />\n          <img\n            src=\"/voicebox-logo-app.webp\"\n            alt=\"\"\n            className=\"relative shrink-0 object-contain\"\n            style={{ width: 260, height: 260 }}\n            draggable={false}\n          />\n          <div className=\"flex flex-col\" style={{ marginLeft: -8 }}>\n            <h1\n              className=\"font-bold tracking-tight\"\n              style={{\n                fontSize: 72,\n                lineHeight: 1,\n                color: 'hsl(30, 10%, 94%)',\n              }}\n            >\n              Voicebox\n            </h1>\n            <p\n              style={{\n                fontSize: 24,\n                lineHeight: 1.4,\n                marginTop: 16,\n                color: 'hsl(30, 5%, 55%)',\n              }}\n            >\n              Open source voice cloning.\n              <br />\n              Local-first. Free forever.\n            </p>\n          </div>\n        </div>\n\n        {/* App screenshot — right, overflowing */}\n        <img\n          src=\"/assets/app-screenshot-1.webp\"\n          alt=\"\"\n          className=\"pointer-events-none absolute top-1/2 -translate-y-1/2 z-10\"\n          style={{\n            right: -300,\n            width: 900,\n          }}\n          draggable={false}\n        />\n\n        {/* Border overlay */}\n        <div className=\"pointer-events-none absolute inset-0 ring-1 ring-inset ring-white/[0.06]\" />\n      </div>\n\n      {/* Helper text */}\n      <div className=\"fixed bottom-6 left-1/2 -translate-x-1/2 text-xs text-white/30\">\n        1200 &times; 630 &mdash; Right-click the card or screenshot at 1:1 zoom\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "landing/src/app/page.tsx",
    "content": "'use client';\n\nimport { Github, Globe, Languages, MessageSquare, Zap } from 'lucide-react';\nimport { useEffect, useState } from 'react';\nimport { ControlUI } from '@/components/ControlUI';\nimport { Features } from '@/components/Features';\nimport { Footer } from '@/components/Footer';\nimport { Navbar } from '@/components/Navbar';\nimport { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/PlatformIcons';\nimport { VoiceCreator } from '@/components/VoiceCreator';\nimport { DOWNLOAD_LINKS, GITHUB_REPO } from '@/lib/constants';\nimport type { DownloadLinks } from '@/lib/releases';\n\nexport default function Home() {\n  const [downloadLinks, setDownloadLinks] = useState<DownloadLinks>(DOWNLOAD_LINKS);\n  const [version, setVersion] = useState<string | null>(null);\n  const [totalDownloads, setTotalDownloads] = useState<number | null>(null);\n\n  useEffect(() => {\n    fetch('/api/releases')\n      .then((res) => {\n        if (!res.ok) throw new Error('Failed to fetch releases');\n        return res.json();\n      })\n      .then((data) => {\n        if (data.downloadLinks) setDownloadLinks(data.downloadLinks);\n        if (data.version) setVersion(data.version);\n        if (data.totalDownloads != null) setTotalDownloads(data.totalDownloads);\n      })\n      .catch((error) => {\n        console.error('Failed to fetch release info:', error);\n      });\n  }, []);\n\n  return (\n    <>\n      <Navbar />\n\n      {/* ── Hero Section ─────────────────────────────────────────────── */}\n      <section className=\"relative pt-32 pb-16\">\n        {/* Background glow */}\n        <div className=\"hero-glow hero-glow-fade pointer-events-none absolute inset-0 -top-32\">\n          <div className=\"absolute left-1/2 top-0 -translate-x-1/2 w-[800px] h-[600px] rounded-full bg-accent/15 blur-[150px]\" />\n          <div className=\"absolute left-1/2 top-12 -translate-x-1/2 w-[500px] h-[400px] rounded-full bg-accent/10 blur-[80px]\" />\n        </div>\n\n        <div className=\"relative mx-auto max-w-7xl px-6 text-center\">\n          {/* Logo */}\n          <div\n            className=\"fade-in mx-auto mb-8 h-[120px] w-[120px] md:h-[160px] md:w-[160px]\"\n            style={{ animationDelay: '0ms' }}\n          >\n            {/* eslint-disable-next-line @next/next/no-img-element */}\n            <img\n              src=\"/voicebox-logo-app.webp\"\n              alt=\"Voicebox\"\n              className=\"h-full w-full object-contain\"\n            />\n          </div>\n\n          {/* Headline */}\n          <div className=\"fade-in relative\" style={{ animationDelay: '100ms' }}>\n            <h1 className=\"text-5xl font-bold tracking-tighter leading-[0.9] text-foreground md:text-7xl lg:text-8xl\">\n              Your voice, your machine.\n            </h1>\n          </div>\n\n          {/* Subtitle */}\n          <p\n            className=\"fade-in mx-auto mt-6 max-w-2xl text-lg text-muted-foreground md:text-xl\"\n            style={{ animationDelay: '200ms' }}\n          >\n            Open source voice cloning studio with support for multiple TTS engines. Clone any voice,\n            generate natural speech, and compose multi-voice projects — all running locally.\n          </p>\n\n          {/* CTAs */}\n          <div\n            className=\"fade-in mt-10 flex flex-col sm:flex-row items-center justify-center gap-4\"\n            style={{ animationDelay: '300ms' }}\n          >\n            <a\n              href=\"#download\"\n              className=\"rounded-full bg-accent px-8 py-3.5 text-sm font-semibold uppercase tracking-wider text-white shadow-[0_4px_20px_hsl(43_60%_50%/0.3),inset_0_2px_0_rgba(255,255,255,0.2),inset_0_-2px_0_rgba(0,0,0,0.1)] transition-all hover:bg-accent-faint active:shadow-[0_2px_10px_hsl(43_60%_50%/0.3),inset_0_4px_8px_rgba(0,0,0,0.3)]\"\n            >\n              Download\n            </a>\n            <a\n              href={GITHUB_REPO}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"flex items-center gap-2 rounded-full border border-border/60 bg-card/40 backdrop-blur-sm px-6 py-3 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground hover:border-border\"\n            >\n              <Github className=\"h-4 w-4\" />\n              View on GitHub\n            </a>\n          </div>\n\n          {/* Version + downloads */}\n          <p\n            className=\"fade-in mt-4 text-xs text-muted-foreground/50\"\n            style={{ animationDelay: '400ms' }}\n          >\n            {version ?? ''}\n            {version && totalDownloads != null ? ' \\u00b7 ' : ''}\n            {totalDownloads != null ? `${totalDownloads.toLocaleString()} downloads` : ''}\n            {version || totalDownloads != null ? ' \\u00b7 ' : ''}\n            macOS, Windows, Linux\n          </p>\n        </div>\n\n        {/* ── ControlUI mockup ─────────────────────────────────────── */}\n        <div className=\"mt-16\">\n          <ControlUI />\n        </div>\n      </section>\n\n      {/* ── Features ─────────────────────────────────────────────── */}\n      <Features />\n\n      {/* ── Voice Creator ────────────────────────────────────────── */}\n      <VoiceCreator />\n\n      {/* ── Models ─────────────────────────────────────────────────── */}\n      <section id=\"about\" className=\"border-t border-border py-24\">\n        <div className=\"mx-auto max-w-5xl px-6\">\n          <div className=\"text-center mb-14\">\n            <h2 className=\"text-3xl font-semibold tracking-tight text-foreground md:text-4xl mb-4\">\n              Multi-Engine Architecture\n            </h2>\n            <p className=\"text-muted-foreground max-w-2xl mx-auto\">\n              Choose the right model for every job. All models run locally on your hardware —\n              download once, use forever.\n            </p>\n          </div>\n\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n            {/* Qwen3-TTS */}\n            <div className=\"rounded-xl border border-border bg-card/60 backdrop-blur-sm p-6 transition-colors hover:border-accent/30\">\n              <div className=\"flex items-start justify-between mb-3\">\n                <div>\n                  <h3 className=\"text-base font-semibold text-foreground\">Qwen3-TTS</h3>\n                  <span className=\"text-xs text-muted-foreground/60\">by Alibaba</span>\n                </div>\n                <div className=\"flex gap-1.5\">\n                  <span className=\"text-[10px] px-2 py-0.5 rounded-full border border-border bg-background text-muted-foreground\">\n                    1.7B\n                  </span>\n                  <span className=\"text-[10px] px-2 py-0.5 rounded-full border border-border bg-background text-muted-foreground\">\n                    0.6B\n                  </span>\n                </div>\n              </div>\n              <p className=\"text-sm text-muted-foreground leading-relaxed mb-4\">\n                High-quality multilingual voice cloning with natural prosody. The only engine with\n                delivery instructions — control tone, pace, and emotion with natural language.\n              </p>\n              <div className=\"flex flex-wrap gap-2\">\n                <span className=\"flex items-center gap-1 text-[11px] text-muted-foreground/70\">\n                  <Globe className=\"h-3 w-3\" />\n                  10 languages\n                </span>\n                <span className=\"flex items-center gap-1 text-[11px] text-muted-foreground/70\">\n                  <MessageSquare className=\"h-3 w-3\" />\n                  Delivery instructions\n                </span>\n              </div>\n            </div>\n\n            {/* Chatterbox */}\n            <div className=\"rounded-xl border border-border bg-card/60 backdrop-blur-sm p-6 transition-colors hover:border-accent/30\">\n              <div className=\"flex items-start justify-between mb-3\">\n                <div>\n                  <h3 className=\"text-base font-semibold text-foreground\">Chatterbox</h3>\n                  <span className=\"text-xs text-muted-foreground/60\">by Resemble AI</span>\n                </div>\n              </div>\n              <p className=\"text-sm text-muted-foreground leading-relaxed mb-4\">\n                Production-grade voice cloning with the broadest language support. 23 languages with\n                zero-shot cloning and emotion exaggeration control.\n              </p>\n              <div className=\"flex flex-wrap gap-2\">\n                <span className=\"flex items-center gap-1 text-[11px] text-muted-foreground/70\">\n                  <Languages className=\"h-3 w-3\" />\n                  23 languages\n                </span>\n              </div>\n            </div>\n\n            {/* Chatterbox Turbo */}\n            <div className=\"rounded-xl border border-border bg-card/60 backdrop-blur-sm p-6 transition-colors hover:border-accent/30\">\n              <div className=\"flex items-start justify-between mb-3\">\n                <div>\n                  <h3 className=\"text-base font-semibold text-foreground\">Chatterbox Turbo</h3>\n                  <span className=\"text-xs text-muted-foreground/60\">by Resemble AI</span>\n                </div>\n                <span className=\"text-[10px] px-2 py-0.5 rounded-full border border-border bg-background text-muted-foreground\">\n                  350M\n                </span>\n              </div>\n              <p className=\"text-sm text-muted-foreground leading-relaxed mb-4\">\n                Lightweight and fast. Supports paralinguistic tags — embed [laugh], [sigh], [gasp]\n                and more directly in your text for expressive, natural speech.\n              </p>\n              <div className=\"flex flex-wrap gap-2\">\n                <span className=\"flex items-center gap-1 text-[11px] text-muted-foreground/70\">\n                  <Zap className=\"h-3 w-3\" />\n                  350M params\n                </span>\n                <span className=\"flex items-center gap-1 text-[11px] text-muted-foreground/70\">\n                  <MessageSquare className=\"h-3 w-3\" />\n                  [laugh] [sigh] tags\n                </span>\n              </div>\n            </div>\n\n            {/* LuxTTS */}\n            <div className=\"rounded-xl border border-border bg-card/60 backdrop-blur-sm p-6 transition-colors hover:border-accent/30\">\n              <div className=\"flex items-start justify-between mb-3\">\n                <div>\n                  <h3 className=\"text-base font-semibold text-foreground\">LuxTTS</h3>\n                  <span className=\"text-xs text-muted-foreground/60\">by ZipVoice</span>\n                </div>\n              </div>\n              <p className=\"text-sm text-muted-foreground leading-relaxed mb-4\">\n                Ultra-fast, CPU-friendly voice cloning at 48kHz. Exceeds 150x realtime on CPU with\n                ~1GB VRAM. The fastest engine for quick iterations.\n              </p>\n              <div className=\"flex flex-wrap gap-2\">\n                <span className=\"flex items-center gap-1 text-[11px] text-muted-foreground/70\">\n                  <Zap className=\"h-3 w-3\" />\n                  150x realtime\n                </span>\n                <span className=\"flex items-center gap-1 text-[11px] text-muted-foreground/70\">\n                  48kHz output\n                </span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </section>\n\n      {/* ── Download Section ─────────────────────────────────────── */}\n      <section id=\"download\" className=\"border-t border-border py-24\">\n        <div className=\"mx-auto max-w-4xl px-6\">\n          <div className=\"text-center mb-12\">\n            <h2 className=\"text-3xl font-semibold tracking-tight text-foreground md:text-4xl mb-4\">\n              Download Voicebox\n            </h2>\n            <p className=\"text-muted-foreground\">\n              Available for macOS, Windows, and Linux. No dependencies required.\n            </p>\n          </div>\n\n          <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-2xl mx-auto\">\n            {/* macOS ARM */}\n            <a\n              href={downloadLinks.macArm}\n              download\n              className=\"flex items-center rounded-xl border border-border bg-card/60 backdrop-blur-sm px-5 py-4 transition-all hover:border-accent/30 hover:bg-card group\"\n            >\n              <AppleIcon className=\"h-6 w-6 shrink-0 text-muted-foreground group-hover:text-foreground transition-colors\" />\n              <div className=\"ml-4\">\n                <div className=\"text-sm font-medium\">macOS</div>\n                <div className=\"text-xs text-muted-foreground\">Apple Silicon (ARM)</div>\n              </div>\n            </a>\n\n            {/* macOS Intel */}\n            <a\n              href={downloadLinks.macIntel}\n              download\n              className=\"flex items-center rounded-xl border border-border bg-card/60 backdrop-blur-sm px-5 py-4 transition-all hover:border-accent/30 hover:bg-card group\"\n            >\n              <AppleIcon className=\"h-6 w-6 shrink-0 text-muted-foreground group-hover:text-foreground transition-colors\" />\n              <div className=\"ml-4\">\n                <div className=\"text-sm font-medium\">macOS</div>\n                <div className=\"text-xs text-muted-foreground\">Intel (x64)</div>\n              </div>\n            </a>\n\n            {/* Windows */}\n            <a\n              href={downloadLinks.windows}\n              download\n              className=\"flex items-center rounded-xl border border-border bg-card/60 backdrop-blur-sm px-5 py-4 transition-all hover:border-accent/30 hover:bg-card group\"\n            >\n              <WindowsIcon className=\"h-6 w-6 shrink-0 text-muted-foreground group-hover:text-foreground transition-colors\" />\n              <div className=\"ml-4\">\n                <div className=\"text-sm font-medium\">Windows</div>\n                <div className=\"text-xs text-muted-foreground\">64-bit (MSI)</div>\n              </div>\n            </a>\n\n            {/* Linux */}\n            <a\n              href=\"/linux-install\"\n              className=\"flex items-center rounded-xl border border-border bg-card/60 backdrop-blur-sm px-5 py-4 transition-all hover:border-accent/30 hover:bg-card group\"\n            >\n              <LinuxIcon className=\"h-6 w-6 shrink-0 text-muted-foreground group-hover:text-foreground transition-colors\" />\n              <div className=\"ml-4\">\n                <div className=\"text-sm font-medium\">Linux</div>\n                <div className=\"text-xs text-muted-foreground\">Build from source</div>\n              </div>\n            </a>\n          </div>\n\n          {/* GitHub link */}\n          <div className=\"mt-6 text-center\">\n            <a\n              href={`${GITHUB_REPO}/releases`}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n            >\n              <Github className=\"h-4 w-4\" />\n              View all releases on GitHub\n            </a>\n          </div>\n        </div>\n      </section>\n\n      {/* ── Footer ───────────────────────────────────────────────── */}\n      <Footer />\n    </>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/Banner.tsx",
    "content": "import { ArrowRight } from 'lucide-react';\n\nexport function Banner() {\n  return (\n    <div className=\"bg-primary/[0.06] border-b border-border backdrop-blur-sm\">\n      <div className=\"container mx-auto px-4\">\n        <div className=\"flex items-center justify-center h-10 text-sm\">\n          <a\n            href=\"https://spacebot.sh\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors group\"\n          >\n            <span>\n              Also by the creator of Voicebox:{' '}\n              <strong className=\"text-foreground/90\">Spacebot</strong>, an AI agent OS for teams.\n              Connect Discord, Slack, or Telegram in one click.\n            </span>\n            <ArrowRight className=\"h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5\" />\n          </a>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/ControlUI.tsx",
    "content": "'use client';\n\nimport { motion } from 'framer-motion';\nimport {\n  AudioLines,\n  Box,\n  Download,\n  Mic,\n  MoreHorizontal,\n  Pencil,\n  Server,\n  Sparkles,\n  Speaker,\n  Star,\n  Trash2,\n  Volume2,\n  Wand2,\n} from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { LandingAudioPlayer, unlockAudioContext } from './LandingAudioPlayer';\n\n// ─── Data ───────────────────────────────────────────────────────────────────\n// Edit this section to customise all the content shown in the ControlUI demo.\n\ninterface VoiceProfile {\n  name: string;\n  description: string;\n  language: string;\n  hasEffects: boolean;\n}\n\n/** Voice profiles shown in the grid / scroll strip. Index matters — DemoScript references profiles by index. */\nconst PROFILES: VoiceProfile[] = [\n  {\n    name: 'Jarvis',\n    description: 'Dry wit, composed British AI assistant',\n    language: 'en',\n    hasEffects: true,\n  },\n  {\n    name: 'Samuel L. Jackson',\n    description: 'Commanding intensity with sharp, punchy delivery',\n    language: 'en',\n    hasEffects: true,\n  },\n  {\n    name: 'Bob Ross',\n    description: 'Gentle, soothing voice full of quiet encouragement',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'Sam Altman',\n    description: 'Measured, thoughtful Silicon Valley cadence',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'Morgan Freeman',\n    description: 'Rich, warm baritone with gravitas and calm authority',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'Linus Tech Tips',\n    description: 'Enthusiastic, fast-paced tech explainer energy',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'Fireship',\n    description: 'Rapid-fire, deadpan tech humor with zero filler',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'Scarlett Johansson',\n    description: 'Smooth, low alto with understated warmth',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'Dario Amodei',\n    description: 'Calm, precise articulation with academic depth',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'David Attenborough',\n    description: 'Warm, reverent narration with wonder and precision',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'Zendaya',\n    description: 'Relaxed, modern delivery with effortless cool',\n    language: 'en',\n    hasEffects: false,\n  },\n  {\n    name: 'Barack Obama',\n    description: 'Measured cadence with rhythmic pauses and gravitas',\n    language: 'en',\n    hasEffects: false,\n  },\n];\n\n/** Each entry is one cycle of the demo animation: select a profile → type text → generate → play audio. */\ninterface DemoStep {\n  profileIndex: number;\n  text: string;\n  audioUrl: string;\n  engine: string;\n  duration: string;\n  effect?: string;\n}\n\nconst DEMO_SCRIPT: DemoStep[] = [\n  {\n    profileIndex: 0,\n    text: 'Sir, I have completed the analysis. Your code has twelve critical vulnerabilities, your coffee is cold, and frankly your commit messages could use some work.',\n    audioUrl: '/audio/jarvis.webm',\n    engine: 'Qwen 1.7B',\n    duration: '0:10',\n    effect: 'Robot',\n  },\n  {\n    profileIndex: 4,\n    text: \"I've narrated penguins, galaxies, and the entire history of mankind. But nothing prepared me for the moment a computer learned to do my job from a five second audio clip.\",\n    audioUrl: '/audio/morganfreeman.webm',\n    engine: 'Qwen 1.7B',\n    duration: '0:11',\n    effect: 'Radio',\n  },\n  {\n    profileIndex: 3,\n    text: \"Open source? [laugh] What's that?\",\n    audioUrl: '/audio/samaltman.webm',\n    engine: 'Chatterbox',\n    duration: '0:03',\n  },\n  {\n    profileIndex: 1,\n    text: \"So let me get this straight. You downloaded an app, pressed a button, and now there's two of me? The world was not ready for one\",\n    audioUrl: '/audio/samjackson.webm',\n    engine: 'Qwen 1.7B',\n    duration: '0:10',\n  },\n  {\n    profileIndex: 5,\n    text: \"So we got this voice cloning software and honestly it's kind of terrifying. Like, my wife could not tell the difference. Voicebox dot s h, link in the description!\",\n    audioUrl: '/audio/linus.webm',\n    engine: 'Qwen 1.7B',\n    duration: '0:11',\n  },\n  {\n    profileIndex: 6,\n    text: 'This is Voicebox in one hundred seconds. It clones voices locally, it runs on your GPU, and no, OpenAI cannot hear you. Lets go.',\n    audioUrl: '/audio/fireship.webm',\n    engine: 'Qwen 0.6B',\n    duration: '0:09',\n  },\n];\n\n/** History rows pre-populated on first load. Oldest first visually (array index 0 = top row). */\ninterface Generation {\n  id: number;\n  profileName: string;\n  text: string;\n  language: string;\n  engine: string;\n  duration: string;\n  timeAgo: string;\n  favorited: boolean;\n  versions: number;\n}\n\nconst INITIAL_GENERATIONS: Generation[] = [\n  {\n    id: 1,\n    profileName: 'Morgan Freeman',\n    text: 'The neural pathways of human speech contain more complexity than any language model can fully capture, yet we keep pushing the boundaries of what is possible.',\n    language: 'en',\n    engine: 'Qwen 1.7B',\n    duration: '0:08',\n    timeAgo: '2 minutes ago',\n    favorited: true,\n    versions: 3,\n  },\n  {\n    id: 2,\n    profileName: 'Samuel L. Jackson',\n    text: 'In a world increasingly shaped by artificial intelligence, the human voice remains our most powerful tool for connection and storytelling.',\n    language: 'en',\n    engine: 'Qwen 1.7B',\n    duration: '0:07',\n    timeAgo: '15 minutes ago',\n    favorited: false,\n    versions: 1,\n  },\n  {\n    id: 3,\n    profileName: 'Jarvis',\n    text: 'The architecture of modern text-to-speech systems reveals an elegant interplay between transformer models and acoustic feature prediction.',\n    language: 'en',\n    engine: 'Qwen 0.6B',\n    duration: '0:09',\n    timeAgo: '1 hour ago',\n    favorited: false,\n    versions: 2,\n  },\n  {\n    id: 4,\n    profileName: 'Bob Ross',\n    text: 'Welcome to the next chapter. Every great story begins with a single voice, and today that voice can be yours.',\n    language: 'en',\n    engine: 'Chatterbox',\n    duration: '0:06',\n    timeAgo: '3 hours ago',\n    favorited: true,\n    versions: 1,\n  },\n  {\n    id: 5,\n    profileName: 'Linus Tech Tips',\n    text: 'Local inference gives you complete control over your voice data. No cloud, no subscriptions, no compromises.',\n    language: 'en',\n    engine: 'Qwen 1.7B',\n    duration: '0:05',\n    timeAgo: '5 hours ago',\n    favorited: false,\n    versions: 1,\n  },\n];\n\nconst SIDEBAR_ITEMS = [\n  { icon: Volume2, label: 'Generate' },\n  { icon: AudioLines, label: 'Stories' },\n  { icon: Mic, label: 'Voices' },\n  { icon: Wand2, label: 'Effects' },\n  { icon: Speaker, label: 'Audio' },\n  { icon: Box, label: 'Models' },\n  { icon: Server, label: 'Server' },\n];\n\n// ─── Phase system ───────────────────────────────────────────────────────────\n\ntype Phase = 'idle' | 'selecting' | 'typing' | 'generating' | 'complete' | 'playing';\n\nconst PHASE_DURATIONS: Record<Phase, number> = {\n  idle: 2500,\n  selecting: 800,\n  typing: 6000,\n  generating: 2800,\n  complete: 1200,\n  playing: 4000,\n};\n\n// ─── Typewriter ─────────────────────────────────────────────────────────────\n\nfunction TypewriterText({ text, speed }: { text: string; speed?: number }) {\n  // Default: fill the typing phase duration, leaving 500ms buffer at the end\n  const resolvedSpeed =\n    speed ?? Math.max(20, Math.floor((PHASE_DURATIONS.typing - 500) / text.length));\n  const [displayed, setDisplayed] = useState('');\n  const indexRef = useRef(0);\n\n  useEffect(() => {\n    indexRef.current = 0;\n    setDisplayed('');\n    const interval = setInterval(() => {\n      indexRef.current += 1;\n      if (indexRef.current <= text.length) {\n        setDisplayed(text.slice(0, indexRef.current));\n      } else {\n        clearInterval(interval);\n      }\n    }, resolvedSpeed);\n    return () => clearInterval(interval);\n  }, [text, resolvedSpeed]);\n\n  return (\n    <>\n      {displayed}\n      <span className=\"inline-block h-3.5 w-[2px] animate-pulse bg-foreground/70 ml-[1px] align-middle\" />\n    </>\n  );\n}\n\n// ─── Loading bars (simplified react-loaders replacement) ────────────────────\n\nfunction LoadingBars({ mode }: { mode: 'idle' | 'generating' | 'playing' }) {\n  const barColor = mode !== 'idle' ? 'bg-accent' : 'bg-muted-foreground/40';\n  return (\n    <div className=\"flex items-center gap-[2px] h-5\">\n      {[0, 1, 2, 3, 4].map((i) => (\n        <motion.div\n          key={`${i}-${mode}`}\n          className={`w-[3px] rounded-full ${barColor}`}\n          animate={\n            mode === 'generating'\n              ? { height: ['6px', '16px', '6px'] }\n              : mode === 'playing'\n                ? { height: ['8px', '14px', '4px', '12px', '8px'] }\n                : { height: '8px' }\n          }\n          transition={\n            mode === 'generating'\n              ? { duration: 0.6, repeat: Infinity, delay: i * 0.08, ease: 'easeInOut' }\n              : mode === 'playing'\n                ? { duration: 1.2, repeat: Infinity, delay: i * 0.15, ease: 'easeInOut' }\n                : {}\n          }\n        />\n      ))}\n    </div>\n  );\n}\n\n// ─── Profile Card ───────────────────────────────────────────────────────────\n\nconst ProfileCard = ({\n  profile,\n  selected,\n  selecting,\n  cardRef,\n}: {\n  profile: VoiceProfile;\n  selected: boolean;\n  selecting: boolean;\n  cardRef?: React.Ref<HTMLDivElement>;\n}) => {\n  return (\n    <motion.div\n      ref={cardRef}\n      className={`rounded-xl border-2 bg-card p-3.5 flex flex-col h-[143px] transition-all duration-200 ${\n        selected ? 'border-accent shadow-md' : 'border-border/50 hover:shadow-sm'\n      } ${selecting && !selected ? 'opacity-60' : ''}`}\n      animate={selecting && selected ? { scale: [1, 1.02, 1] } : {}}\n      transition={{ duration: 0.3 }}\n    >\n      <div className=\"text-[15px] font-bold leading-tight line-clamp-2\">{profile.name}</div>\n      <div className=\"text-[10px] text-muted-foreground line-clamp-2 leading-relaxed mt-1\">\n        {profile.description}\n      </div>\n      <div className=\"flex items-center gap-1.5 mt-2\">\n        <span className=\"text-[10px] px-1.5 py-0.5 rounded-md border border-border text-muted-foreground\">\n          {profile.language}\n        </span>\n        {profile.hasEffects && <Sparkles className=\"h-3 w-3 text-accent fill-accent\" />}\n      </div>\n      <div className=\"flex items-center gap-1 mt-auto justify-end\">\n        <Download className=\"h-3.5 w-3.5 text-muted-foreground/40\" />\n        <Pencil className=\"h-3.5 w-3.5 text-muted-foreground/40\" />\n        <Trash2 className=\"h-3.5 w-3.5 text-muted-foreground/40\" />\n      </div>\n    </motion.div>\n  );\n};\n\n// ─── History Row ────────────────────────────────────────────────────────────\n\nfunction HistoryRow({\n  gen,\n  mode,\n  isNew,\n}: {\n  gen: Generation;\n  mode: 'idle' | 'generating' | 'playing';\n  isNew: boolean;\n}) {\n  return (\n    <motion.div\n      className={`border rounded-md transition-colors text-left w-full ${\n        mode === 'playing' ? 'bg-muted/70' : 'bg-card'\n      }`}\n      initial={isNew ? { opacity: 0, y: -8 } : false}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.3, ease: 'easeOut' }}\n    >\n      <div className=\"flex items-stretch gap-3 h-[80px] p-2.5\">\n        {/* Status icon */}\n        <div className=\"w-8 flex items-center justify-center shrink-0\">\n          <LoadingBars mode={mode} />\n        </div>\n\n        {/* Meta info */}\n        <div className=\"flex flex-col gap-1 w-36 shrink-0 justify-center\">\n          <div className=\"text-[12px] font-medium truncate\">{gen.profileName}</div>\n          <div className=\"flex items-center gap-2 text-[10px] text-muted-foreground\">\n            <span>{gen.language}</span>\n            <span>{gen.engine}</span>\n            {mode !== 'generating' && <span>{gen.duration}</span>}\n          </div>\n          <div className=\"text-[10px] text-muted-foreground\">\n            {mode === 'generating' ? (\n              <span className=\"text-accent\">Generating...</span>\n            ) : (\n              gen.timeAgo\n            )}\n          </div>\n        </div>\n\n        {/* Transcript */}\n        <div className=\"flex-1 min-w-0 flex items-center\">\n          <div className=\"text-[11px] text-muted-foreground line-clamp-3 leading-relaxed\">\n            {gen.text}\n          </div>\n        </div>\n\n        {/* Action buttons */}\n        <div className=\"flex flex-col justify-center items-center gap-0.5 shrink-0\">\n          <button className=\"h-5 w-5 flex items-center justify-center rounded-sm hover:bg-muted\">\n            <Star\n              className={`h-2.5 w-2.5 ${\n                gen.favorited ? 'text-accent fill-accent' : 'text-muted-foreground/50'\n              }`}\n            />\n          </button>\n          {gen.versions > 1 && (\n            <button className=\"h-5 w-5 flex items-center justify-center rounded-sm hover:bg-muted\">\n              <AudioLines className=\"h-2.5 w-2.5 text-muted-foreground/50\" />\n            </button>\n          )}\n          <button className=\"h-5 w-5 flex items-center justify-center rounded-sm hover:bg-muted\">\n            <MoreHorizontal className=\"h-2.5 w-2.5 text-muted-foreground/50\" />\n          </button>\n        </div>\n      </div>\n    </motion.div>\n  );\n}\n\n// ─── Floating Generate Box ──────────────────────────────────────────────────\n\nfunction FloatingGenerateBox({\n  phase,\n  typingText,\n  selectedProfile,\n  engine,\n  effect,\n}: {\n  phase: Phase;\n  typingText: string;\n  selectedProfile: VoiceProfile | null;\n  engine: string;\n  effect?: string;\n}) {\n  const isFocused = phase === 'typing' || phase === 'generating';\n  const isGenerating = phase === 'generating';\n\n  return (\n    <motion.div\n      className=\"bg-background/30 backdrop-blur-2xl border border-accent/20 rounded-[1.5rem] shadow-2xl p-2.5\"\n      animate={{\n        borderColor: isGenerating\n          ? 'hsl(43 50% 45% / 0.35)'\n          : isFocused\n            ? 'hsl(43 50% 45% / 0.25)'\n            : 'hsl(43 50% 45% / 0.15)',\n      }}\n      transition={{ duration: 0.3 }}\n    >\n      {/* Text area + generate button */}\n      <div className=\"flex items-start gap-2\">\n        <div className=\"flex-1 min-w-0\">\n          <motion.div\n            className=\"overflow-hidden\"\n            animate={{ height: isFocused ? 100 : 32 }}\n            transition={{ duration: 0.25, ease: 'easeOut' }}\n          >\n            <div\n              className=\"text-[12.5px] text-muted-foreground/60 px-2 py-1 leading-relaxed\"\n              style={{ minHeight: isFocused ? 100 : 32 }}\n            >\n              {phase === 'typing' ? (\n                <span className=\"text-foreground\">\n                  <TypewriterText text={typingText} />\n                </span>\n              ) : phase === 'generating' ? (\n                <span className=\"text-muted-foreground/40\">{typingText}</span>\n              ) : (\n                <span>\n                  {selectedProfile\n                    ? `Generate speech using ${selectedProfile.name}...`\n                    : 'Select a voice profile above...'}\n                </span>\n              )}\n            </div>\n          </motion.div>\n        </div>\n\n        {/* Generate button */}\n        <button className=\"h-8 w-8 rounded-full bg-accent flex items-center justify-center shrink-0 shadow-lg\">\n          <Sparkles className=\"h-3.5 w-3.5 text-white fill-white\" />\n        </button>\n      </div>\n\n      {/* Bottom selectors */}\n      <div className=\"flex items-center gap-1.5 mt-2\">\n        <span className=\"text-[10px] px-2 py-1 rounded-full border border-border bg-card text-muted-foreground\">\n          English\n        </span>\n        <span className=\"text-[10px] px-2 py-1 rounded-full border border-border bg-card text-muted-foreground\">\n          {engine}\n        </span>\n        <span\n          className={`text-[10px] px-2 py-1 rounded-full border flex items-center gap-1 ${\n            effect\n              ? 'border-accent/30 bg-accent/10 text-accent'\n              : 'border-border bg-card text-muted-foreground'\n          }`}\n        >\n          <Sparkles className={`h-2.5 w-2.5 ${effect ? 'fill-accent' : ''}`} />\n          {effect || 'Effect'}\n        </span>\n      </div>\n    </motion.div>\n  );\n}\n\n// ─── Main ControlUI ─────────────────────────────────────────────────────────\n\nexport function ControlUI() {\n  const [phase, setPhase] = useState<Phase>('idle');\n  const [selectedIndex, setSelectedIndex] = useState(DEMO_SCRIPT[0].profileIndex);\n  const [cycle, setCycle] = useState(0);\n  const [newGenId, setNewGenId] = useState<number | null>(null);\n  const [generations, setGenerations] = useState<Generation[]>([...INITIAL_GENERATIONS]);\n  const [isMuted, setIsMuted] = useState(true);\n  const [isVisible, setIsVisible] = useState(true);\n  const [pageHidden, setPageHidden] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const phaseRef = useRef(phase);\n  const mobileCardRefs = useRef<Map<number, HTMLDivElement>>(new Map());\n  const desktopCardRefs = useRef<Map<number, HTMLDivElement>>(new Map());\n  const profileGridRef = useRef<HTMLDivElement>(null);\n  const [scrollLeft, setScrollLeft] = useState(0);\n  phaseRef.current = phase;\n\n  const step = DEMO_SCRIPT[cycle % DEMO_SCRIPT.length];\n  const selectedProfile = PROFILES[selectedIndex];\n\n  // Scroll to selected profile card — accounts for generate box overlay on desktop\n  useEffect(() => {\n    const isMobile = window.innerWidth < 768;\n\n    if (isMobile) {\n      const el = mobileCardRefs.current.get(selectedIndex);\n      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });\n      return;\n    }\n\n    // Desktop\n    const el = desktopCardRefs.current.get(selectedIndex);\n    const scrollContainer = profileGridRef.current;\n    if (!el || !scrollContainer) return;\n\n    const containerTop = scrollContainer.getBoundingClientRect().top;\n    const elTop = el.getBoundingClientRect().top;\n    const elRelTop = elTop - containerTop + scrollContainer.scrollTop;\n\n    const rowHeight = 145;\n    const generateBoxHeight = 200;\n    const visibleTop = scrollContainer.scrollTop;\n    const visibleBottom = visibleTop + scrollContainer.clientHeight - generateBoxHeight;\n    const elRelBottom = elRelTop + el.offsetHeight;\n\n    if (elRelTop >= visibleTop && elRelBottom <= visibleBottom) {\n      return;\n    }\n\n    const target = elRelTop - rowHeight;\n    scrollContainer.scrollTo({ top: Math.max(0, target), behavior: 'smooth' });\n  }, [selectedIndex]);\n\n  // Visibility detection\n  useEffect(() => {\n    const observer = new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting), {\n      threshold: 0,\n    });\n    if (containerRef.current) observer.observe(containerRef.current);\n\n    const handleVisibility = () => setPageHidden(document.visibilityState !== 'visible');\n    document.addEventListener('visibilitychange', handleVisibility);\n\n    return () => {\n      observer.disconnect();\n      document.removeEventListener('visibilitychange', handleVisibility);\n    };\n  }, []);\n\n  const paused = !isVisible || pageHidden;\n\n  // Phase cycling — `playing` phase is driven by audio finish, not a timeout\n  useEffect(() => {\n    if (paused || phase === 'playing') return;\n\n    const duration = PHASE_DURATIONS[phase];\n    const timer = setTimeout(() => {\n      console.log(\n        '[ControlUI] phase transition',\n        phase,\n        '→ next, cycle:',\n        cycle,\n        'step profile:',\n        PROFILES[step.profileIndex].name,\n      );\n      switch (phase) {\n        case 'idle': {\n          setSelectedIndex(step.profileIndex);\n          setPhase('selecting');\n          break;\n        }\n        case 'selecting':\n          setPhase('typing');\n          break;\n        case 'typing': {\n          const profile = PROFILES[step.profileIndex];\n          const newGen: Generation = {\n            id: Date.now(),\n            profileName: profile.name,\n            text: step.text,\n            language: profile.language,\n            engine: step.engine,\n            duration: step.duration,\n            timeAgo: 'just now',\n            favorited: false,\n            versions: 1,\n          };\n          setGenerations((prev) => [newGen, ...prev.slice(0, 5)]);\n          setNewGenId(newGen.id);\n          setPhase('generating');\n          break;\n        }\n        case 'generating':\n          setPhase('playing');\n          break;\n      }\n    }, duration);\n\n    return () => clearTimeout(timer);\n  }, [phase, paused, step, cycle]);\n\n  const handleAudioFinish = useCallback(() => {\n    if (phaseRef.current !== 'playing') return;\n    setPhase('idle');\n    setCycle((c) => c + 1);\n    setNewGenId(null);\n  }, []);\n\n  const isGenerating = phase === 'generating';\n\n  return (\n    <div ref={containerRef} className=\"relative z-20 mx-auto w-full max-w-6xl px-6\">\n      {/* Unmute button with handwritten hint */}\n      <div className=\"flex justify-end mb-3\">\n        <div className=\"relative\">\n          {/* Handwritten hint — absolutely positioned above the button */}\n          {isMuted && (\n            <motion.div\n              className=\"absolute select-none pointer-events-none\"\n              style={{ top: -30, right: 100 }}\n              initial={{ opacity: 0, y: 6 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{ delay: 2, duration: 0.6, ease: 'easeOut' }}\n            >\n              <span\n                className=\"text-xl text-accent/80 whitespace-nowrap\"\n                style={{\n                  fontFamily: \"'Caveat', 'Segoe Script', 'Comic Sans MS', cursive\",\n                  letterSpacing: '0.02em',\n                }}\n              >\n                try me!\n              </span>\n              {/* Curved arrow from text down-right toward the button */}\n              <svg\n                width=\"22\"\n                height=\"11\"\n                viewBox=\"0 0 80 40\"\n                fill=\"none\"\n                className=\"text-accent/70 absolute\"\n                style={{ top: 14, left: 60 }}\n                aria-hidden=\"true\"\n              >\n                <title>Arrow</title>\n                <path\n                  d=\"M4 4 C20 4, 40 8, 55 20 C62 26, 66 32, 70 36\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"3\"\n                  strokeLinecap=\"round\"\n                  fill=\"none\"\n                />\n                <path\n                  d=\"M58 42 L70 36 L64 22\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"3\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  transform=\"rotate(35, 70, 36)\"\n                  fill=\"none\"\n                />\n              </svg>\n            </motion.div>\n          )}\n          <button\n            onClick={() => {\n              unlockAudioContext();\n              setIsMuted(!isMuted);\n            }}\n            className=\"flex items-center gap-2 px-3 py-1.5 rounded-full border border-border bg-card/50 backdrop-blur text-xs text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            {isMuted ? (\n              <>\n                <Volume2 className=\"h-3.5 w-3.5\" />\n                <span>Unmute</span>\n              </>\n            ) : (\n              <>\n                <Volume2 className=\"h-3.5 w-3.5 text-accent\" />\n                <span>Mute</span>\n              </>\n            )}\n          </button>\n        </div>\n      </div>\n\n      <div className=\"overflow-hidden rounded-2xl border border-app-line bg-app-box shadow-[0_25px_60px_rgba(0,0,0,0.5),0_8px_20px_rgba(0,0,0,0.3)] md:h-[640px] pointer-events-none select-none\">\n        <div className=\"flex flex-col md:flex-row h-full\">\n          {/* ── Sidebar (hidden on mobile) ─────────────────────────── */}\n          <div className=\"hidden md:flex w-16 shrink-0 border-r border-app-line bg-sidebar flex-col items-center py-4 gap-4\">\n            {/* Logo */}\n            <div className=\"mb-1\">\n              <div\n                className=\"w-9 h-9 rounded-lg overflow-hidden\"\n                style={{\n                  filter:\n                    'drop-shadow(0 0 6px hsl(43 50% 45% / 0.5)) drop-shadow(0 0 14px hsl(43 50% 45% / 0.35))',\n                }}\n              >\n                <img\n                  src=\"/voicebox-logo-app.webp\"\n                  alt=\"\"\n                  className=\"w-full h-full object-contain\"\n                />\n              </div>\n            </div>\n\n            {/* Nav items */}\n            <div className=\"flex flex-col gap-2\">\n              {SIDEBAR_ITEMS.map((item, i) => {\n                const Icon = item.icon;\n                const active = i === 0;\n                return (\n                  <div\n                    key={item.label}\n                    className={`w-9 h-9 rounded-full flex items-center justify-center transition-all duration-200 ${\n                      active\n                        ? 'bg-white/[0.07] text-foreground shadow-lg backdrop-blur-sm border border-white/[0.08]'\n                        : 'text-muted-foreground/60'\n                    }`}\n                  >\n                    <Icon className=\"h-4 w-4\" />\n                  </div>\n                );\n              })}\n            </div>\n\n            {/* Version */}\n            <div className=\"mt-auto text-[8px] text-muted-foreground/40\">v0.2.0</div>\n          </div>\n\n          {/* ── Main content ──────────────────────────────────────── */}\n          <div className=\"flex-1 flex flex-col md:flex-row min-w-0 relative\">\n            {/* Left: Profiles + Generate box */}\n            <div className=\"flex flex-col min-w-0 relative md:flex-1 md:overflow-hidden\">\n              {/* Gradient fade overlay — sits between header and scroll content */}\n              <div className=\"hidden md:block absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-app-box to-transparent z-[1] pointer-events-none\" />\n\n              {/* Header — floats above everything */}\n              <div className=\"absolute top-0 left-0 right-0 z-10 px-4 pt-4 md:pt-6 pb-2 flex items-center justify-between\">\n                <h2 className=\"text-base font-bold\">Voicebox</h2>\n                <div className=\"flex items-center gap-1.5\">\n                  <button className=\"h-6 text-[10px] px-2.5 rounded-full border border-border bg-card text-muted-foreground flex items-center gap-1\">\n                    Import Voice\n                  </button>\n                  <button className=\"h-6 text-[10px] px-2.5 rounded-full bg-accent text-accent-foreground flex items-center\">\n                    Create Voice\n                  </button>\n                </div>\n              </div>\n\n              {/* Scrollable profile cards — scrolls behind header + gradient */}\n              <div\n                ref={profileGridRef}\n                className=\"flex-1 min-h-0 md:overflow-y-auto md:pt-14 pt-12\"\n              >\n                <div className=\"px-4\">\n                  {/* Mobile: horizontal scroll strip with edge fade */}\n                  <div className=\"relative md:hidden\">\n                    {scrollLeft > 0 && (\n                      <div className=\"absolute left-0 top-0 bottom-0 w-6 bg-gradient-to-r from-app-box to-transparent z-10\" />\n                    )}\n                    <div className=\"absolute right-0 top-0 bottom-0 w-6 bg-gradient-to-l from-app-box to-transparent z-10\" />\n                    <div\n                      className=\"flex gap-2 overflow-x-auto pb-2\"\n                      onScroll={(e) => setScrollLeft(e.currentTarget.scrollLeft)}\n                    >\n                      {PROFILES.map((profile, i) => (\n                        <div\n                          key={profile.name}\n                          className=\"shrink-0 w-[140px]\"\n                          ref={(el) => {\n                            if (el) mobileCardRefs.current.set(i, el);\n                          }}\n                        >\n                          <ProfileCard\n                            profile={profile}\n                            selected={i === selectedIndex}\n                            selecting={phase === 'selecting'}\n                          />\n                        </div>\n                      ))}\n                    </div>\n                  </div>\n\n                  {/* Desktop: 3-col grid */}\n                  <div className=\"hidden md:grid grid-cols-3 gap-2 mt-1 pb-44\">\n                    {PROFILES.map((profile, i) => (\n                      <ProfileCard\n                        key={profile.name}\n                        profile={profile}\n                        selected={i === selectedIndex}\n                        selecting={phase === 'selecting'}\n                        cardRef={(el: HTMLDivElement | null) => {\n                          if (el) desktopCardRefs.current.set(i, el);\n                        }}\n                      />\n                    ))}\n                  </div>\n                </div>\n              </div>\n\n              {/* Floating generate box — desktop: absolute overlay, mobile: inline */}\n              <div className=\"px-3 pt-2 pb-3 md:pt-0 md:absolute md:left-4 md:right-4 md:bottom-[117px] md:z-20 md:pb-0 md:px-0\">\n                <FloatingGenerateBox\n                  phase={phase}\n                  typingText={step.text}\n                  selectedProfile={selectedProfile}\n                  engine={step.engine}\n                  effect={step.effect}\n                />\n              </div>\n            </div>\n\n            {/* Right/Below: History */}\n            <div className=\"md:w-[48%] shrink-0 flex flex-col min-w-0 border-t md:border-t-0 border-app-line\">\n              <div className=\"max-h-[360px] md:max-h-none flex-1 overflow-hidden px-3 pt-3 md:pt-6 pb-3\">\n                <div className=\"flex flex-col gap-2\">\n                  {generations.map((gen) => {\n                    const isThisNew = gen.id === newGenId;\n                    const rowMode: 'idle' | 'generating' | 'playing' =\n                      isThisNew && isGenerating\n                        ? 'generating'\n                        : isThisNew && phase === 'playing'\n                          ? 'playing'\n                          : 'idle';\n                    return <HistoryRow key={gen.id} gen={gen} mode={rowMode} isNew={isThisNew} />;\n                  })}\n                </div>\n              </div>\n            </div>\n\n            {/* Audio player */}\n            <LandingAudioPlayer\n              audioUrl={step.audioUrl}\n              title={selectedProfile.name}\n              playing={phase === 'playing'}\n              muted={isMuted}\n              onFinish={handleAudioFinish}\n              onClose={() => {}}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/DownloadSection.tsx",
    "content": "'use client';\n\nimport { Download, Laptop, Monitor, Terminal } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent } from '@/components/ui/card';\nimport { DOWNLOAD_LINKS, LATEST_VERSION } from '@/lib/constants';\n\nexport function DownloadSection() {\n  const downloads = [\n    {\n      platform: 'Mac',\n      icon: Laptop,\n      link: DOWNLOAD_LINKS.macArm,\n      description: 'macOS (Intel + Apple Silicon)',\n      disabled: false,\n    },\n    {\n      platform: 'Windows',\n      icon: Monitor,\n      link: DOWNLOAD_LINKS.windows,\n      description: 'Windows x64',\n      disabled: false,\n    },\n    {\n      platform: 'Linux',\n      icon: Terminal,\n      link: DOWNLOAD_LINKS.linux,\n      description: 'Linux AppImage',\n      disabled: true,\n    },\n  ];\n\n  return (\n    <div className=\"space-y-6\">\n      <div className=\"text-center\">\n        <p className=\"text-sm text-muted-foreground mb-2\">Latest Version</p>\n        <p className=\"text-2xl font-bold\">{LATEST_VERSION}</p>\n      </div>\n      <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-6\">\n        {downloads.map(({ platform, icon: Icon, link, description, disabled }) => (\n          <Card\n            key={platform}\n            className={`transition-all duration-200 ${\n              disabled\n                ? 'opacity-50'\n                : 'hover:border-primary/20 hover:shadow-lg hover:shadow-primary/3 hover:-translate-y-0.5'\n            }`}\n          >\n            <CardContent className=\"p-6\">\n              <div className=\"flex flex-col items-center text-center space-y-4\">\n                <div className=\"p-3 rounded-xl bg-muted/50 backdrop-blur-sm border border-border\">\n                  <Icon className=\"h-8 w-8\" />\n                </div>\n                <div>\n                  <h3 className=\"text-lg font-semibold mb-1\">{platform}</h3>\n                  <p className=\"text-sm text-muted-foreground\">{description}</p>\n                </div>\n                <Button asChild size=\"lg\" className=\"w-full\" disabled={disabled}>\n                  <a href={link} download className={disabled ? 'pointer-events-none' : ''}>\n                    <Download className=\"h-4 w-4 mr-2\" />\n                    Download\n                  </a>\n                </Button>\n              </div>\n            </CardContent>\n          </Card>\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/Features.tsx",
    "content": "'use client';\n\nimport { motion } from 'framer-motion';\nimport { AudioLines, Cloud, MessageSquareText, Mic, Sparkles, TextCursorInput } from 'lucide-react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\n// ─── Lazy load wrapper ──────────────────────────────────────────────────────\n\nfunction LazyLoad({\n  children,\n  className,\n  rootMargin = '200px',\n}: {\n  children: React.ReactNode;\n  className?: string;\n  rootMargin?: string;\n}) {\n  const ref = useRef<HTMLDivElement>(null);\n  const [visible, setVisible] = useState(false);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) {\n          setVisible(true);\n          observer.disconnect();\n        }\n      },\n      { rootMargin },\n    );\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [rootMargin]);\n\n  return (\n    <div ref={ref} className={className}>\n      {visible ? children : null}\n    </div>\n  );\n}\n\n// ─── Animation: Voice Cloning ───────────────────────────────────────────────\n\nfunction VoiceCloningAnimation() {\n  const [phase, setPhase] = useState(0);\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setPhase((p) => (p + 1) % 3);\n    }, 2400);\n    return () => clearInterval(interval);\n  }, []);\n\n  const samples = ['Sample 1', 'Sample 2', 'Sample 3'];\n  const bars = [0.4, 0.7, 0.5, 0.9, 0.3, 0.6, 0.8, 0.4, 0.7, 0.5, 0.3, 0.6];\n\n  return (\n    <div className=\"h-40 w-full flex items-center justify-center overflow-hidden rounded-md bg-app-darkerBox/50 p-4\">\n      <div className=\"flex flex-col items-center gap-3 w-full max-w-[200px]\">\n        {/* Sample pills */}\n        <div className=\"flex gap-1.5\">\n          {samples.map((s, i) => (\n            <motion.div\n              key={s}\n              className=\"text-[9px] px-2 py-1 rounded-full border font-medium\"\n              animate={{\n                borderColor: i === phase ? 'hsl(43 50% 45% / 0.5)' : 'rgba(255,255,255,0.06)',\n                backgroundColor: i === phase ? 'hsl(43 50% 45% / 0.08)' : 'rgba(255,255,255,0.02)',\n                color: i === phase ? 'hsl(43 50% 45%)' : 'rgba(255,255,255,0.4)',\n              }}\n              transition={{ duration: 0.3 }}\n            >\n              {s}\n            </motion.div>\n          ))}\n        </div>\n\n        {/* Waveform visualization */}\n        <div className=\"flex items-center gap-[2px] h-10 w-full justify-center\">\n          {bars.map((h, i) => (\n            <motion.div\n              key={i}\n              className=\"w-[4px] rounded-full\"\n              animate={{\n                height: `${h * 100}%`,\n                backgroundColor: phase === 2 ? 'hsl(43 50% 45%)' : 'rgba(255,255,255,0.15)',\n              }}\n              transition={{\n                height: { duration: 0.6, delay: i * 0.04, ease: 'easeInOut' },\n                backgroundColor: { duration: 0.3 },\n              }}\n            />\n          ))}\n        </div>\n\n        {/* Result label */}\n        <motion.div\n          className=\"text-[9px] font-mono\"\n          animate={{\n            opacity: phase === 2 ? 1 : 0.3,\n            color: phase === 2 ? 'hsl(43 50% 45%)' : 'rgba(255,255,255,0.3)',\n          }}\n          transition={{ duration: 0.3 }}\n        >\n          voice profile ready\n        </motion.div>\n      </div>\n    </div>\n  );\n}\n\n// ─── Mini waveform for clips ────────────────────────────────────────────────\n// Fixed-width dense waveform that overflows — the clip container clips it.\n// This way resizing a clip just reveals/hides bars instead of re-rendering.\n\nconst WAVEFORM_BAR_COUNT = 60;\n\nfunction MiniWaveform({ seed, color }: { seed: number; color: string }) {\n  // Deterministic pseudo-random waveform that looks like real speech audio.\n  // Uses layered noise at different frequencies for natural envelope + detail.\n  const bars = useMemo(() => {\n    // Seeded pseudo-random number generator (deterministic per seed)\n    let s = seed * 9301 + 49297;\n    const rand = () => {\n      s = (s * 16807 + 0) % 2147483647;\n      return s / 2147483647;\n    };\n\n    // Pre-generate random values\n    const r = Array.from({ length: WAVEFORM_BAR_COUNT }, () => rand());\n\n    return Array.from({ length: WAVEFORM_BAR_COUNT }, (_, i) => {\n      const t = i / WAVEFORM_BAR_COUNT;\n\n      // Slow envelope — broad amplitude shape (words / phrases)\n      const envelope =\n        0.3 +\n        0.35 *\n          Math.sin(t * Math.PI * (2 + (seed % 3))) *\n          Math.sin(t * Math.PI * (1.3 + seed * 0.7)) +\n        0.2 * Math.sin(t * Math.PI * (4.7 + seed * 1.3));\n\n      // Medium variation — syllable-level bumps\n      const mid = 0.15 * Math.sin(i * 0.8 + seed * 3.1) * Math.cos(i * 1.3 + seed);\n\n      // High-frequency noise — individual sample jitter\n      const noise = (r[i] - 0.5) * 0.25;\n\n      // Combine and clamp\n      const raw = envelope + mid + noise;\n      return Math.max(0.06, Math.min(1, raw));\n    });\n  }, [seed]);\n\n  return (\n    <div className=\"flex items-center h-full overflow-hidden\">\n      {bars.map((h, i) => (\n        <div\n          key={`w-${seed}-${i}`}\n          className=\"shrink-0 rounded-full opacity-50\"\n          style={{\n            width: 2,\n            marginRight: 1,\n            height: `${h * 100}%`,\n            backgroundColor: color,\n          }}\n        />\n      ))}\n    </div>\n  );\n}\n\n// ─── Animation: Stories Editor ───────────────────────────────────────────────\n\n// Clip shape: id, profile, track, left (px out of 220), width (px), waveform seed\ntype DemoClip = { id: string; profile: string; track: number; x: number; w: number; seed: number };\n\nconst INITIAL_CLIPS: DemoClip[] = [\n  { id: 'n1', profile: 'Morgan', track: 0, x: 4, w: 70, seed: 1 },\n  { id: 'n2', profile: 'Morgan', track: 0, x: 135, w: 35, seed: 2 },\n  { id: 'a1', profile: 'Scarlett', track: 1, x: 25, w: 40, seed: 3 },\n  { id: 'a2', profile: 'Scarlett', track: 1, x: 120, w: 35, seed: 4 },\n  { id: 'b1', profile: 'Jarvis', track: 2, x: 70, w: 45, seed: 5 },\n];\n\n// Timeline width the clips live inside\nconst TL_W = 220;\n// Each action returns a new clips array (or modifies in place)\ntype Action = { label: string; apply: (clips: DemoClip[]) => DemoClip[] };\n\nconst ACTIONS: Action[] = [\n  // 0 — move Jarvis clip earlier\n  { label: 'Move clip', apply: (c) => c.map((cl) => (cl.id === 'b1' ? { ...cl, x: 55 } : cl)) },\n  // 1 — split Morgan's first clip into two with visible gap\n  {\n    label: 'Split clip',\n    apply: (c) => {\n      // Idempotent: if n1b already exists, the split already happened\n      if (c.some((cl) => cl.id === 'n1b')) return c;\n      const clip = c.find((cl) => cl.id === 'n1');\n      if (!clip) return c;\n      const leftW = 25;\n      const gap = 8;\n      const rightW = clip.w - leftW - gap;\n      return [\n        ...c.filter((cl) => cl.id !== 'n1'),\n        { ...clip, w: leftW, id: 'n1' },\n        {\n          id: 'n1b',\n          profile: clip.profile,\n          track: clip.track,\n          x: clip.x + leftW + gap,\n          w: rightW,\n          seed: 6,\n        },\n      ];\n    },\n  },\n  // 2 — trim Scarlett's second clip shorter\n  { label: 'Trim clip', apply: (c) => c.map((cl) => (cl.id === 'a2' ? { ...cl, w: 25 } : cl)) },\n  // 3 — duplicate Jarvis to track 0\n  {\n    label: 'Duplicate',\n    apply: (c) => {\n      // Idempotent: if b1d already exists, the duplicate already happened\n      if (c.some((cl) => cl.id === 'b1d')) return c;\n      const clip = c.find((cl) => cl.id === 'b1');\n      if (!clip) return c;\n      return [...c, { ...clip, id: 'b1d', track: 0, x: 180, w: 35, seed: 7 }];\n    },\n  },\n  // 4 — reset\n  { label: '', apply: () => INITIAL_CLIPS },\n];\n\nfunction StoriesAnimation() {\n  const [clips, setClips] = useState<DemoClip[]>(INITIAL_CLIPS);\n  const [actionIndex, setActionIndex] = useState(-1);\n  const [playheadX, setPlayheadX] = useState(0);\n  const [selectedId, setSelectedId] = useState<string | null>(null);\n  const playheadRef = useRef<ReturnType<typeof requestAnimationFrame>>(0);\n\n  // Animate the playhead continuously\n  useEffect(() => {\n    let start: number | null = null;\n    const speed = 12; // px per second\n    const animate = (ts: number) => {\n      if (start === null) start = ts;\n      const elapsed = (ts - start) / 1000;\n      setPlayheadX((elapsed * speed) % TL_W);\n      playheadRef.current = requestAnimationFrame(animate);\n    };\n    playheadRef.current = requestAnimationFrame(animate);\n    return () => cancelAnimationFrame(playheadRef.current);\n  }, []);\n\n  // Step through actions\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setActionIndex((prev) => {\n        const next = (prev + 1) % ACTIONS.length;\n        setClips((current) => ACTIONS[next].apply(current));\n        // Highlight the clip being acted on\n        if (next === 0) setSelectedId('b1');\n        else if (next === 1) setSelectedId('n1');\n        else if (next === 2) setSelectedId('a2');\n        else if (next === 3) setSelectedId('b1');\n        else setSelectedId(null);\n        return next;\n      });\n    }, 2600);\n    return () => clearInterval(interval);\n  }, []);\n\n  const trackLabels = ['1', '0', '-1'];\n  const timeMarkers = [0, 2, 4, 6, 8];\n  const accentColor = 'hsl(43 50% 45%)';\n  const accentFg = 'hsl(30 10% 94%)';\n\n  return (\n    <div className=\"h-40 w-full flex flex-col overflow-hidden rounded-md bg-app-darkerBox/50\">\n      {/* Toolbar */}\n      <div className=\"flex items-center gap-1.5 px-2 py-1 border-b border-app-line bg-app-darkBox/60 shrink-0\">\n        <div className=\"w-1.5 h-1.5 rounded-full bg-ink-faint/40\" />\n        <div className=\"flex items-center gap-1\">\n          <div className=\"w-4 h-4 rounded flex items-center justify-center bg-app-button\">\n            <div className=\"border-l-[4px] border-l-ink-faint border-t-[3px] border-t-transparent border-b-[3px] border-b-transparent ml-0.5\" />\n          </div>\n          <div className=\"w-4 h-4 rounded flex items-center justify-center bg-app-button\">\n            <div className=\"w-2 h-2 rounded-sm bg-ink-faint/60\" />\n          </div>\n        </div>\n        <span className=\"text-[8px] text-ink-faint font-mono ml-1 tabular-nums\">0:03 / 0:10</span>\n        <div className=\"flex-1\" />\n        {actionIndex >= 0 && actionIndex < ACTIONS.length - 1 && (\n          <motion.span\n            key={actionIndex}\n            className=\"text-[7px] font-medium px-1.5 py-0.5 rounded-full\"\n            style={{\n              backgroundColor: `${accentColor.replace(')', ' / 0.15)')}`,\n              color: accentColor,\n            }}\n            initial={{ opacity: 0, y: 3 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.25 }}\n          >\n            {ACTIONS[actionIndex].label}\n          </motion.span>\n        )}\n        <div className=\"flex items-center gap-0.5\">\n          <span className=\"text-[7px] text-ink-faint\">Zoom</span>\n          <div className=\"w-3 h-3 rounded flex items-center justify-center bg-app-button text-[8px] text-ink-faint\">\n            -\n          </div>\n          <div className=\"w-3 h-3 rounded flex items-center justify-center bg-app-button text-[8px] text-ink-faint\">\n            +\n          </div>\n        </div>\n      </div>\n\n      {/* Timeline */}\n      <div className=\"flex flex-1 min-h-0\">\n        {/* Track labels sidebar */}\n        <div className=\"w-7 shrink-0 border-r border-app-line bg-app-darkBox/30 flex flex-col\">\n          <div className=\"h-5 border-b border-app-line\" />\n          {trackLabels.map((label) => (\n            <div\n              key={label}\n              className=\"flex-1 flex items-center justify-center border-b border-app-line\"\n            >\n              <span className=\"text-[7px] text-ink-faint select-none\">{label}</span>\n            </div>\n          ))}\n        </div>\n\n        {/* Tracks area */}\n        <div className=\"flex-1 relative overflow-hidden flex flex-col\">\n          {/* Time ruler */}\n          <div className=\"h-5 shrink-0 border-b border-app-line bg-app-darkBox/20 relative\">\n            {timeMarkers.map((t) => (\n              <div\n                key={`tm-${t}`}\n                className=\"absolute top-0 h-full flex flex-col justify-end pb-0.5\"\n                style={{ left: `${(t / 10) * 100}%` }}\n              >\n                <div className=\"h-1.5 w-px bg-app-line\" />\n                <span className=\"text-[7px] text-ink-faint ml-0.5 select-none\">{`0:0${t}`}</span>\n              </div>\n            ))}\n          </div>\n\n          {/* Track rows + clips — same parent so percentages match */}\n          <div className=\"flex-1 relative min-h-0\">\n            {/* Track rows background */}\n            {trackLabels.map((label, i) => (\n              <div\n                key={`bg-${label}`}\n                className=\"border-b border-app-line absolute left-0 right-0\"\n                style={{\n                  height: `${100 / 3}%`,\n                  top: `${(i * 100) / 3}%`,\n                  backgroundColor: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)',\n                }}\n              />\n            ))}\n\n            {/* Clips */}\n            {clips.map((clip) => {\n              const trackIdx = clip.track;\n              const isSelected = clip.id === selectedId;\n              const clipTop = `calc(${(trackIdx * 100) / 3}% + 2px)`;\n              const clipHeight = `calc(${100 / 3}% - 4px)`;\n              return (\n                <motion.div\n                  key={clip.id}\n                  className=\"absolute rounded overflow-hidden\"\n                  initial={false}\n                  style={{\n                    height: clipHeight,\n                    left: `${(clip.x / TL_W) * 100}%`,\n                    width: `${(clip.w / TL_W) * 100}%`,\n                    top: clipTop,\n                  }}\n                  animate={{\n                    left: `${(clip.x / TL_W) * 100}%`,\n                    width: `${(clip.w / TL_W) * 100}%`,\n                    top: clipTop,\n                  }}\n                  transition={{ type: 'spring', stiffness: 200, damping: 25 }}\n                >\n                  <div\n                    className=\"w-full h-full rounded overflow-hidden flex flex-col\"\n                    style={{\n                      backgroundColor: isSelected ? 'hsl(43 50% 45%)' : 'hsl(43 45% 40%)',\n                      boxShadow: isSelected\n                        ? 'inset 0 0 0 1px hsl(43 50% 55%), 0 0 0 1px hsl(30 10% 94% / 0.4)'\n                        : 'inset 0 0 0 1px hsl(30 10% 94% / 0.1)',\n                    }}\n                  >\n                    {/* Profile label — scaled to bypass browser min font size */}\n                    <div className=\"shrink-0 relative\" style={{ height: 9 }}>\n                      <span\n                        className=\"text-[10px] font-medium leading-none absolute top-0 left-0.5 origin-top-left opacity-80 whitespace-nowrap\"\n                        style={{ color: accentFg, transform: 'scale(0.75)' }}\n                      >\n                        {clip.profile}\n                      </span>\n                    </div>\n                    {/* Waveform — absolutely positioned so it never affects clip width */}\n                    <div className=\"absolute left-0 right-0 bottom-0\" style={{ top: 9 }}>\n                      <MiniWaveform seed={clip.seed} color={accentFg} />\n                    </div>\n                  </div>\n                  {/* Trim handles on selected */}\n                  {isSelected && (\n                    <>\n                      <div\n                        className=\"absolute left-0 top-0 bottom-0 w-1 rounded-l\"\n                        style={{ backgroundColor: 'hsl(30 10% 94% / 0.25)' }}\n                      />\n                      <div\n                        className=\"absolute right-0 top-0 bottom-0 w-1 rounded-r\"\n                        style={{ backgroundColor: 'hsl(30 10% 94% / 0.25)' }}\n                      />\n                    </>\n                  )}\n                </motion.div>\n              );\n            })}\n\n            {/* Playhead */}\n            <motion.div\n              className=\"absolute top-0 bottom-0 w-[2px] rounded-full z-20 pointer-events-none\"\n              style={{ backgroundColor: accentColor }}\n              animate={{ left: `${(playheadX / TL_W) * 100}%` }}\n              transition={{ duration: 0.05, ease: 'linear' }}\n            >\n              <div\n                className=\"absolute -top-0.5 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full\"\n                style={{ backgroundColor: accentColor }}\n              />\n            </motion.div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// ─── Animation: Effects Pipeline ────────────────────────────────────────────\n\nfunction EffectsAnimation() {\n  const [activeEffect, setActiveEffect] = useState(0);\n  const effects = [\n    { name: 'Pitch Shift', param: '-3 semitones', color: '#3b82f6' },\n    { name: 'Reverb', param: 'Room 0.7', color: '#8b5cf6' },\n    { name: 'Compressor', param: '-15 dB', color: '#ec4899' },\n    { name: 'Low-Pass', param: '6000 Hz', color: '#14b8a6' },\n  ];\n\n  // Waveform bars — original shape\n  const rawBars = [0.3, 0.6, 0.8, 0.5, 0.9, 0.4, 0.7, 0.3, 0.6, 0.5, 0.8, 0.4, 0.7, 0.9, 0.3];\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setActiveEffect((p) => (p + 1) % effects.length);\n    }, 2200);\n    return () => clearInterval(interval);\n  }, [effects.length]);\n\n  return (\n    <div className=\"h-40 w-full flex flex-col items-center justify-center overflow-hidden rounded-md bg-app-darkerBox/50 p-4 gap-3\">\n      {/* Effects chain */}\n      <div className=\"flex items-center gap-1\">\n        {effects.map((fx, i) => (\n          <div key={fx.name} className=\"flex items-center gap-1\">\n            <motion.div\n              className=\"text-[8px] px-2 py-0.5 rounded-full border font-medium\"\n              animate={{\n                borderColor: i <= activeEffect ? `${fx.color}60` : 'rgba(255,255,255,0.06)',\n                backgroundColor: i <= activeEffect ? `${fx.color}15` : 'rgba(255,255,255,0.02)',\n                color: i <= activeEffect ? fx.color : 'rgba(255,255,255,0.3)',\n              }}\n              transition={{ duration: 0.3 }}\n            >\n              {fx.name}\n            </motion.div>\n            {i < effects.length - 1 && (\n              <motion.span\n                className=\"text-[8px]\"\n                animate={{\n                  color: i < activeEffect ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.08)',\n                }}\n                transition={{ duration: 0.3 }}\n              >\n                &rarr;\n              </motion.span>\n            )}\n          </div>\n        ))}\n      </div>\n\n      {/* Waveform that morphs as effects are applied */}\n      <div className=\"flex items-center gap-[2px] h-10 w-full max-w-[200px] justify-center\">\n        {rawBars.map((h, i) => {\n          // Each effect stage progressively transforms the shape\n          const shifted = activeEffect >= 0 ? h * (0.7 + 0.3 * Math.sin(i * 0.8)) : h;\n          const dampened = activeEffect >= 1 ? shifted * (0.6 + 0.4 * Math.cos(i * 0.3)) : shifted;\n          const compressed = activeEffect >= 2 ? 0.3 + dampened * 0.5 : dampened;\n          const filtered = activeEffect >= 3 ? compressed * (1 - i * 0.03) : compressed;\n          const finalH = Math.max(0.08, Math.min(1, filtered));\n\n          return (\n            <motion.div\n              key={`bar-${i}`}\n              className=\"w-[3px] rounded-full\"\n              animate={{\n                height: `${finalH * 100}%`,\n                backgroundColor: effects[activeEffect].color,\n              }}\n              transition={{\n                height: { duration: 0.5, delay: i * 0.02, ease: 'easeInOut' },\n                backgroundColor: { duration: 0.4 },\n              }}\n            />\n          );\n        })}\n      </div>\n\n      {/* Active effect detail */}\n      <motion.div\n        className=\"text-[9px] font-mono text-ink-faint\"\n        key={activeEffect}\n        initial={{ opacity: 0, y: 4 }}\n        animate={{ opacity: 1, y: 0 }}\n        transition={{ duration: 0.3 }}\n      >\n        {effects[activeEffect].name}: {effects[activeEffect].param}\n      </motion.div>\n    </div>\n  );\n}\n\n// ─── Animation: Local or Remote ─────────────────────────────────────────────\n\nfunction LocalRemoteAnimation() {\n  const [mode, setMode] = useState(0);\n  const modes = ['Local GPU', 'Remote Server'];\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setMode((p) => (p + 1) % 2);\n    }, 2800);\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <div className=\"h-40 w-full flex items-center justify-center overflow-hidden rounded-md bg-app-darkerBox/50 p-4\">\n      <div className=\"flex flex-col items-center gap-4 w-full max-w-[180px]\">\n        {/* Toggle */}\n        <div className=\"flex gap-1 p-0.5 rounded-full border border-app-line bg-app-darkerBox\">\n          {modes.map((m, i) => (\n            <motion.div\n              key={m}\n              className=\"text-[9px] px-3 py-1 rounded-full font-medium\"\n              animate={{\n                backgroundColor: i === mode ? 'hsl(43 50% 45%)' : 'transparent',\n                color: i === mode ? 'hsl(30 10% 94%)' : 'rgba(255,255,255,0.35)',\n              }}\n              transition={{ duration: 0.25 }}\n            >\n              {m}\n            </motion.div>\n          ))}\n        </div>\n\n        {/* Status */}\n        <div className=\"flex flex-col items-center gap-2\">\n          <motion.div\n            className=\"w-2 h-2 rounded-full\"\n            animate={{\n              backgroundColor: mode === 0 ? '#4ade80' : '#3b82f6',\n              boxShadow: mode === 0 ? '0 0 8px #4ade80' : '0 0 8px #3b82f6',\n            }}\n            transition={{ duration: 0.3 }}\n          />\n          <span className=\"text-[9px] text-ink-faint font-mono\">\n            {mode === 0 ? 'Metal acceleration active' : 'Connected to 192.168.1.50'}\n          </span>\n          <span className=\"text-[8px] text-ink-faint/60 font-mono\">\n            {mode === 0 ? 'VRAM: 8.2 / 16.0 GB' : 'Latency: 12ms | CUDA'}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// ─── Animation: Transcription ───────────────────────────────────────────────\n\nfunction TranscriptionAnimation() {\n  const [charIndex, setCharIndex] = useState(0);\n  const text = 'The quick brown fox jumps over the lazy dog near the riverbank.';\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setCharIndex((p) => {\n        if (p >= text.length) return 0;\n        return p + 1;\n      });\n    }, 80);\n    return () => clearInterval(interval);\n  }, [text.length]);\n\n  return (\n    <div className=\"h-40 w-full flex flex-col items-center justify-center overflow-hidden rounded-md bg-app-darkerBox/50 p-4 gap-3\">\n      {/* Fake waveform */}\n      <div className=\"flex items-center gap-[1px] h-6 w-full max-w-[180px] justify-center\">\n        {Array.from({ length: 30 }, (_, i) => {\n          const h = 0.2 + 0.8 * Math.abs(Math.sin(i * 0.5 + charIndex * 0.1));\n          const active = i < (charIndex / text.length) * 30;\n          return (\n            <div\n              key={i}\n              className={`w-[3px] rounded-full transition-colors duration-100 ${\n                active ? 'bg-accent' : 'bg-app-line'\n              }`}\n              style={{ height: `${h * 100}%` }}\n            />\n          );\n        })}\n      </div>\n\n      {/* Transcribed text */}\n      <div className=\"text-[10px] text-ink-dull font-mono max-w-[200px] text-center leading-relaxed min-h-[32px]\">\n        {text.slice(0, charIndex)}\n        {charIndex < text.length && (\n          <span className=\"inline-block w-[2px] h-3 bg-accent animate-pulse ml-[1px] align-middle\" />\n        )}\n      </div>\n    </div>\n  );\n}\n\n// ─── Animation: Unlimited Length ─────────────────────────────────────────────\n\nfunction UnlimitedLengthAnimation() {\n  const [phase, setPhase] = useState(0);\n\n  const chunks = [\n    'The morning sun crept over the mountains, casting long shadows across the valley below.',\n    'Birds stirred in the canopy, their songs weaving through the cool air like threads of gold.',\n    'Far below, a river wound its way through ancient stones, carrying whispers of the night.',\n  ];\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setPhase((p) => (p + 1) % 4); // 0-2 = processing chunks, 3 = crossfade/done\n    }, 2000);\n    return () => clearInterval(interval);\n  }, []);\n\n  return (\n    <div className=\"h-40 w-full flex flex-col items-center justify-center overflow-hidden rounded-md bg-app-darkerBox/50 p-4 gap-2.5\">\n      {/* Chunk pills */}\n      <div className=\"flex flex-col gap-1 w-full max-w-[220px]\">\n        {chunks.map((chunk, i) => (\n          <motion.div\n            key={`chunk-${i}`}\n            className=\"flex items-center gap-1.5 px-2 py-1 rounded border text-[8px]\"\n            animate={{\n              borderColor:\n                phase === 3\n                  ? 'hsl(43 50% 45% / 0.3)'\n                  : i === phase\n                    ? 'hsl(43 50% 45% / 0.5)'\n                    : i < phase\n                      ? 'rgba(255,255,255,0.12)'\n                      : 'rgba(255,255,255,0.06)',\n              backgroundColor:\n                phase === 3\n                  ? 'hsl(43 50% 45% / 0.04)'\n                  : i === phase\n                    ? 'hsl(43 50% 45% / 0.08)'\n                    : i < phase\n                      ? 'rgba(255,255,255,0.04)'\n                      : 'rgba(255,255,255,0.02)',\n            }}\n            transition={{ duration: 0.4 }}\n          >\n            {/* Status indicator */}\n            <motion.div\n              className=\"w-1.5 h-1.5 rounded-full shrink-0\"\n              animate={{\n                backgroundColor:\n                  phase === 3\n                    ? 'hsl(43 50% 50%)'\n                    : i === phase\n                      ? 'hsl(43 50% 50%)'\n                      : i < phase\n                        ? 'rgba(255,255,255,0.3)'\n                        : 'rgba(255,255,255,0.1)',\n                boxShadow:\n                  i === phase && phase < 3 ? '0 0 6px hsl(43 50% 50%)' : '0 0 0px transparent',\n              }}\n              transition={{ duration: 0.3 }}\n            />\n            <span\n              className={`truncate font-mono ${\n                phase === 3 || i <= phase ? 'text-ink-dull' : 'text-ink-faint/50'\n              }`}\n            >\n              {chunk}\n            </span>\n          </motion.div>\n        ))}\n      </div>\n\n      {/* Crossfade / result bar */}\n      <div className=\"flex items-center gap-1 w-full max-w-[220px]\">\n        {chunks.map((_, i) => (\n          <motion.div\n            key={`seg-${i}`}\n            className=\"h-1.5 flex-1 rounded-full\"\n            animate={{\n              backgroundColor:\n                phase === 3\n                  ? 'hsl(43 50% 45%)'\n                  : i < phase\n                    ? 'rgba(255,255,255,0.2)'\n                    : i === phase\n                      ? 'hsl(43 50% 45% / 0.5)'\n                      : 'rgba(255,255,255,0.06)',\n            }}\n            transition={{ duration: 0.4 }}\n          />\n        ))}\n      </div>\n\n      {/* Status text */}\n      <motion.div\n        className=\"text-[9px] font-mono\"\n        key={phase}\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        transition={{ duration: 0.3 }}\n      >\n        <span className={phase === 3 ? 'text-accent' : 'text-ink-faint'}>\n          {phase < 3\n            ? `generating chunk ${phase + 1} of ${chunks.length}...`\n            : 'crossfaded & ready'}\n        </span>\n      </motion.div>\n    </div>\n  );\n}\n\n// ─── Feature data ───────────────────────────────────────────────────────────\n\nconst FEATURES = [\n  {\n    title: 'Near-Perfect Voice Cloning',\n    description:\n      'Multiple TTS engines for exceptional voice quality. Clone any voice from a few seconds of audio with natural intonation and emotion.',\n    icon: Mic,\n    animation: VoiceCloningAnimation,\n  },\n  {\n    title: 'Stories Editor',\n    description:\n      'Create multi-voice narratives with a timeline-based editor. Arrange tracks, trim clips, and mix conversations between characters.',\n    icon: AudioLines,\n    animation: StoriesAnimation,\n  },\n  {\n    title: 'Audio Effects Pipeline',\n    description:\n      'Apply pitch shift, reverb, delay, compression, and more — then save as presets. Preview effects live and set defaults per voice profile.',\n    icon: Sparkles,\n    animation: EffectsAnimation,\n  },\n  {\n    title: 'Local or Remote',\n    description:\n      'Run GPU inference locally with Metal, CUDA, ROCm, Intel Arc, or DirectML — or connect to a remote machine. One-click server setup with automatic discovery.',\n    icon: Cloud,\n    animation: LocalRemoteAnimation,\n  },\n  {\n    title: 'Audio Transcription',\n    description:\n      'Powered by Whisper for accurate speech-to-text. Automatically extract reference text from voice samples.',\n    icon: MessageSquareText,\n    animation: TranscriptionAnimation,\n  },\n  {\n    title: 'Unlimited Generation Length',\n    description:\n      'Generate up to 50,000 characters in one go. Text is auto-split at sentence boundaries, generated per-chunk, and crossfaded seamlessly.',\n    icon: TextCursorInput,\n    animation: UnlimitedLengthAnimation,\n  },\n];\n\n// ─── Feature Card ───────────────────────────────────────────────────────────\n\nfunction FeatureCard({ feature }: { feature: (typeof FEATURES)[number] }) {\n  const Icon = feature.icon;\n  const Animation = feature.animation;\n\n  return (\n    <div className=\"rounded-lg border border-app-line bg-app-darkBox overflow-hidden\">\n      <LazyLoad>\n        <div className=\"pointer-events-none select-none\">\n          <Animation />\n        </div>\n      </LazyLoad>\n      <div className=\"p-5\">\n        <div className=\"flex items-center gap-2 mb-2\">\n          <Icon className=\"h-4 w-4 text-accent\" />\n          <h3 className=\"text-[15px] font-medium text-foreground\">{feature.title}</h3>\n        </div>\n        <p className=\"text-sm leading-relaxed text-muted-foreground\">{feature.description}</p>\n      </div>\n    </div>\n  );\n}\n\n// ─── Features Section ───────────────────────────────────────────────────────\n\nexport function Features() {\n  return (\n    <section id=\"features\" className=\"border-t border-border py-24\">\n      <div className=\"mx-auto max-w-7xl px-6\">\n        <div className=\"mb-16 text-center\">\n          <h2 className=\"text-3xl font-semibold tracking-tight text-foreground md:text-4xl mb-4\">\n            Professional voice tools, zero compromise\n          </h2>\n          <p className=\"text-muted-foreground max-w-2xl mx-auto\">\n            Everything you need to clone voices, generate speech, and produce multi-voice content —\n            running entirely on your machine.\n          </p>\n        </div>\n        <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n          {FEATURES.map((feature) => (\n            <FeatureCard key={feature.title} feature={feature} />\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/Footer.tsx",
    "content": "import Image from 'next/image';\nimport Link from 'next/link';\nimport { GITHUB_REPO } from '@/lib/constants';\n\nexport function Footer() {\n  return (\n    <footer className=\"border-t border-border py-12\">\n      <div className=\"mx-auto max-w-7xl px-6\">\n        <div className=\"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 mb-10\">\n          {/* Brand */}\n          <div className=\"md:col-span-1\">\n            <div className=\"flex items-center gap-2.5 mb-4\">\n              <Image\n                src=\"/voicebox-logo-app.webp\"\n                alt=\"Voicebox\"\n                width={24}\n                height={24}\n                className=\"h-6 w-6\"\n              />\n              <span className=\"text-sm font-semibold\">Voicebox</span>\n            </div>\n            <p className=\"text-sm text-muted-foreground leading-relaxed\">\n              Open source voice cloning studio. Local-first, free forever.\n            </p>\n          </div>\n\n          {/* Product */}\n          <div>\n            <h4 className=\"text-sm font-semibold mb-3\">Product</h4>\n            <ul className=\"space-y-2 text-sm text-muted-foreground\">\n              <li>\n                <a href=\"#features\" className=\"hover:text-foreground transition-colors\">\n                  Features\n                </a>\n              </li>\n              <li>\n                <a href=\"#download\" className=\"hover:text-foreground transition-colors\">\n                  Download\n                </a>\n              </li>\n              <li>\n                <a href=\"#about\" className=\"hover:text-foreground transition-colors\">\n                  About\n                </a>\n              </li>\n            </ul>\n          </div>\n\n          {/* Resources */}\n          <div>\n            <h4 className=\"text-sm font-semibold mb-3\">Resources</h4>\n            <ul className=\"space-y-2 text-sm text-muted-foreground\">\n              <li>\n                <Link\n                  href=\"https://docs.voicebox.sh\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"hover:text-foreground transition-colors\"\n                >\n                  Documentation\n                </Link>\n              </li>\n              <li>\n                <Link\n                  href={GITHUB_REPO}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"hover:text-foreground transition-colors\"\n                >\n                  Source Code\n                </Link>\n              </li>\n              <li>\n                <Link\n                  href={`${GITHUB_REPO}/releases`}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"hover:text-foreground transition-colors\"\n                >\n                  Releases\n                </Link>\n              </li>\n              <li>\n                <Link\n                  href={`${GITHUB_REPO}/issues`}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"hover:text-foreground transition-colors\"\n                >\n                  Issues\n                </Link>\n              </li>\n            </ul>\n          </div>\n\n          {/* Also by */}\n          <div>\n            <h4 className=\"text-sm font-semibold mb-3\">Also By</h4>\n            <ul className=\"space-y-2 text-sm text-muted-foreground\">\n              <li>\n                <a\n                  href=\"https://spacebot.sh\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"hover:text-foreground transition-colors\"\n                >\n                  Spacebot\n                </a>\n              </li>\n              <li>\n                <a\n                  href=\"https://spacedrive.com\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"hover:text-foreground transition-colors\"\n                >\n                  Spacedrive\n                </a>\n              </li>\n            </ul>\n          </div>\n        </div>\n\n        <div className=\"border-t border-border pt-6\">\n          <p className=\"text-center text-sm text-muted-foreground\">\n            &copy; {new Date().getFullYear()} Voicebox. Open source under MIT license.\n          </p>\n        </div>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/Header.tsx",
    "content": "'use client';\n\nimport { Github } from 'lucide-react';\nimport Link from 'next/link';\nimport { Button } from '@/components/ui/button';\nimport { GITHUB_REPO } from '@/lib/constants';\n\nexport function Header() {\n  return (\n    <header className=\"border-b border-border bg-background/60 backdrop-blur-2xl sticky top-0 z-50 supports-[backdrop-filter]:bg-background/40\">\n      <div className=\"container mx-auto px-4\">\n        <div className=\"flex items-center justify-between h-16\">\n          {/* Logo */}\n          <Link\n            href=\"/\"\n            className=\"flex items-center gap-2 font-bold text-xl sm:text-2xl hover:opacity-80 transition-opacity tracking-tight\"\n          >\n            Voicebox\n          </Link>\n\n          {/* Actions */}\n          <div className=\"flex items-center gap-2 sm:gap-4\">\n            <Button variant=\"outline\" size=\"sm\" asChild className=\"hidden sm:flex\">\n              <a href={GITHUB_REPO} target=\"_blank\" rel=\"noopener noreferrer\">\n                <Github className=\"h-4 w-4 mr-2\" />\n                GitHub\n              </a>\n            </Button>\n            <Button variant=\"ghost\" size=\"icon\" asChild className=\"sm:hidden\">\n              <a href={GITHUB_REPO} target=\"_blank\" rel=\"noopener noreferrer\" aria-label=\"GitHub\">\n                <Github className=\"h-5 w-5\" />\n              </a>\n            </Button>\n          </div>\n        </div>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/LandingAudioPlayer.tsx",
    "content": "'use client';\n\nimport { Pause, Play, Repeat, Volume2, VolumeX } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport WaveSurfer from 'wavesurfer.js';\n\nfunction formatDuration(seconds: number): string {\n  const m = Math.floor(seconds / 60);\n  const s = Math.floor(seconds % 60);\n  return `${m}:${s.toString().padStart(2, '0')}`;\n}\n\n// Shared ref so the unmute button can unlock WaveSurfer's audio on iOS Safari\n// Must call .play() on WaveSurfer's actual media element during a user gesture\nlet sharedWaveSurfer: WaveSurfer | null = null;\nlet audioUnlocked = false;\n\nexport function unlockAudioContext() {\n  if (audioUnlocked) return;\n  audioUnlocked = true;\n\n  // Unlock WaveSurfer's internal audio element\n  // Skip if already playing — the context is already unlocked and the\n  // play/pause/reset dance would destroy the active playback.\n  if (sharedWaveSurfer && !sharedWaveSurfer.isPlaying()) {\n    const media = sharedWaveSurfer.getMediaElement();\n    if (media) {\n      media.muted = true;\n      media\n        .play()\n        .then(() => {\n          media.pause();\n          media.muted = false;\n          media.currentTime = 0;\n        })\n        .catch(() => {});\n    }\n  }\n\n  // Also unlock a standalone AudioContext as fallback\n  try {\n    const ctx = new (\n      window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n    )();\n    const buffer = ctx.createBuffer(1, 1, 22050);\n    const source = ctx.createBufferSource();\n    source.buffer = buffer;\n    source.connect(ctx.destination);\n    source.start(0);\n  } catch {\n    // Silently fail\n  }\n}\n\ninterface LandingAudioPlayerProps {\n  audioUrl: string;\n  title: string;\n  playing: boolean;\n  muted: boolean;\n  onFinish: () => void;\n  onClose: () => void;\n}\n\nexport function LandingAudioPlayer({\n  audioUrl,\n  title,\n  playing,\n  muted,\n  onFinish,\n  onClose,\n}: LandingAudioPlayerProps) {\n  const waveformRef = useRef<HTMLDivElement>(null);\n  const wavesurferRef = useRef<WaveSurfer | null>(null);\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [currentTime, setCurrentTime] = useState(0);\n  const [duration, setDuration] = useState(0);\n  const [volume, setVolume] = useState(0.75);\n  const [isLooping, setIsLooping] = useState(false);\n  const [isReady, setIsReady] = useState(false);\n  const onFinishRef = useRef(onFinish);\n  onFinishRef.current = onFinish;\n  const playingRef = useRef(playing);\n  playingRef.current = playing;\n  const mutedRef = useRef(muted);\n  mutedRef.current = muted;\n\n  // Initialize WaveSurfer\n  useEffect(() => {\n    const initWaveSurfer = () => {\n      const container = waveformRef.current;\n      if (!container) {\n        setTimeout(initWaveSurfer, 50);\n        return;\n      }\n\n      const rect = container.getBoundingClientRect();\n      if (rect.width === 0 || rect.height === 0) {\n        setTimeout(initWaveSurfer, 50);\n        return;\n      }\n\n      // Clean up existing instance\n      if (wavesurferRef.current) {\n        wavesurferRef.current.destroy();\n        wavesurferRef.current = null;\n      }\n\n      const root = document.documentElement;\n      const getCSSVar = (varName: string) => {\n        const value = getComputedStyle(root).getPropertyValue(varName).trim();\n        return value ? `hsl(${value})` : '';\n      };\n\n      const ws = WaveSurfer.create({\n        container,\n        waveColor: getCSSVar('--muted'),\n        progressColor: getCSSVar('--accent'),\n        cursorColor: getCSSVar('--accent'),\n        barWidth: 2,\n        barRadius: 2,\n        height: 80,\n        normalize: true,\n        interact: true,\n        mediaControls: false,\n      });\n\n      ws.on('ready', () => {\n        setDuration(ws.getDuration());\n        ws.setVolume(mutedRef.current ? 0 : volume);\n        setIsReady(true);\n      });\n\n      ws.on('play', () => {\n        console.log('[Player] play event');\n        setIsPlaying(true);\n      });\n      ws.on('pause', () => {\n        console.log('[Player] pause event');\n        setIsPlaying(false);\n      });\n\n      ws.on('timeupdate', (time: number) => {\n        setCurrentTime(Math.min(time, ws.getDuration()));\n      });\n\n      let didFinish = false;\n      ws.on('finish', () => {\n        if (didFinish) return;\n        didFinish = true;\n        console.log(\n          '[Player] finish event, currentTime:',\n          ws.getCurrentTime(),\n          'duration:',\n          ws.getDuration(),\n        );\n        setIsPlaying(false);\n        onFinishRef.current();\n      });\n\n      ws.load(audioUrl);\n      wavesurferRef.current = ws;\n      sharedWaveSurfer = ws;\n    };\n\n    setIsReady(false);\n    setCurrentTime(0);\n    setDuration(0);\n\n    requestAnimationFrame(() => {\n      requestAnimationFrame(() => {\n        setTimeout(initWaveSurfer, 10);\n      });\n    });\n\n    return () => {\n      if (wavesurferRef.current) {\n        wavesurferRef.current.destroy();\n        wavesurferRef.current = null;\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [audioUrl]);\n\n  // Respond to external play/stop signals\n  useEffect(() => {\n    const ws = wavesurferRef.current;\n    console.log('[Player] effect', { playing, isReady, hasWs: !!ws });\n    if (!ws || !isReady) return;\n\n    if (playing) {\n      // Resume the AudioContext first (required for iOS Safari after unlock)\n      const backend = ws.getMediaElement();\n      if (backend && 'context' in backend) {\n        const ctx = (backend as unknown as { context: AudioContext }).context;\n        if (ctx?.state === 'suspended') ctx.resume();\n      }\n      ws.play()\n        .then(() => {\n          console.log('[Player] play succeeded');\n        })\n        .catch((e: Error) => {\n          if (e.name === 'NotAllowedError') {\n            console.warn('[Player] Autoplay blocked by browser — waiting for user gesture');\n          } else {\n            console.error('[Player] play failed', e);\n          }\n        });\n    } else {\n      ws.pause();\n    }\n  }, [playing, isReady]);\n\n  // Sync volume and muted state\n  useEffect(() => {\n    if (wavesurferRef.current) {\n      wavesurferRef.current.setVolume(muted ? 0 : volume);\n    }\n  }, [volume, muted]);\n\n  const handlePlayPause = useCallback(() => {\n    if (!wavesurferRef.current) return;\n    wavesurferRef.current.playPause();\n  }, []);\n\n  return (\n    <div className=\"absolute bottom-0 left-0 right-0 border-t border-border bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 z-30\">\n      <div className=\"px-4 py-3 flex flex-col md:flex-row md:items-center gap-2 md:gap-4\">\n        {/* Waveform — full width row on mobile, inline on desktop */}\n        <div className=\"min-w-0 min-h-[60px] md:min-h-[80px] md:flex-1 md:order-2\">\n          <div ref={waveformRef} className=\"w-full h-full min-h-[60px] md:min-h-[80px]\" />\n        </div>\n\n        {/* Controls row */}\n        <div className=\"flex items-center gap-3 md:contents\">\n          {/* Play/Pause */}\n          <button\n            onClick={handlePlayPause}\n            disabled={!isReady}\n            className=\"h-10 w-10 rounded-full bg-accent flex items-center justify-center shrink-0 disabled:opacity-50 md:order-1 shadow-lg\"\n          >\n            {isPlaying ? (\n              <Pause className=\"h-5 w-5 text-accent-foreground fill-accent-foreground\" />\n            ) : (\n              <Play className=\"h-5 w-5 ml-0.5 text-accent-foreground fill-accent-foreground\" />\n            )}\n          </button>\n\n          {/* Time */}\n          <div className=\"flex items-center gap-1 text-sm text-muted-foreground shrink-0 md:order-3\">\n            <span className=\"font-mono text-xs\">{formatDuration(currentTime)}</span>\n            <span className=\"text-xs\">/</span>\n            <span className=\"font-mono text-xs\">{formatDuration(duration)}</span>\n          </div>\n\n          {/* Title */}\n          {title && (\n            <div className=\"text-sm font-medium truncate max-w-[200px] shrink-0 hidden lg:block md:order-4\">\n              {title}\n            </div>\n          )}\n\n          {/* Loop */}\n          <button\n            onClick={() => setIsLooping(!isLooping)}\n            className={`h-8 w-8 flex items-center justify-center rounded-sm shrink-0 hover:bg-muted md:order-5 ${\n              isLooping ? 'text-foreground' : 'text-muted-foreground'\n            }`}\n          >\n            <Repeat className=\"h-4 w-4\" />\n          </button>\n\n          {/* Volume */}\n          <div className=\"flex items-center gap-2 shrink-0 w-[140px] md:order-6 mr-3\">\n            <button\n              onClick={() => setVolume(volume > 0 ? 0 : 0.75)}\n              className=\"h-8 w-8 flex items-center justify-center hover:bg-muted rounded-sm\"\n            >\n              {volume > 0 ? (\n                <Volume2 className=\"h-4 w-4 text-muted-foreground\" />\n              ) : (\n                <VolumeX className=\"h-4 w-4 text-muted-foreground\" />\n              )}\n            </button>\n            <input\n              type=\"range\"\n              min={0}\n              max={100}\n              value={volume * 100}\n              onChange={(e) => setVolume(Number(e.target.value) / 100)}\n              className=\"flex-1 h-1 appearance-none bg-muted rounded-full accent-foreground cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-foreground\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/Navbar.tsx",
    "content": "'use client';\n\nimport { Github } from 'lucide-react';\nimport Image from 'next/image';\nimport { useEffect, useState } from 'react';\nimport { GITHUB_REPO } from '@/lib/constants';\n\nfunction formatStarCount(count: number): string {\n  if (count >= 1000) {\n    const k = count / 1000;\n    return k % 1 === 0 ? `${k}k` : `${k.toFixed(1)}k`;\n  }\n  return count.toString();\n}\n\nexport function Navbar() {\n  const [starCount, setStarCount] = useState<number | null>(null);\n\n  useEffect(() => {\n    fetch('/api/stars')\n      .then((res) => {\n        if (!res.ok) throw new Error('Failed to fetch stars');\n        return res.json();\n      })\n      .then((data) => {\n        if (typeof data.count === 'number') setStarCount(data.count);\n      })\n      .catch((error) => {\n        console.error('Failed to fetch star count:', error);\n      });\n  }, []);\n\n  return (\n    <nav className=\"fixed inset-x-0 top-0 z-50 border-b border-border/50 bg-background/80 backdrop-blur-xl\">\n      <div className=\"mx-auto grid max-w-7xl grid-cols-3 items-center px-6 py-3\">\n        {/* Logo + wordmark */}\n        <a href=\"/\" className=\"flex items-center gap-2.5 justify-self-start\">\n          <Image\n            src=\"/voicebox-logo-app.webp\"\n            alt=\"Voicebox\"\n            width={28}\n            height={28}\n            className=\"h-7 w-7\"\n          />\n          <span className=\"text-[15px] font-semibold text-foreground\">Voicebox</span>\n        </a>\n\n        {/* Nav links - centered */}\n        <div className=\"hidden sm:flex items-center gap-1 justify-self-center\">\n          <a\n            href=\"#features\"\n            className=\"rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground\"\n          >\n            Features\n          </a>\n          <a\n            href=\"#about\"\n            className=\"rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground\"\n          >\n            About\n          </a>\n          <a\n            href=\"#download\"\n            className=\"rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground\"\n          >\n            Download\n          </a>\n          <a\n            href=\"https://docs.voicebox.sh\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground\"\n          >\n            Docs\n          </a>\n        </div>\n\n        {/* GitHub star button */}\n        <a\n          href={GITHUB_REPO}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"flex items-center gap-2 justify-self-end rounded-lg border border-border/60 bg-card/60 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground hover:border-border\"\n        >\n          <Github className=\"h-4 w-4\" />\n          <span className=\"text-[13px] font-medium\">Star</span>\n          {starCount !== null && (\n            <span className=\"border-l border-border/60 pl-2 text-[13px] font-semibold text-foreground\">\n              {formatStarCount(starCount)}\n            </span>\n          )}\n        </a>\n      </div>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/PlatformIcons.tsx",
    "content": "export function AppleIcon({ className }: { className?: string }) {\n  return (\n    <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\n      <path d=\"M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z\" />\n    </svg>\n  );\n}\n\nexport function WindowsIcon({ className }: { className?: string }) {\n  return (\n    <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\n      <path d=\"M3 12V6.75l6-1.32v6.48L3 12zm17-9v8.75l-10 .15V5.21L20 3zM3 13l6 .09v7.81l-6-1.15V13zm17 .25V22l-10-1.8v-7.15l10 .15z\" />\n    </svg>\n  );\n}\n\nexport function LinuxIcon({ className }: { className?: string }) {\n  return (\n    <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\n      <path d=\"M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.26-.195.69-.133 1.001.054.27.112.553.077.784-.12.794-.3 1.593-.3 2.406 0 .599.18 1.193.3 1.791.12.599.3 1.193.3 1.792 0 .812.18 1.611.3 2.405.035.23-.023.514-.077.783-.062.312-.127.742.133 1.002a.424.424 0 00.11.135c-.123.805.01 1.657.287 2.489.589 1.771 1.831 3.47 2.716 4.521.75 1.067 0.974 1.928 1.05 3.02.065 1.491-1.056 5.965 3.17 6.298.165.013.325.021.48.021.155 0 .315-.008.48-.021 4.226-.333 3.105-4.807 3.17-6.298.076-1.092.3-1.953 1.05-3.02.885-1.051 2.127-2.75 2.716-4.521.278-.832.41-1.684.287-2.489a.424.424 0 00.11-.135c.26-.26.195-.69.133-1.001-.054-.27-.112-.553-.077-.784.12-.794.3-1.593.3-2.406 0-.599-.18-1.193-.3-1.791-.12-.599-.3-1.193-.3-1.792 0-.812-.18-1.611-.3-2.405-.035-.23.023-.514.077-.783.062-.312.127-.742-.133-1.002a.424.424 0 00-.11-.135c.123-.805-.01-1.657-.287-2.489-.589-1.771-1.831-3.47-2.716-4.521-.75-1.067-.974-1.928-1.05-3.02-.065-1.491 1.056-5.965-3.17-6.298C12.819.008 12.659 0 12.504 0z\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/VoiceCreator.tsx",
    "content": "'use client';\n\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { Mic, Monitor, Upload } from 'lucide-react';\nimport { useEffect, useMemo, useState } from 'react';\n\n// ─── Waveform bars generator ────────────────────────────────────────────────\n\nfunction generateWaveformBars(count: number, seed: number): number[] {\n  const bars: number[] = [];\n  for (let i = 0; i < count; i++) {\n    const x = i / count;\n    // Speech-like envelope: ramp up, sustain, taper\n    const envelope = Math.sin(x * Math.PI) * 0.8 + 0.2;\n    // Layered pseudo-random noise\n    const n1 = Math.sin(seed * 127.1 + i * 43.7) * 0.5 + 0.5;\n    const n2 = Math.sin(seed * 269.5 + i * 17.3) * 0.3 + 0.5;\n    const n3 = Math.sin(seed * 53.9 + i * 97.1) * 0.2 + 0.5;\n    const noise = (n1 + n2 + n3) / 3;\n    bars.push(envelope * noise);\n  }\n  return bars;\n}\n\n// ─── Animated waveform background ───────────────────────────────────────────\n\nfunction WaveformBackground({ active }: { active: boolean }) {\n  const bars = useMemo(() => generateWaveformBars(60, 42), []);\n\n  return (\n    <div className=\"absolute inset-0 pointer-events-none flex items-end justify-center overflow-hidden\">\n      <div className=\"flex items-end gap-[2px] w-full h-full px-4 pb-4\">\n        {bars.map((h, i) => {\n          const maxH = 120; // max bar height in px\n          const baseH = 4;\n          const activeH = baseH + h * maxH;\n          const idleH = baseH + h * maxH * 0.25;\n          return (\n            <motion.div\n              key={i}\n              className=\"flex-1 rounded-full bg-accent\"\n              animate={{\n                opacity: active ? 0.35 : 0.1,\n                height: active ? [idleH, activeH, idleH * 1.5, activeH * 0.7, idleH] : idleH,\n              }}\n              transition={\n                active\n                  ? {\n                      duration: 1.0 + (i % 5) * 0.12,\n                      repeat: Infinity,\n                      repeatType: 'mirror',\n                      delay: (i % 7) * 0.04,\n                      ease: 'easeInOut',\n                    }\n                  : { duration: 0.6 }\n              }\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\n// ─── Tab content panels ─────────────────────────────────────────────────────\n\nfunction UploadPanel() {\n  const [hasFile, setHasFile] = useState(false);\n\n  useEffect(() => {\n    // Simulate file drop after 2s\n    const t1 = setTimeout(() => setHasFile(true), 2000);\n    const t2 = setTimeout(() => setHasFile(false), 5000);\n    return () => {\n      clearTimeout(t1);\n      clearTimeout(t2);\n    };\n  }, []);\n\n  return (\n    <div\n      className={`relative flex flex-col items-center justify-center gap-3 p-6 border-2 rounded-lg min-h-[180px] transition-colors duration-300 ${\n        hasFile ? 'border-accent bg-accent/5' : 'border-dashed border-muted-foreground/25'\n      }`}\n    >\n      <AnimatePresence mode=\"wait\">\n        {!hasFile ? (\n          <motion.div\n            key=\"idle\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"flex flex-col items-center gap-3\"\n          >\n            <div className=\"h-10 px-5 rounded-md bg-accent text-accent-foreground flex items-center gap-2 text-sm font-medium\">\n              <Upload className=\"h-4 w-4\" />\n              Choose File\n            </div>\n            <p className=\"text-xs text-muted-foreground text-center\">\n              Drag and drop an audio file, or click to browse.\n              <br />\n              Maximum duration: 30 seconds.\n            </p>\n          </motion.div>\n        ) : (\n          <motion.div\n            key=\"file\"\n            initial={{ opacity: 0, scale: 0.95 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"flex flex-col items-center gap-3\"\n          >\n            <div className=\"flex items-center gap-2\">\n              <Upload className=\"h-4 w-4 text-accent\" />\n              <span className=\"text-sm font-medium\">sample-voice-clip.wav</span>\n            </div>\n            <div className=\"flex gap-2\">\n              <div className=\"h-8 px-3 rounded-md border border-border flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <span>0:04</span>\n              </div>\n              <div className=\"h-8 px-3 rounded-md border border-border flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <Mic className=\"h-3 w-3\" />\n                Transcribe\n              </div>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction RecordPanel() {\n  const [state, setState] = useState<'idle' | 'recording' | 'done'>('idle');\n  const [elapsed, setElapsed] = useState(0);\n\n  useEffect(() => {\n    const t1 = setTimeout(() => setState('recording'), 1500);\n    const t2 = setTimeout(() => setState('done'), 5500);\n    const t3 = setTimeout(() => {\n      setState('idle');\n      setElapsed(0);\n    }, 8000);\n    return () => {\n      clearTimeout(t1);\n      clearTimeout(t2);\n      clearTimeout(t3);\n    };\n  }, []);\n\n  // Timer\n  useEffect(() => {\n    if (state !== 'recording') return;\n    setElapsed(0);\n    const interval = setInterval(() => setElapsed((e) => e + 1), 1000);\n    return () => clearInterval(interval);\n  }, [state]);\n\n  const formatTime = (s: number) => `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, '0')}`;\n\n  return (\n    <div\n      className={`relative flex flex-col items-center justify-center gap-3 p-6 border-2 rounded-lg min-h-[180px] overflow-hidden transition-colors duration-300 ${\n        state === 'recording'\n          ? 'border-accent bg-accent/5'\n          : state === 'done'\n            ? 'border-accent bg-accent/5'\n            : 'border-dashed border-muted-foreground/25'\n      }`}\n    >\n      <WaveformBackground active={state === 'recording'} />\n\n      <AnimatePresence mode=\"wait\">\n        {state === 'idle' && (\n          <motion.div\n            key=\"idle\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"relative z-10 flex flex-col items-center gap-3\"\n          >\n            <div className=\"h-10 px-5 rounded-md bg-accent text-accent-foreground flex items-center gap-2 text-sm font-medium\">\n              <Mic className=\"h-4 w-4\" />\n              Start Recording\n            </div>\n            <p className=\"text-xs text-muted-foreground text-center\">\n              Click to record from your microphone.\n              <br />\n              Maximum duration: 30 seconds.\n            </p>\n          </motion.div>\n        )}\n\n        {state === 'recording' && (\n          <motion.div\n            key=\"recording\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"relative z-10 flex flex-col items-center gap-3\"\n          >\n            <div className=\"flex items-center gap-3\">\n              <div className=\"h-3 w-3 rounded-full bg-accent animate-pulse\" />\n              <span className=\"text-lg font-mono font-semibold\">{formatTime(elapsed)}</span>\n            </div>\n            <div className=\"h-9 px-4 rounded-md bg-accent text-accent-foreground flex items-center gap-2 text-sm font-medium\">\n              <div className=\"h-3 w-3 rounded-sm bg-accent-foreground\" />\n              Stop Recording\n            </div>\n            <p className=\"text-xs text-muted-foreground\">{formatTime(30 - elapsed)} remaining</p>\n          </motion.div>\n        )}\n\n        {state === 'done' && (\n          <motion.div\n            key=\"done\"\n            initial={{ opacity: 0, scale: 0.95 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"relative z-10 flex flex-col items-center gap-3\"\n          >\n            <div className=\"flex items-center gap-2\">\n              <Mic className=\"h-4 w-4 text-accent\" />\n              <span className=\"text-sm font-medium\">Recording complete</span>\n            </div>\n            <div className=\"flex gap-2\">\n              <div className=\"h-8 px-3 rounded-md border border-border flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <span>0:04</span>\n              </div>\n              <div className=\"h-8 px-3 rounded-md border border-border flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <Mic className=\"h-3 w-3\" />\n                Transcribe\n              </div>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction SystemPanel() {\n  const [state, setState] = useState<'idle' | 'capturing' | 'done'>('idle');\n  const [elapsed, setElapsed] = useState(0);\n\n  useEffect(() => {\n    const t1 = setTimeout(() => setState('capturing'), 1500);\n    const t2 = setTimeout(() => setState('done'), 5500);\n    const t3 = setTimeout(() => {\n      setState('idle');\n      setElapsed(0);\n    }, 8000);\n    return () => {\n      clearTimeout(t1);\n      clearTimeout(t2);\n      clearTimeout(t3);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (state !== 'capturing') return;\n    setElapsed(0);\n    const interval = setInterval(() => setElapsed((e) => e + 1), 1000);\n    return () => clearInterval(interval);\n  }, [state]);\n\n  const formatTime = (s: number) => `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, '0')}`;\n\n  return (\n    <div\n      className={`relative flex flex-col items-center justify-center gap-3 p-6 border-2 rounded-lg min-h-[180px] overflow-hidden transition-colors duration-300 ${\n        state === 'capturing'\n          ? 'border-accent bg-accent/5'\n          : state === 'done'\n            ? 'border-accent bg-accent/5'\n            : 'border-dashed border-muted-foreground/25'\n      }`}\n    >\n      <WaveformBackground active={state === 'capturing'} />\n\n      <AnimatePresence mode=\"wait\">\n        {state === 'idle' && (\n          <motion.div\n            key=\"idle\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"relative z-10 flex flex-col items-center gap-3\"\n          >\n            <div className=\"h-10 px-5 rounded-md bg-accent text-accent-foreground flex items-center gap-2 text-sm font-medium\">\n              <Monitor className=\"h-4 w-4\" />\n              Start Capture\n            </div>\n            <p className=\"text-xs text-muted-foreground text-center\">\n              Capture audio playing on your system.\n              <br />\n              Maximum duration: 30 seconds.\n            </p>\n          </motion.div>\n        )}\n\n        {state === 'capturing' && (\n          <motion.div\n            key=\"capturing\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"relative z-10 flex flex-col items-center gap-3\"\n          >\n            <div className=\"flex items-center gap-3\">\n              <div className=\"h-3 w-3 rounded-full bg-accent animate-pulse\" />\n              <span className=\"text-lg font-mono font-semibold\">{formatTime(elapsed)}</span>\n            </div>\n            <div className=\"h-9 px-4 rounded-md bg-accent text-accent-foreground flex items-center gap-2 text-sm font-medium\">\n              <div className=\"h-3 w-3 rounded-sm bg-accent-foreground\" />\n              Stop Capture\n            </div>\n            <p className=\"text-xs text-muted-foreground\">{formatTime(30 - elapsed)} remaining</p>\n          </motion.div>\n        )}\n\n        {state === 'done' && (\n          <motion.div\n            key=\"done\"\n            initial={{ opacity: 0, scale: 0.95 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"relative z-10 flex flex-col items-center gap-3\"\n          >\n            <div className=\"flex items-center gap-2\">\n              <Monitor className=\"h-4 w-4 text-accent\" />\n              <span className=\"text-sm font-medium\">Capture complete</span>\n            </div>\n            <div className=\"flex gap-2\">\n              <div className=\"h-8 px-3 rounded-md border border-border flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <span>0:04</span>\n              </div>\n              <div className=\"h-8 px-3 rounded-md border border-border flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <Mic className=\"h-3 w-3\" />\n                Transcribe\n              </div>\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\n// ─── Tab selector ───────────────────────────────────────────────────────────\n\nconst TABS = [\n  { id: 'upload' as const, label: 'Upload', icon: Upload },\n  { id: 'record' as const, label: 'Microphone', icon: Mic },\n  { id: 'system' as const, label: 'System Audio', icon: Monitor },\n];\n\ntype TabId = (typeof TABS)[number]['id'];\n\n// ─── Main section ───────────────────────────────────────────────────────────\n\nexport function VoiceCreator() {\n  const [activeTab, setActiveTab] = useState<TabId>('record');\n  const [cycleKey, setCycleKey] = useState(0);\n\n  // Auto-cycle tabs\n  useEffect(() => {\n    const tabOrder: TabId[] = ['record', 'upload', 'system'];\n    let idx = tabOrder.indexOf(activeTab);\n\n    const interval = setInterval(() => {\n      idx = (idx + 1) % tabOrder.length;\n      setActiveTab(tabOrder[idx]);\n      setCycleKey((k) => k + 1);\n    }, 9000);\n\n    return () => clearInterval(interval);\n  }, [activeTab]);\n\n  return (\n    <section className=\"border-t border-border py-24\">\n      <div className=\"mx-auto max-w-5xl px-6\">\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-16 items-center\">\n          {/* Left: Copy */}\n          <div>\n            <h2 className=\"text-3xl font-semibold tracking-tight text-foreground md:text-4xl mb-4\">\n              Clone any voice in seconds\n            </h2>\n            <p className=\"text-muted-foreground mb-6\">\n              Three ways to capture a voice sample. Upload a clip, record from your microphone, or\n              capture audio playing on your system. Voicebox clones the voice from as little as 3\n              seconds of audio.\n            </p>\n            <div className=\"space-y-3\">\n              <div className=\"flex items-start gap-3\">\n                <div className=\"h-8 w-8 rounded-lg bg-accent/10 flex items-center justify-center shrink-0 mt-0.5\">\n                  <Upload className=\"h-4 w-4 text-accent\" />\n                </div>\n                <div>\n                  <div className=\"text-sm font-medium\">Upload a clip</div>\n                  <div className=\"text-xs text-muted-foreground\">\n                    Drag and drop any audio file — WAV, MP3, FLAC, or WebM.\n                  </div>\n                </div>\n              </div>\n              <div className=\"flex items-start gap-3\">\n                <div className=\"h-8 w-8 rounded-lg bg-accent/10 flex items-center justify-center shrink-0 mt-0.5\">\n                  <Mic className=\"h-4 w-4 text-accent\" />\n                </div>\n                <div>\n                  <div className=\"text-sm font-medium\">Record from microphone</div>\n                  <div className=\"text-xs text-muted-foreground\">\n                    Live waveform preview while you record. Up to 30 seconds.\n                  </div>\n                </div>\n              </div>\n              <div className=\"flex items-start gap-3\">\n                <div className=\"h-8 w-8 rounded-lg bg-accent/10 flex items-center justify-center shrink-0 mt-0.5\">\n                  <Monitor className=\"h-4 w-4 text-accent\" />\n                </div>\n                <div>\n                  <div className=\"text-sm font-medium\">System audio capture</div>\n                  <div className=\"text-xs text-muted-foreground\">\n                    Clone a voice from a YouTube video, podcast, or any app playing audio.\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          {/* Right: Animated UI mock */}\n          <div className=\"rounded-xl border border-app-line bg-app-darkBox overflow-hidden pointer-events-none select-none\">\n            <div className=\"p-5\">\n              {/* Tab bar */}\n              <div className=\"flex rounded-lg border border-border bg-card/50 p-1 mb-4\">\n                {TABS.map((tab) => {\n                  const Icon = tab.icon;\n                  const isActive = activeTab === tab.id;\n                  return (\n                    <button\n                      key={tab.id}\n                      onClick={() => {\n                        setActiveTab(tab.id);\n                        setCycleKey((k) => k + 1);\n                      }}\n                      className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${\n                        isActive\n                          ? 'bg-background text-foreground shadow-sm'\n                          : 'text-muted-foreground hover:text-foreground'\n                      }`}\n                    >\n                      <Icon className=\"h-3.5 w-3.5\" />\n                      <span className=\"hidden sm:inline\">{tab.label}</span>\n                    </button>\n                  );\n                })}\n              </div>\n\n              {/* Panel */}\n              <AnimatePresence mode=\"wait\">\n                <motion.div\n                  key={`${activeTab}-${cycleKey}`}\n                  initial={{ opacity: 0, y: 6 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0, y: -6 }}\n                  transition={{ duration: 0.2 }}\n                >\n                  {activeTab === 'upload' && <UploadPanel />}\n                  {activeTab === 'record' && <RecordPanel />}\n                  {activeTab === 'system' && <SystemPanel />}\n                </motion.div>\n              </AnimatePresence>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/ui/button.tsx",
    "content": "import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n  'inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',\n  {\n    variants: {\n      variant: {\n        default:\n          'bg-primary/10 backdrop-blur-sm border border-border text-primary hover:bg-primary/11 hover:border-primary/15 shadow-lg shadow-black/20 active:scale-[0.99]',\n        destructive:\n          'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-lg shadow-destructive/20 active:scale-[0.98]',\n        outline:\n          'border border-border bg-background/50 backdrop-blur-sm hover:bg-foreground/5 transition-all active:scale-[0.99]',\n        secondary:\n          'bg-secondary text-secondary-foreground hover:bg-secondary/70 backdrop-blur-sm active:scale-[0.99]',\n        ghost:\n          'hover:bg-accent/30 hover:text-accent-foreground backdrop-blur-sm active:scale-[0.99]',\n        link: 'text-primary underline-offset-4 hover:underline',\n      },\n      size: {\n        default: 'h-10 px-4 py-2',\n        sm: 'h-9 rounded-lg px-3',\n        lg: 'h-12 rounded-xl text-base px-8 font-semibold',\n        icon: 'h-10 w-10 rounded-xl',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : 'button';\n    return (\n      <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />\n    );\n  },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "landing/src/components/ui/card.tsx",
    "content": "import * as React from 'react';\nimport { cn } from '@/lib/utils';\n\nconst Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div\n      ref={ref}\n      className={cn(\n        'rounded-2xl border border-border bg-card backdrop-blur-xl text-card-foreground shadow-lg shadow-black/20',\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nCard.displayName = 'Card';\n\nconst CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6 pb-4', className)} {...props} />\n  ),\n);\nCardHeader.displayName = 'CardHeader';\n\nconst CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(\n  ({ className, ...props }, ref) => (\n    <h3\n      ref={ref}\n      className={cn('text-2xl font-semibold leading-none tracking-tight', className)}\n      {...props}\n    />\n  ),\n);\nCardTitle.displayName = 'CardTitle';\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />\n));\nCardDescription.displayName = 'CardDescription';\n\nconst CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn('p-6 pt-4', className)} {...props} />\n  ),\n);\nCardContent.displayName = 'CardContent';\n\nconst CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />\n  ),\n);\nCardFooter.displayName = 'CardFooter';\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };\n"
  },
  {
    "path": "landing/src/components/ui/feature-card.tsx",
    "content": "import { ReactNode } from 'react';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from './card';\nimport { cn } from '@/lib/utils';\n\ninterface FeatureCardProps {\n  title: string;\n  description: string;\n  icon?: ReactNode;\n  className?: string;\n}\n\nexport function FeatureCard({ title, description, icon, className }: FeatureCardProps) {\n  return (\n    <Card\n      className={cn(\n        'text-center hover:border-primary/20 hover:shadow-lg hover:shadow-primary/3 transition-all duration-200 hover:-translate-y-0.5',\n        className,\n      )}\n    >\n      <CardHeader>\n        {icon && (\n          <div className=\"flex justify-center mb-3\">\n            <div className=\"p-3 rounded-xl bg-muted/50 backdrop-blur-sm border border-border\">\n              {icon}\n            </div>\n          </div>\n        )}\n        <CardTitle className=\"text-xl sm:text-2xl\">{title}</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <CardDescription className=\"text-sm sm:text-base\">{description}</CardDescription>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/ui/hero.tsx",
    "content": "'use client';\n\nimport Image from 'next/image';\nimport type { ReactNode } from 'react';\nimport { cn } from '@/lib/utils';\n\ninterface HeroProps {\n  title: ReactNode;\n  description: string;\n  actions?: ReactNode;\n  className?: string;\n  showLogo?: boolean;\n}\n\nexport function Hero({ title, description, actions, className, showLogo = true }: HeroProps) {\n  return (\n    <section className={cn('relative py-12 sm:py-16 md:py-20 lg:py-24', className)}>\n      <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center\">\n        {/* Left side - Content */}\n        <div className=\"space-y-6 lg:pr-8\">\n          {showLogo && (\n            <div className=\"flex lg:justify-start justify-center mb-6\">\n              <Image\n                src=\"/voicebox-logo-2.png\"\n                alt=\"Voicebox Logo\"\n                width={1024}\n                height={1024}\n                className=\"w-32 sm:w-40 md:w-48 h-auto\"\n                priority\n              />\n            </div>\n          )}\n          <h1 className=\"text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold leading-tight text-left\">\n            {title}\n          </h1>\n          <p className=\"text-lg sm:text-xl md:text-2xl text-foreground/70 max-w-xl text-left\">\n            {description}\n          </p>\n        </div>\n\n        {/* Right side - Actions */}\n        <div className=\"flex flex-col items-start lg:items-end gap-4\">{actions}</div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/ui/section.tsx",
    "content": "import { cn } from '@/lib/utils';\nimport { ReactNode } from 'react';\n\ninterface SectionProps {\n  children: ReactNode;\n  className?: string;\n  id?: string;\n}\n\nexport function Section({ children, className, id }: SectionProps) {\n  return (\n    <section id={id} className={cn('space-y-6 sm:space-y-8', className)}>\n      {children}\n    </section>\n  );\n}\n\nexport function SectionTitle({ children, className }: { children: ReactNode; className?: string }) {\n  return (\n    <h2\n      className={cn(\n        'text-2xl sm:text-3xl md:text-4xl font-bold text-center md:text-left',\n        className,\n      )}\n    >\n      {children}\n    </h2>\n  );\n}\n"
  },
  {
    "path": "landing/src/components/ui/separator.tsx",
    "content": "import * as React from 'react';\nimport * as SeparatorPrimitive from '@radix-ui/react-separator';\nimport { cn } from '@/lib/utils';\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (\n  <SeparatorPrimitive.Root\n    ref={ref}\n    decorative={decorative}\n    orientation={orientation}\n    className={cn(\n      'shrink-0 bg-border',\n      orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',\n      className,\n    )}\n    {...props}\n  />\n));\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "landing/src/lib/constants.ts",
    "content": "// Download links for voicebox releases\n// These are fallback values - link to releases page if API fails\nexport const LATEST_VERSION = 'v0.1.0';\n\nexport const GITHUB_REPO = 'https://github.com/jamiepine/voicebox';\nexport const GITHUB_RELEASES_PAGE = `${GITHUB_REPO}/releases`;\n\nexport const DOWNLOAD_LINKS = {\n  macArm: GITHUB_RELEASES_PAGE,\n  macIntel: GITHUB_RELEASES_PAGE,\n  windows: GITHUB_RELEASES_PAGE,\n  linux: GITHUB_RELEASES_PAGE,\n} as const;\n\n// Export function to get dynamic download links\nexport { getLatestRelease } from './releases';\n"
  },
  {
    "path": "landing/src/lib/releases.ts",
    "content": "// Fetch latest release information from GitHub\nexport interface DownloadLinks {\n  macArm: string;\n  macIntel: string;\n  windows: string;\n  linux: string;\n}\n\nexport interface ReleaseInfo {\n  version: string;\n  downloadLinks: DownloadLinks;\n  totalDownloads: number;\n}\n\nconst GITHUB_REPO = 'jamiepine/voicebox';\nconst GITHUB_API_BASE = 'https://api.github.com';\n\n// Cache for release info (in-memory cache, resets on server restart)\nlet cachedReleaseInfo: ReleaseInfo | null = null;\nlet cacheTimestamp: number = 0;\nconst CACHE_DURATION = 1000 * 60 * 5; // 5 minutes\n\n// Cache for star count\nlet cachedStarCount: number | null = null;\nlet starCacheTimestamp: number = 0;\n\n/**\n * Fetches the latest release from GitHub and extracts download links\n */\nexport async function getLatestRelease(): Promise<ReleaseInfo> {\n  // Return cached data if still valid\n  const now = Date.now();\n  if (cachedReleaseInfo && now - cacheTimestamp < CACHE_DURATION) {\n    return cachedReleaseInfo;\n  }\n\n  try {\n    const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases/latest`, {\n      cache: 'no-store',\n      headers: {\n        Accept: 'application/vnd.github.v3+json',\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`GitHub API error: ${response.status}`);\n    }\n\n    const release = await response.json();\n    const version = release.tag_name;\n    const assets = release.assets || [];\n\n    // Extract download links based on file patterns\n    const downloadLinks: Partial<DownloadLinks> = {};\n\n    for (const asset of assets) {\n      const name = asset.name.toLowerCase();\n      const url = asset.browser_download_url;\n\n      // Skip signature files and other non-downloadable files\n      if (name.endsWith('.sig') || name.endsWith('.json') || name.endsWith('.txt')) {\n        continue;\n      }\n\n      if ((name.includes('aarch64') || name.includes('arm64')) && name.endsWith('.dmg')) {\n        downloadLinks.macArm = url;\n      } else if (name.includes('x64') && name.endsWith('.dmg')) {\n        downloadLinks.macIntel = url;\n      } else if (name.endsWith('.msi')) {\n        downloadLinks.windows = url;\n      } else if (name.endsWith('.appimage') || name.endsWith('.deb')) {\n        downloadLinks.linux = url;\n      }\n    }\n\n    // Fetch total downloads across ALL releases\n    const totalDownloads = await getTotalDownloads();\n\n    // Fallback: construct URLs if not found in assets\n    const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}`;\n\n    const releaseInfo: ReleaseInfo = {\n      version,\n      totalDownloads,\n      downloadLinks: {\n        macArm:\n          downloadLinks.macArm || `${baseUrl}/Voicebox_${version.replace('v', '')}_aarch64.dmg`,\n        macIntel:\n          downloadLinks.macIntel || `${baseUrl}/Voicebox_${version.replace('v', '')}_x64.dmg`,\n        windows:\n          downloadLinks.windows || `${baseUrl}/voicebox_${version.replace('v', '')}_x64_en-US.msi`,\n        linux: downloadLinks.linux || `${baseUrl}/voicebox_x86_64-unknown-linux-gnu.AppImage`,\n      },\n    };\n\n    // Update cache\n    cachedReleaseInfo = releaseInfo;\n    cacheTimestamp = now;\n\n    return releaseInfo;\n  } catch (error) {\n    console.error('Failed to fetch latest release:', error);\n    throw error;\n  }\n}\n\n// Cache for total download count\nlet cachedTotalDownloads: number | null = null;\nlet downloadsCacheTimestamp: number = 0;\n\n/**\n * Fetches download counts across ALL releases (paginated)\n */\nasync function getTotalDownloads(): Promise<number> {\n  const now = Date.now();\n  if (cachedTotalDownloads !== null && now - downloadsCacheTimestamp < CACHE_DURATION) {\n    return cachedTotalDownloads;\n  }\n\n  let total = 0;\n  let page = 1;\n\n  try {\n    while (true) {\n      const response = await fetch(\n        `${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases?per_page=100&page=${page}`,\n        {\n          cache: 'no-store',\n          headers: { Accept: 'application/vnd.github.v3+json' },\n        },\n      );\n\n      if (!response.ok) break;\n\n      const releases = await response.json();\n      if (!Array.isArray(releases) || releases.length === 0) break;\n\n      for (const release of releases) {\n        for (const asset of release.assets || []) {\n          total += asset.download_count || 0;\n        }\n      }\n\n      if (releases.length < 100) break;\n      page++;\n    }\n\n    cachedTotalDownloads = total;\n    downloadsCacheTimestamp = now;\n  } catch (error) {\n    console.error('Failed to fetch total downloads:', error);\n    if (cachedTotalDownloads !== null) return cachedTotalDownloads;\n  }\n\n  return total;\n}\n\n/**\n * Fetches the star count for the repo from GitHub\n */\nexport async function getStarCount(): Promise<number> {\n  const now = Date.now();\n  if (cachedStarCount !== null && now - starCacheTimestamp < CACHE_DURATION) {\n    return cachedStarCount;\n  }\n\n  try {\n    const response = await fetch(`${GITHUB_API_BASE}/repos/${GITHUB_REPO}`, {\n      next: { revalidate: 600 },\n      headers: {\n        Accept: 'application/vnd.github.v3+json',\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`GitHub API error: ${response.status}`);\n    }\n\n    const repo = await response.json();\n    const count = repo.stargazers_count ?? 0;\n\n    cachedStarCount = count;\n    starCacheTimestamp = now;\n\n    return count;\n  } catch (error) {\n    console.error('Failed to fetch star count:', error);\n    if (cachedStarCount !== null) return cachedStarCount;\n    throw error;\n  }\n}\n"
  },
  {
    "path": "landing/src/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "landing/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: ['class'],\n  content: [\n    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',\n    './src/components/**/*.{js,ts,jsx,tsx,mdx}',\n    './src/app/**/*.{js,ts,jsx,tsx,mdx}',\n  ],\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],\n      },\n      colors: {\n        border: 'hsl(var(--border))',\n        input: 'hsl(var(--input))',\n        ring: 'hsl(var(--ring))',\n        background: 'hsl(var(--background))',\n        foreground: 'hsl(var(--foreground))',\n        primary: {\n          DEFAULT: 'hsl(var(--primary))',\n          foreground: 'hsl(var(--primary-foreground))',\n        },\n        secondary: {\n          DEFAULT: 'hsl(var(--secondary))',\n          foreground: 'hsl(var(--secondary-foreground))',\n        },\n        destructive: {\n          DEFAULT: 'hsl(var(--destructive))',\n          foreground: 'hsl(var(--destructive-foreground))',\n        },\n        muted: {\n          DEFAULT: 'hsl(var(--muted))',\n          foreground: 'hsl(var(--muted-foreground))',\n        },\n        accent: {\n          DEFAULT: 'hsl(var(--accent))',\n          foreground: 'hsl(var(--accent-foreground))',\n          faint: 'hsl(var(--accent-faint))',\n          deep: 'hsl(var(--accent-deep))',\n          glow: 'hsl(var(--accent-glow))',\n        },\n        popover: {\n          DEFAULT: 'hsl(var(--popover))',\n          foreground: 'hsl(var(--popover-foreground))',\n        },\n        card: {\n          DEFAULT: 'hsl(var(--card))',\n          foreground: 'hsl(var(--card-foreground))',\n        },\n        // App surface tokens\n        app: {\n          DEFAULT: 'hsl(var(--app))',\n          box: 'hsl(var(--app-box))',\n          darkBox: 'hsl(var(--app-dark-box))',\n          darkerBox: 'hsl(var(--app-darker-box))',\n          lightBox: 'hsl(var(--app-light-box))',\n          line: 'hsl(var(--app-line))',\n          button: 'hsl(var(--app-button))',\n          hover: 'hsl(var(--app-hover))',\n          selected: 'hsl(var(--app-selected))',\n        },\n        ink: {\n          DEFAULT: 'hsl(var(--ink))',\n          dull: 'hsl(var(--ink-dull))',\n          faint: 'hsl(var(--ink-faint))',\n        },\n        sidebar: {\n          DEFAULT: 'hsl(var(--sidebar))',\n          line: 'hsl(var(--sidebar-line))',\n        },\n      },\n      borderRadius: {\n        lg: 'var(--radius)',\n        md: 'calc(var(--radius) - 2px)',\n        sm: 'calc(var(--radius) - 4px)',\n      },\n    },\n  },\n  plugins: [require('tailwindcss-animate')],\n};\n"
  },
  {
    "path": "landing/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"voicebox\",\n  \"version\": \"0.3.1\",\n  \"private\": true,\n  \"workspaces\": [\n    \"app\",\n    \"tauri\",\n    \"web\",\n    \"landing\"\n  ],\n  \"scripts\": {\n    \"dev\": \"bun run setup:dev && cd tauri && bun run tauri dev\",\n    \"dev:web\": \"cd web && bun run dev\",\n    \"dev:landing\": \"cd landing && bun run dev\",\n    \"dev:server\": \"uvicorn backend.main:app --reload --port 17493\",\n    \"setup:dev\": \"bun run scripts/setup-dev-sidecar.js\",\n    \"build\": \"./scripts/build-server.sh && cd tauri && bun run tauri build\",\n    \"build:web\": \"cd web && bun run build\",\n    \"build:landing\": \"cd landing && bun run build\",\n    \"build:release\": \"./scripts/prepare-release.sh\",\n    \"generate:api\": \"./scripts/generate-api.sh\",\n    \"generate:keys\": \"cd tauri && bun tauri signer generate -w ~/.tauri/voicebox.key\",\n    \"build:server\": \"./scripts/build-server.sh\",\n    \"update:icons\": \"./scripts/update-icons.sh\",\n    \"convert:assets\": \"./scripts/convert-assets.sh\",\n    \"lint\": \"biome lint .\",\n    \"lint:fix\": \"biome lint --write .\",\n    \"format\": \"biome format --write .\",\n    \"format:check\": \"biome format .\",\n    \"check\": \"biome check .\",\n    \"check:fix\": \"biome check --write .\",\n    \"ci\": \"biome ci .\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"2.3.12\",\n    \"@types/node\": \"^20.0.0\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"typescript\": \"^5.6.0\"\n  },\n  \"engines\": {\n    \"bun\": \">=1.0.0\"\n  },\n  \"packageManager\": \"bun@1.3.8\",\n  \"dependencies\": {\n    \"loaders.css\": \"^0.1.2\",\n    \"react-loaders\": \"^3.0.1\"\n  }\n}\n"
  },
  {
    "path": "requirements.txt",
    "content": "uvicorn\nfastapi\nsqlalchemy\ntorch\ntorchvision\nsoundfile\nlibrosa\npython-multipart\nhuggingface_hub\n"
  },
  {
    "path": "scripts/build-server.sh",
    "content": "#!/bin/bash\n# Build Python server binary for all platforms\n\nset -e\n\n# Determine platform\nPLATFORM=$(rustc --print host-tuple 2>/dev/null || echo \"unknown\")\n\necho \"Building voicebox-server for platform: $PLATFORM\"\n\n# Build Python binary\n# Resolve PATH to absolute paths before changing directory\nexport PATH=\"$(cd \"$(dirname \"$0\")/..\" && pwd)/backend/venv/bin:$PATH\"\ncd backend\n\n# Check if PyInstaller is installed\nif ! python -c \"import PyInstaller\" 2>/dev/null; then\n    echo \"Installing PyInstaller...\"\n    python -m pip install pyinstaller\nfi\n\n# Build binary\npython build_binary.py\n\n# Create binaries directory if it doesn't exist\nmkdir -p ../tauri/src-tauri/binaries\n\n# Copy binary with platform suffix\nif [ -f dist/voicebox-server ]; then\n    cp dist/voicebox-server ../tauri/src-tauri/binaries/voicebox-server-${PLATFORM}\n    chmod +x ../tauri/src-tauri/binaries/voicebox-server-${PLATFORM}\n    echo \"Built voicebox-server-${PLATFORM}\"\nelif [ -f dist/voicebox-server.exe ]; then\n    cp dist/voicebox-server.exe ../tauri/src-tauri/binaries/voicebox-server-${PLATFORM}.exe\n    echo \"Built voicebox-server-${PLATFORM}.exe\"\nelse\n    echo \"Error: Binary not found in dist/\"\n    exit 1\nfi\n\necho \"Build complete!\"\n"
  },
  {
    "path": "scripts/convert-assets.sh",
    "content": "#!/bin/bash\nset -e\n\n# Asset Conversion Script\n# Converts PNG → WebP and MOV → WebM in public folders\n# Deletes original files after successful conversion\n\ncd \"$(dirname \"$0\")/..\"\n\n# Directories to process\nDIRS=(\n  \"landing/public\"\n  \"docs/public\"\n)\n\n# Track counts\npng_converted=0\nmov_converted=0\npng_failed=0\nmov_failed=0\n\necho \"🔄 Converting assets to web-optimized formats...\"\necho \"\"\n\n# Check for required tools\ncheck_dependencies() {\n  local missing=()\n  \n  if ! command -v cwebp &> /dev/null && ! command -v ffmpeg &> /dev/null; then\n    missing+=(\"cwebp or ffmpeg (for PNG→WebP)\")\n  fi\n  \n  if ! command -v ffmpeg &> /dev/null; then\n    missing+=(\"ffmpeg (for MOV→WebM)\")\n  fi\n  \n  if [ ${#missing[@]} -ne 0 ]; then\n    echo \"❌ Missing required tools:\"\n    for tool in \"${missing[@]}\"; do\n      echo \"   - $tool\"\n    done\n    echo \"\"\n    echo \"Install with: brew install webp ffmpeg\"\n    exit 1\n  fi\n}\n\n# Convert PNG to WebP\nconvert_png() {\n  local input=\"$1\"\n  local output=\"${input%.png}.webp\"\n  \n  echo \"  Converting: $input\"\n  \n  if command -v cwebp &> /dev/null; then\n    # Use cwebp for best quality/size ratio\n    if cwebp -q 90 \"$input\" -o \"$output\" 2>/dev/null; then\n      rm \"$input\"\n      echo \"    ✓ → $output\"\n      return 0\n    fi\n  elif command -v ffmpeg &> /dev/null; then\n    # Fallback to ffmpeg\n    if ffmpeg -i \"$input\" -c:v libwebp -quality 90 \"$output\" -y 2>/dev/null; then\n      rm \"$input\"\n      echo \"    ✓ → $output\"\n      return 0\n    fi\n  fi\n  \n  echo \"    ✗ Failed to convert\"\n  return 1\n}\n\n# Convert MOV to WebM\nconvert_mov() {\n  local input=\"$1\"\n  local output=\"${input%.mov}.webm\"\n  \n  echo \"  Converting: $input\"\n  \n  if ffmpeg -i \"$input\" -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus \"$output\" -y 2>/dev/null; then\n    rm \"$input\"\n    echo \"    ✓ → $output\"\n    return 0\n  fi\n  \n  echo \"    ✗ Failed to convert\"\n  return 1\n}\n\n# Main execution\ncheck_dependencies\n\nfor dir in \"${DIRS[@]}\"; do\n  if [ ! -d \"$dir\" ]; then\n    echo \"⚠ Directory not found: $dir (skipping)\"\n    continue\n  fi\n  \n  echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n  echo \"📁 Processing: $dir\"\n  echo \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\n  \n  # Find and convert PNGs (recursively)\n  while IFS= read -r -d '' file; do\n    if convert_png \"$file\"; then\n      ((png_converted++))\n    else\n      ((png_failed++))\n    fi\n  done < <(find \"$dir\" -type f -name \"*.png\" -print0 2>/dev/null)\n  \n  # Find and convert MOVs (recursively)\n  while IFS= read -r -d '' file; do\n    if convert_mov \"$file\"; then\n      ((mov_converted++))\n    else\n      ((mov_failed++))\n    fi\n  done < <(find \"$dir\" -type f -name \"*.mov\" -print0 2>/dev/null)\n  \n  # Also check for uppercase extensions\n  while IFS= read -r -d '' file; do\n    if convert_png \"$file\"; then\n      ((png_converted++))\n    else\n      ((png_failed++))\n    fi\n  done < <(find \"$dir\" -type f -name \"*.PNG\" -print0 2>/dev/null)\n  \n  while IFS= read -r -d '' file; do\n    if convert_mov \"$file\"; then\n      ((mov_converted++))\n    else\n      ((mov_failed++))\n    fi\n  done < <(find \"$dir\" -type f -name \"*.MOV\" -print0 2>/dev/null)\n  \n  echo \"\"\ndone\n\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"✅ Conversion complete!\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\necho \"Results:\"\necho \"  PNG → WebP: $png_converted converted, $png_failed failed\"\necho \"  MOV → WebM: $mov_converted converted, $mov_failed failed\"\n\nif [ $png_converted -eq 0 ] && [ $mov_converted -eq 0 ]; then\n  echo \"\"\n  echo \"No PNG or MOV files found to convert.\"\nfi\n"
  },
  {
    "path": "scripts/generate-api.sh",
    "content": "#!/bin/bash\n# Generate OpenAPI TypeScript client from FastAPI schema\n\nset -e\n\necho \"Generating OpenAPI client...\"\n\n# Check if backend is running\nif ! curl -s http://localhost:17493/openapi.json > /dev/null 2>&1; then\n    echo \"Backend not running. Starting backend...\"\n    cd backend\n    \n    # Check if virtual environment exists\n    if [ ! -d \"venv\" ]; then\n        echo \"Creating virtual environment...\"\n        python -m venv venv\n    fi\n    \n    source venv/bin/activate 2>/dev/null || source venv/Scripts/activate 2>/dev/null\n    \n    # Install dependencies if needed\n    if ! python -c \"import fastapi\" 2>/dev/null; then\n        echo \"Installing backend dependencies...\"\n        pip install -r requirements.txt\n    fi\n    \n    # Start backend in background\n    echo \"Starting backend server...\"\n    uvicorn main:app --port 17493 &  # Keep the generator on the app's documented local backend port.\n    BACKEND_PID=$!\n    \n    # Wait for server to be ready\n    echo \"Waiting for server to start...\"\n    for _ in {1..30}; do\n        if curl -s http://localhost:17493/openapi.json > /dev/null 2>&1; then\n            break\n        fi\n        sleep 1\n    done\n    \n    if ! curl -s http://localhost:17493/openapi.json > /dev/null 2>&1; then\n        echo \"Error: Backend failed to start\"\n        kill $BACKEND_PID 2>/dev/null || true\n        exit 1\n    fi\n    \n    echo \"Backend started (PID: $BACKEND_PID)\"\n    STARTED_BACKEND=true\nelse\n    STARTED_BACKEND=false\nfi\n\n# Download OpenAPI schema\necho \"Downloading OpenAPI schema...\"\ncurl -s http://localhost:17493/openapi.json > app/openapi.json\n\n# Check if openapi-typescript-codegen is installed\nif ! bunx --bun openapi-typescript-codegen --version > /dev/null 2>&1; then\n    echo \"Installing openapi-typescript-codegen...\"\n    bun add -d openapi-typescript-codegen\nfi\n\n# Generate TypeScript client\necho \"Generating TypeScript client...\"\ncd app\nbunx --bun openapi-typescript-codegen \\\n    --input openapi.json \\\n    --output src/lib/api \\\n    --client fetch \\\n    --useOptions \\\n    --exportSchemas true\n\necho \"API client generated in app/src/lib/api\"\n\n# Clean up\nif [ \"$STARTED_BACKEND\" = true ]; then\n    echo \"Stopping backend server...\"\n    kill $BACKEND_PID 2>/dev/null || true\nfi\n\necho \"Done!\"\n"
  },
  {
    "path": "scripts/package_cuda.py",
    "content": "\"\"\"\nPackage the PyInstaller --onedir CUDA build into two archives.\n\nTakes the PyInstaller --onedir output directory and splits it into:\n  1. voicebox-server-cuda.tar.gz  — server core (exe + non-NVIDIA deps)\n  2. cuda-libs-cu128.tar.gz       — NVIDIA runtime libraries only\n  3. cuda-libs.json                — version manifest for the CUDA libs\n\nUsage:\n    python scripts/package_cuda.py backend/dist/voicebox-server-cuda/\n    python scripts/package_cuda.py backend/dist/voicebox-server-cuda/ --output release-assets/\n    python scripts/package_cuda.py backend/dist/voicebox-server-cuda/ --cuda-libs-version cu128-v1\n\"\"\"\n\nimport argparse\nimport hashlib\nimport json\nimport sys\nimport tarfile\nfrom pathlib import Path\n\n# DLL name prefixes that identify NVIDIA CUDA runtime libraries.\n# These DLLs may appear in different locations depending on the torch\n# and PyInstaller version:\n#   - nvidia/ subdirectories (older torch with separate nvidia-* packages)\n#   - _internal/torch/lib/ (torch 2.10+ bundles NVIDIA DLLs directly)\n#   - Top-level directory (some PyInstaller versions)\nNVIDIA_DLL_PREFIXES = (\n    \"cublas\",\n    \"cublaslt\",\n    \"cudart\",\n    \"cudnn\",\n    \"cufft\",\n    \"cufftw\",\n    \"curand\",\n    \"cusolver\",\n    \"cusolvermg\",\n    \"cusparse\",\n    \"nvjitlink\",\n    \"nvrtc\",\n    \"nccl\",\n    \"caffe2_nvrtc\",\n)\n\n# Files to keep in the server core even if they match NVIDIA prefixes.\n# These are small Python modules or stubs, not the large runtime DLLs.\nNVIDIA_KEEP_IN_CORE = {\n    \"torch/cuda/nccl.py\",\n    \"torch/_inductor/codegen/cuda/cutlass_lib_extensions/cutlass_mock_imports/cuda/cudart.py\",\n}\n\n\ndef is_nvidia_file(rel_path: str) -> bool:\n    \"\"\"Check if a relative path belongs to the NVIDIA CUDA libs.\n\n    Identifies large NVIDIA runtime DLLs (.dll/.so) regardless of where\n    PyInstaller placed them. Excludes small Python stubs that happen to\n    share NVIDIA-related names.\n    \"\"\"\n    rel_lower = rel_path.lower().replace(\"\\\\\", \"/\")\n\n    # Never split out Python source files or small stubs\n    if rel_lower in NVIDIA_KEEP_IN_CORE:\n        return False\n\n    # Files under nvidia/ subdirectory tree (older torch layout)\n    if rel_lower.startswith(\"nvidia/\") or \"/nvidia/\" in rel_lower:\n        # Only DLLs/shared objects — not .py, .dist-info, etc.\n        if rel_lower.endswith((\".dll\", \".so\")):\n            return True\n        # Include entire nvidia/ namespace package tree\n        for part in rel_lower.split(\"/\"):\n            if part == \"nvidia\":\n                return True\n\n    # NVIDIA DLLs anywhere in the tree (e.g. _internal/torch/lib/cublas64_12.dll)\n    name = rel_lower.rsplit(\"/\", 1)[-1]\n    if name.endswith(\".dll\") or name.endswith(\".so\"):\n        name_no_ext = name.rsplit(\".\", 1)[0]\n        for prefix in NVIDIA_DLL_PREFIXES:\n            if name_no_ext.startswith(prefix):\n                return True\n\n    return False\n\n\ndef sha256_file(path: Path) -> str:\n    \"\"\"Compute SHA-256 hex digest of a file.\"\"\"\n    h = hashlib.sha256()\n    with open(path, \"rb\") as f:\n        while True:\n            chunk = f.read(1024 * 1024)\n            if not chunk:\n                break\n            h.update(chunk)\n    return h.hexdigest()\n\n\ndef package(\n    onedir_path: Path,\n    output_dir: Path,\n    cuda_libs_version: str,\n    torch_compat: str,\n):\n    output_dir.mkdir(parents=True, exist_ok=True)\n\n    # Collect all files in the onedir output, split into core vs nvidia\n    core_files = []\n    nvidia_files = []\n\n    for item in sorted(onedir_path.rglob(\"*\")):\n        if item.is_dir():\n            continue\n        rel = item.relative_to(onedir_path)\n        rel_str = str(rel)\n        if is_nvidia_file(rel_str):\n            nvidia_files.append((rel_str, item))\n        else:\n            core_files.append((rel_str, item))\n\n    core_size = sum(f.stat().st_size for _, f in core_files)\n    nvidia_size = sum(f.stat().st_size for _, f in nvidia_files)\n\n    print(f\"Input directory: {onedir_path}\")\n    print(f\"Core files:  {len(core_files)} ({core_size / (1024**2):.1f} MB)\")\n    print(f\"NVIDIA files: {len(nvidia_files)} ({nvidia_size / (1024**2):.1f} MB)\")\n\n    if not nvidia_files:\n        print(\n            f\"ERROR: No NVIDIA files found in {onedir_path}. \"\n            \"Refusing to create an empty CUDA libs archive.\",\n            file=sys.stderr,\n        )\n        print(\n            \"Make sure you built with --cuda and the NVIDIA packages are present.\",\n            file=sys.stderr,\n        )\n        sys.exit(1)\n\n    # Create server core archive\n    # Files are stored relative to the archive root (no parent directory prefix)\n    # so extracting to backends/cuda/ puts everything at the right level.\n    server_archive = output_dir / \"voicebox-server-cuda.tar.gz\"\n    print(f\"\\nCreating server core archive: {server_archive.name}\")\n    with tarfile.open(server_archive, \"w:gz\") as tar:\n        for rel_str, full_path in core_files:\n            tar.add(full_path, arcname=rel_str)\n    server_sha = sha256_file(server_archive)\n    (output_dir / \"voicebox-server-cuda.tar.gz.sha256\").write_text(\n        f\"{server_sha}  voicebox-server-cuda.tar.gz\\n\"\n    )\n    print(f\"  Size: {server_archive.stat().st_size / (1024**2):.1f} MB\")\n    print(f\"  SHA-256: {server_sha[:16]}...\")\n\n    # Create CUDA libs archive\n    cuda_libs_archive = output_dir / f\"cuda-libs-{cuda_libs_version}.tar.gz\"\n    print(f\"\\nCreating CUDA libs archive: {cuda_libs_archive.name}\")\n    with tarfile.open(cuda_libs_archive, \"w:gz\") as tar:\n        for rel_str, full_path in nvidia_files:\n            tar.add(full_path, arcname=rel_str)\n    cuda_sha = sha256_file(cuda_libs_archive)\n    (output_dir / f\"cuda-libs-{cuda_libs_version}.tar.gz.sha256\").write_text(\n        f\"{cuda_sha}  cuda-libs-{cuda_libs_version}.tar.gz\\n\"\n    )\n    print(f\"  Size: {cuda_libs_archive.stat().st_size / (1024**2):.1f} MB\")\n    print(f\"  SHA-256: {cuda_sha[:16]}...\")\n\n    # Write cuda-libs.json manifest\n    manifest = {\n        \"version\": cuda_libs_version,\n        \"torch_compat\": torch_compat,\n        \"archive\": cuda_libs_archive.name,\n        \"sha256\": cuda_sha,\n    }\n    manifest_path = output_dir / \"cuda-libs.json\"\n    manifest_path.write_text(json.dumps(manifest, indent=2) + \"\\n\")\n    print(f\"\\nManifest: {manifest_path.name}\")\n    print(json.dumps(manifest, indent=2))\n\n    # Summary\n    total_input = core_size + nvidia_size\n    total_output = server_archive.stat().st_size + cuda_libs_archive.stat().st_size\n    print(f\"\\nTotal input:  {total_input / (1024**3):.2f} GB\")\n    print(f\"Total output: {total_output / (1024**3):.2f} GB (compressed)\")\n    print(\n        f\"Server core:  {server_archive.stat().st_size / (1024**2):.1f} MB (redownloaded on app update)\"\n    )\n    print(\n        f\"CUDA libs:    {cuda_libs_archive.stat().st_size / (1024**2):.1f} MB (cached until CUDA toolkit bump)\"\n    )\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Package PyInstaller --onedir CUDA build into server + CUDA libs archives\"\n    )\n    parser.add_argument(\n        \"input\",\n        type=Path,\n        help=\"Path to PyInstaller --onedir output directory (e.g. backend/dist/voicebox-server-cuda/)\",\n    )\n    parser.add_argument(\n        \"--output\",\n        type=Path,\n        default=None,\n        help=\"Output directory for archives (default: same as input parent)\",\n    )\n    parser.add_argument(\n        \"--cuda-libs-version\",\n        type=str,\n        default=\"cu128-v1\",\n        help=\"Version string for the CUDA libs archive (default: cu128-v1)\",\n    )\n    parser.add_argument(\n        \"--torch-compat\",\n        type=str,\n        default=\">=2.7.0,<2.11.0\",\n        help=\"Torch version compatibility range (default: >=2.6.0,<2.11.0)\",\n    )\n    args = parser.parse_args()\n\n    if not args.input.is_dir():\n        print(f\"Error: {args.input} is not a directory\", file=sys.stderr)\n        print(\"Expected a PyInstaller --onedir output directory.\", file=sys.stderr)\n        sys.exit(1)\n\n    output_dir = args.output or args.input.parent\n    package(args.input, output_dir, args.cuda_libs_version, args.torch_compat)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/prepare-release.sh",
    "content": "#!/bin/bash\n\n# Script to prepare a signed release build\n# Usage: ./scripts/prepare-release.sh\n\nset -e\n\necho \"🔑 Checking for signing keys...\"\n\nif [ ! -f ~/.tauri/voicebox.key ]; then\n  echo \"❌ Private key not found at ~/.tauri/voicebox.key\"\n  echo \"Run: cd tauri && bun tauri signer generate -w ~/.tauri/voicebox.key\"\n  exit 1\nfi\n\nif [ ! -f ~/.tauri/voicebox.key.pub ]; then\n  echo \"❌ Public key not found at ~/.tauri/voicebox.key.pub\"\n  exit 1\nfi\n\necho \"✅ Signing keys found\"\necho \"\"\n\n# Check if public key is in tauri.conf.json\nif grep -q \"REPLACE_WITH_YOUR_PUBLIC_KEY\" tauri/src-tauri/tauri.conf.json; then\n  echo \"⚠️  Public key not configured in tauri.conf.json\"\n  echo \"\"\n  echo \"Add this to tauri/src-tauri/tauri.conf.json:\"\n  echo \"\"\n  cat ~/.tauri/voicebox.key.pub\n  echo \"\"\n  exit 1\nfi\n\necho \"🔧 Setting up environment...\"\n\nexport TAURI_SIGNING_PRIVATE_KEY=\"$(cat ~/.tauri/voicebox.key)\"\nexport TAURI_SIGNING_PRIVATE_KEY_PASSWORD=\"\"\n\necho \"✅ Environment configured\"\necho \"\"\n\necho \"📦 Building release...\"\necho \"\"\n\nbun run build\n\necho \"\"\necho \"✅ Release build complete!\"\necho \"\"\necho \"📂 Bundles created in: tauri/src-tauri/target/release/bundle/\"\necho \"\"\necho \"Next steps:\"\necho \"1. Create a GitHub release\"\necho \"2. Upload all files from the bundle directory\"\necho \"3. Create latest.json with update metadata\"\necho \"\"\n"
  },
  {
    "path": "scripts/setup-dev-sidecar.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Creates placeholder sidecar binaries for development mode.\n *\n * In dev mode, Tauri requires the sidecar binary files to exist at compile time,\n * even though developers typically run the Python server manually.\n *\n * This script creates minimal placeholder binaries that allow Tauri to compile.\n * The actual server should be started separately with `bun run dev:server`.\n */\n\nimport { execSync } from 'child_process';\nimport { existsSync, mkdirSync, statSync, writeFileSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst BINARIES_DIR = join(__dirname, '..', 'tauri', 'src-tauri', 'binaries');\n\n// Minimum size to consider a binary \"real\" (placeholder is ~256 bytes, real is MBs)\nconst MIN_REAL_BINARY_SIZE = 10000;\n\n// Get the current platform's target triple\nfunction getTargetTriple() {\n  try {\n    const triple = execSync('rustc --print host-tuple', { encoding: 'utf-8' }).trim();\n    return triple;\n  } catch {\n    // Fallback detection\n    const platform = process.platform;\n    const arch = process.arch;\n\n    if (platform === 'win32') {\n      return arch === 'x64' ? 'x86_64-pc-windows-msvc' : 'i686-pc-windows-msvc';\n    } else if (platform === 'darwin') {\n      return arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';\n    } else if (platform === 'linux') {\n      return arch === 'x64' ? 'x86_64-unknown-linux-gnu' : 'aarch64-unknown-linux-gnu';\n    }\n\n    throw new Error(`Unsupported platform: ${platform}/${arch}`);\n  }\n}\n\n// Create a minimal executable for the platform\nfunction createPlaceholderBinary(targetTriple) {\n  const isWindows = targetTriple.includes('windows');\n  const binaryName = `voicebox-server-${targetTriple}${isWindows ? '.exe' : ''}`;\n  const binaryPath = join(BINARIES_DIR, binaryName);\n\n  // Check if real binary already exists (larger than our placeholder)\n  if (existsSync(binaryPath)) {\n    try {\n      const stats = statSync(binaryPath);\n      if (stats.size > MIN_REAL_BINARY_SIZE) {\n        console.log(\n          `Real binary already exists: ${binaryName} (${(stats.size / 1024 / 1024).toFixed(1)} MB)`,\n        );\n        return;\n      }\n    } catch {\n      // File exists but can't stat - try to replace it\n    }\n  }\n\n  // Ensure binaries directory exists\n  if (!existsSync(BINARIES_DIR)) {\n    mkdirSync(BINARIES_DIR, { recursive: true });\n  }\n\n  if (isWindows) {\n    // Create a minimal valid Windows PE executable that exits with code 1\n    // This is the smallest valid PE that Windows will accept\n    const minimalPE = Buffer.from([\n      // DOS Header\n      0x4d,\n      0x5a,\n      0x90,\n      0x00,\n      0x03,\n      0x00,\n      0x00,\n      0x00,\n      0x04,\n      0x00,\n      0x00,\n      0x00,\n      0xff,\n      0xff,\n      0x00,\n      0x00,\n      0xb8,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x40,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x80,\n      0x00,\n      0x00,\n      0x00,\n      // DOS Stub\n      0x0e,\n      0x1f,\n      0xba,\n      0x0e,\n      0x00,\n      0xb4,\n      0x09,\n      0xcd,\n      0x21,\n      0xb8,\n      0x01,\n      0x4c,\n      0xcd,\n      0x21,\n      0x54,\n      0x68,\n      0x69,\n      0x73,\n      0x20,\n      0x70,\n      0x72,\n      0x6f,\n      0x67,\n      0x72,\n      0x61,\n      0x6d,\n      0x20,\n      0x63,\n      0x61,\n      0x6e,\n      0x6e,\n      0x6f,\n      0x74,\n      0x20,\n      0x62,\n      0x65,\n      0x20,\n      0x72,\n      0x75,\n      0x6e,\n      0x20,\n      0x69,\n      0x6e,\n      0x20,\n      0x44,\n      0x4f,\n      0x53,\n      0x20,\n      0x6d,\n      0x6f,\n      0x64,\n      0x65,\n      0x2e,\n      0x0d,\n      0x0d,\n      0x0a,\n      0x24,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      // PE Signature\n      0x50,\n      0x45,\n      0x00,\n      0x00,\n      // COFF Header (x64)\n      0x64,\n      0x86, // Machine: AMD64\n      0x01,\n      0x00, // NumberOfSections: 1\n      0x00,\n      0x00,\n      0x00,\n      0x00, // TimeDateStamp\n      0x00,\n      0x00,\n      0x00,\n      0x00, // PointerToSymbolTable\n      0x00,\n      0x00,\n      0x00,\n      0x00, // NumberOfSymbols\n      0xf0,\n      0x00, // SizeOfOptionalHeader\n      0x22,\n      0x00, // Characteristics: EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE\n      // Optional Header (PE32+)\n      0x0b,\n      0x02, // Magic: PE32+\n      0x00,\n      0x00, // Linker version\n      0x00,\n      0x00,\n      0x00,\n      0x00, // SizeOfCode\n      0x00,\n      0x00,\n      0x00,\n      0x00, // SizeOfInitializedData\n      0x00,\n      0x00,\n      0x00,\n      0x00, // SizeOfUninitializedData\n      0x00,\n      0x10,\n      0x00,\n      0x00, // AddressOfEntryPoint\n      0x00,\n      0x00,\n      0x00,\n      0x00, // BaseOfCode\n      0x00,\n      0x00,\n      0x00,\n      0x40,\n      0x01,\n      0x00,\n      0x00,\n      0x00, // ImageBase\n      0x00,\n      0x10,\n      0x00,\n      0x00, // SectionAlignment\n      0x00,\n      0x02,\n      0x00,\n      0x00, // FileAlignment\n      0x06,\n      0x00,\n      0x00,\n      0x00, // OS version\n      0x00,\n      0x00,\n      0x00,\n      0x00, // Image version\n      0x06,\n      0x00,\n      0x00,\n      0x00, // Subsystem version\n      0x00,\n      0x00,\n      0x00,\n      0x00, // Win32VersionValue\n      0x00,\n      0x20,\n      0x00,\n      0x00, // SizeOfImage\n      0x00,\n      0x02,\n      0x00,\n      0x00, // SizeOfHeaders\n      0x00,\n      0x00,\n      0x00,\n      0x00, // CheckSum\n      0x03,\n      0x00, // Subsystem: CONSOLE\n      0x60,\n      0x01, // DllCharacteristics\n      // Stack/Heap sizes (8 bytes each for PE32+)\n      0x00,\n      0x00,\n      0x10,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x10,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00,\n      0x00, // LoaderFlags\n      0x10,\n      0x00,\n      0x00,\n      0x00, // NumberOfRvaAndSizes\n    ]);\n\n    // Pad to 512 bytes minimum for valid PE\n    const paddedPE = Buffer.alloc(512);\n    minimalPE.copy(paddedPE);\n    writeFileSync(binaryPath, paddedPE);\n  } else {\n    // Create a minimal shell script for Unix-like systems\n    const script = `#!/bin/sh\necho \"[voicebox-server] Dev mode placeholder - start the real server with: bun run dev:server\"\nexit 1\n`;\n    writeFileSync(binaryPath, script, { mode: 0o755 });\n  }\n\n  console.log(`Created dev placeholder: ${binaryName}`);\n}\n\nfunction main() {\n  const targetTriple = getTargetTriple();\n  createPlaceholderBinary(targetTriple);\n}\n\nmain();\n"
  },
  {
    "path": "scripts/test_download_progress.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nTest script to observe exactly how HuggingFace reports download progress\nfor each TTS model. Doesn't load models — just downloads and tracks tqdm.\n\nUsage:\n    backend/venv/bin/python scripts/test_download_progress.py qwen\n    backend/venv/bin/python scripts/test_download_progress.py luxtts\n    backend/venv/bin/python scripts/test_download_progress.py chatterbox\n\nAdd --delete to clear cache first and force a real download:\n    backend/venv/bin/python scripts/test_download_progress.py chatterbox --delete\n\"\"\"\n\nimport os\nimport shutil\nimport sys\nimport time\nimport threading\nfrom pathlib import Path\nfrom contextlib import contextmanager\n\n# ─── Configuration ────────────────────────────────────────────────────────────\n\nMODELS = {\n    \"qwen\": {\n        \"repo_id\": \"Qwen/Qwen3-TTS-12Hz-1.7B-Base\",\n        \"method\": \"from_pretrained\",\n        \"description\": \"Qwen TTS 1.7B (uses transformers from_pretrained)\",\n    },\n    \"luxtts\": {\n        \"repo_id\": \"YatharthS/LuxTTS\",\n        \"method\": \"snapshot_download\",\n        \"description\": \"LuxTTS (uses snapshot_download)\",\n    },\n    \"chatterbox\": {\n        \"repo_id\": \"ResembleAI/chatterbox\",\n        \"method\": \"snapshot_download\",\n        \"allow_patterns\": [\n            \"ve.pt\",\n            \"t3_mtl23ls_v2.safetensors\",\n            \"s3gen.pt\",\n            \"grapheme_mtl_merged_expanded_v1.json\",\n            \"conds.pt\",\n            \"Cangjie5_TC.json\",\n        ],\n        \"description\": \"Chatterbox Multilingual (uses snapshot_download with allow_patterns)\",\n    },\n}\n\n\n# ─── Progress tracking (mirrors our HFProgressTracker) ────────────────────────\n\nclass ProgressSpy:\n    \"\"\"Intercepts tqdm to see exactly what HF reports.\"\"\"\n\n    def __init__(self):\n        self._lock = threading.Lock()\n        self.events = []  # List of dicts: {time, type, ...}\n        self._original_tqdm_class = None\n        self._original_tqdm_auto = None\n        self._patched_modules = {}\n        self._hf_tqdm_original_update = None\n        self._start_time = None\n\n    def _elapsed(self):\n        return time.time() - self._start_time if self._start_time else 0\n\n    def _log(self, event_type, **kwargs):\n        entry = {\"time\": f\"{self._elapsed():.1f}s\", \"type\": event_type, **kwargs}\n        self.events.append(entry)\n\n        # Live print\n        parts = [f\"[{entry['time']:>7s}] {event_type:>10s}\"]\n        for k, v in kwargs.items():\n            if k in (\"current\", \"total\") and isinstance(v, (int, float)) and v > 1_000_000:\n                parts.append(f\"{k}={v / 1_000_000:.1f}MB\")\n            else:\n                parts.append(f\"{k}={v}\")\n        print(\"  \".join(parts), flush=True)\n\n    def _create_tracked_tqdm_class(self):\n        spy = self\n        original_tqdm = self._original_tqdm_class\n\n        class SpyTqdm(original_tqdm):\n            def __init__(self, *args, **kwargs):\n                desc = kwargs.get(\"desc\", \"\")\n                if not desc and args:\n                    first_arg = args[0]\n                    if isinstance(first_arg, str):\n                        desc = first_arg\n\n                filename = \"\"\n                if desc:\n                    if \":\" in desc:\n                        filename = desc.split(\":\")[0].strip()\n                    else:\n                        filename = desc.strip()\n\n                # Filter out non-standard kwargs\n                tqdm_kwargs = {\n                    'iterable', 'desc', 'total', 'leave', 'file', 'ncols',\n                    'mininterval', 'maxinterval', 'miniters', 'ascii', 'disable',\n                    'unit', 'unit_scale', 'dynamic_ncols', 'smoothing',\n                    'bar_format', 'initial', 'position', 'postfix',\n                    'unit_divisor', 'write_bytes', 'lock_args', 'nrows',\n                    'colour', 'color', 'delay', 'gui', 'disable_default', 'pos',\n                }\n                filtered_kwargs = {k: v for k, v in kwargs.items() if k in tqdm_kwargs}\n\n                try:\n                    super().__init__(*args, **filtered_kwargs)\n                except TypeError:\n                    super().__init__(*args, **kwargs)\n\n                self._spy_filename = filename or \"unknown\"\n                total = getattr(self, \"total\", None)\n\n                spy._log(\n                    \"INIT\",\n                    filename=self._spy_filename,\n                    total=total or 0,\n                    unit=kwargs.get(\"unit\", \"?\"),\n                    unit_scale=kwargs.get(\"unit_scale\", False),\n                    disable=kwargs.get(\"disable\", False),\n                )\n\n            def update(self, n=1):\n                result = super().update(n)\n\n                current = getattr(self, \"n\", 0)\n                total = getattr(self, \"total\", 0)\n                filename = self._spy_filename\n\n                spy._log(\n                    \"UPDATE\",\n                    filename=filename,\n                    n=n,\n                    current=current,\n                    total=total or 0,\n                    pct=f\"{100 * current / total:.1f}%\" if total else \"?\",\n                )\n\n                return result\n\n            def close(self):\n                spy._log(\"CLOSE\", filename=self._spy_filename)\n                return super().close()\n\n        return SpyTqdm\n\n    @contextmanager\n    def patch(self):\n        \"\"\"Context manager that patches tqdm globally — same as HFProgressTracker.\"\"\"\n        self._start_time = time.time()\n\n        try:\n            import tqdm as tqdm_module\n            self._original_tqdm_class = tqdm_module.tqdm\n        except ImportError:\n            yield\n            return\n\n        tracked_tqdm = self._create_tracked_tqdm_class()\n\n        # Patch tqdm.tqdm\n        tqdm_module.tqdm = tracked_tqdm\n\n        # Patch tqdm.auto.tqdm\n        self._original_tqdm_auto = None\n        if hasattr(tqdm_module, \"auto\") and hasattr(tqdm_module.auto, \"tqdm\"):\n            self._original_tqdm_auto = tqdm_module.auto.tqdm\n            tqdm_module.auto.tqdm = tracked_tqdm\n\n        # Patch in sys.modules (same as HFProgressTracker)\n        tqdm_attr_names = ['tqdm', 'base_tqdm', 'old_tqdm']\n        patched_count = 0\n\n        for module_name in list(sys.modules.keys()):\n            if \"huggingface\" in module_name or module_name.startswith(\"tqdm\"):\n                try:\n                    module = sys.modules[module_name]\n                    for attr_name in tqdm_attr_names:\n                        if hasattr(module, attr_name):\n                            attr = getattr(module, attr_name)\n                            is_tqdm_class = (\n                                attr is self._original_tqdm_class\n                                or (self._original_tqdm_auto and attr is self._original_tqdm_auto)\n                                or (\n                                    hasattr(attr, \"__name__\")\n                                    and attr.__name__ == \"tqdm\"\n                                    and hasattr(attr, \"update\")\n                                )\n                            )\n                            if is_tqdm_class:\n                                key = f\"{module_name}.{attr_name}\"\n                                self._patched_modules[key] = (module, attr_name, attr)\n                                setattr(module, attr_name, tracked_tqdm)\n                                patched_count += 1\n                except (AttributeError, TypeError):\n                    pass\n\n        # Monkey-patch HF's tqdm.update (same as HFProgressTracker)\n        try:\n            from huggingface_hub.utils import tqdm as hf_tqdm_module\n            if hasattr(hf_tqdm_module, 'tqdm'):\n                hf_tqdm_class = hf_tqdm_module.tqdm\n                self._hf_tqdm_original_update = hf_tqdm_class.update\n                spy = self\n\n                def patched_update(tqdm_self, n=1):\n                    result = spy._hf_tqdm_original_update(tqdm_self, n)\n                    desc = getattr(tqdm_self, 'desc', '') or ''\n                    current = getattr(tqdm_self, 'n', 0)\n                    total = getattr(tqdm_self, 'total', 0) or 0\n\n                    spy._log(\n                        \"HF_UPDATE\",\n                        desc=desc,\n                        current=current,\n                        total=total,\n                        pct=f\"{100 * current / total:.1f}%\" if total else \"?\",\n                    )\n                    return result\n\n                hf_tqdm_class.update = patched_update\n                patched_count += 1\n        except (ImportError, AttributeError):\n            pass\n\n        print(f\"\\n=== Patched {patched_count} tqdm references ===\\n\", flush=True)\n\n        try:\n            yield\n        finally:\n            # Restore everything\n            import tqdm as tqdm_module\n            tqdm_module.tqdm = self._original_tqdm_class\n            if self._original_tqdm_auto:\n                tqdm_module.auto.tqdm = self._original_tqdm_auto\n            for key, (module, attr_name, original) in self._patched_modules.items():\n                try:\n                    setattr(module, attr_name, original)\n                except (AttributeError, TypeError):\n                    pass\n            if self._hf_tqdm_original_update:\n                try:\n                    from huggingface_hub.utils import tqdm as hf_tqdm_module\n                    if hasattr(hf_tqdm_module, 'tqdm'):\n                        hf_tqdm_module.tqdm.update = self._hf_tqdm_original_update\n                except (ImportError, AttributeError):\n                    pass\n\n    def summary(self):\n        print(\"\\n\" + \"=\" * 70)\n        print(\"SUMMARY\")\n        print(\"=\" * 70)\n\n        inits = [e for e in self.events if e[\"type\"] == \"INIT\"]\n        updates = [e for e in self.events if e[\"type\"] in (\"UPDATE\", \"HF_UPDATE\")]\n\n        print(f\"\\ntqdm bars created: {len(inits)}\")\n        for e in inits:\n            print(f\"  - {e.get('filename', '?'):40s} total={e.get('total', '?')}\")\n\n        print(f\"\\nTotal update calls: {len(updates)}\")\n\n        # Group updates by filename\n        by_file = {}\n        for e in updates:\n            fn = e.get(\"filename\") or e.get(\"desc\", \"unknown\")\n            if fn not in by_file:\n                by_file[fn] = []\n            by_file[fn].append(e)\n\n        for fn, evts in by_file.items():\n            max_current = max(e.get(\"current\", 0) for e in evts)\n            max_total = max(e.get(\"total\", 0) for e in evts)\n            print(f\"\\n  {fn}:\")\n            print(f\"    updates: {len(evts)}\")\n            print(f\"    max current: {max_current:,}\")\n            print(f\"    max total:   {max_total:,}\")\n            if max_total > 0 and max_current > 0:\n                print(f\"    final pct:   {100 * max_current / max_total:.1f}%\")\n            else:\n                print(f\"    final pct:   NO PROGRESS REPORTED\")\n\n\n# ─── Delete cache ─────────────────────────────────────────────────────────────\n\ndef delete_cache(repo_id: str):\n    from huggingface_hub import constants as hf_constants\n    cache_dir = Path(hf_constants.HF_HUB_CACHE)\n    repo_cache = cache_dir / (\"models--\" + repo_id.replace(\"/\", \"--\"))\n    if repo_cache.exists():\n        print(f\"Deleting cache: {repo_cache}\")\n        shutil.rmtree(repo_cache)\n        print(\"Deleted.\")\n    else:\n        print(f\"No cache found at {repo_cache}\")\n\n\n# ─── Download functions ───────────────────────────────────────────────────────\n\ndef download_qwen(spy: ProgressSpy):\n    \"\"\"Mirrors how pytorch_backend.py downloads Qwen.\"\"\"\n    from transformers import AutoModel\n    repo_id = MODELS[\"qwen\"][\"repo_id\"]\n\n    print(f\"Downloading {repo_id} via AutoModel.from_pretrained...\")\n    with spy.patch():\n        # This is what Qwen3TTSModel.from_pretrained does under the hood\n        from huggingface_hub import snapshot_download\n        snapshot_download(repo_id)\n\n\ndef download_luxtts(spy: ProgressSpy):\n    \"\"\"Mirrors how luxtts_backend.py downloads LuxTTS.\"\"\"\n    from huggingface_hub import snapshot_download\n    repo_id = MODELS[\"luxtts\"][\"repo_id\"]\n\n    print(f\"Downloading {repo_id} via snapshot_download...\")\n    with spy.patch():\n        snapshot_download(repo_id)\n\n\ndef download_chatterbox(spy: ProgressSpy):\n    \"\"\"Mirrors how chatterbox_backend.py downloads Chatterbox.\"\"\"\n    from huggingface_hub import snapshot_download\n    cfg = MODELS[\"chatterbox\"]\n\n    print(f\"Downloading {cfg['repo_id']} via snapshot_download with allow_patterns...\")\n    with spy.patch():\n        snapshot_download(\n            repo_id=cfg[\"repo_id\"],\n            repo_type=\"model\",\n            revision=\"main\",\n            allow_patterns=cfg[\"allow_patterns\"],\n            token=os.getenv(\"HF_TOKEN\"),\n        )\n\n\n# ─── Main ─────────────────────────────────────────────────────────────────────\n\ndef main():\n    if len(sys.argv) < 2 or sys.argv[1] not in MODELS:\n        print(f\"Usage: {sys.argv[0]} <{'|'.join(MODELS.keys())}> [--delete]\")\n        sys.exit(1)\n\n    model_key = sys.argv[1]\n    should_delete = \"--delete\" in sys.argv\n    cfg = MODELS[model_key]\n\n    print(f\"\\n{'=' * 70}\")\n    print(f\"Testing download progress for: {cfg['description']}\")\n    print(f\"Repo: {cfg['repo_id']}\")\n    print(f\"Method: {cfg['method']}\")\n    print(f\"{'=' * 70}\\n\")\n\n    if should_delete:\n        delete_cache(cfg[\"repo_id\"])\n        print()\n\n    spy = ProgressSpy()\n\n    dispatch = {\n        \"qwen\": download_qwen,\n        \"luxtts\": download_luxtts,\n        \"chatterbox\": download_chatterbox,\n    }\n\n    try:\n        dispatch[model_key](spy)\n    except Exception as e:\n        print(f\"\\n!!! Download failed: {e}\")\n\n    spy.summary()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/update-icons.sh",
    "content": "#!/bin/bash\nset -e\n\n# Complete Icon Update Script\n# Updates both Liquid Glass icon bundle AND all platform fallback icons from exports\n\ncd \"$(dirname \"$0\")/..\"\n\nEXPORTS_DIR=\"tauri/assets/voicebox_exports\"\nICON_BUNDLE=\"tauri/assets/voicebox.icon\"\nASSETS_DIR=\"$ICON_BUNDLE/Assets\"\nICONS_DIR=\"tauri/src-tauri/icons\"\nLANDING_LOGO=\"landing/public/voicebox-logo.png\"\nLANDING_PUBLIC=\"landing/public\"\nSOURCE_ICON=\"$EXPORTS_DIR/voicebox-iOS-Dark-1024x1024@1x.png\"\n\necho \"🎨 Updating all Voicebox icons from exports...\"\necho \"\"\n\n# Check if source exists\nif [ ! -f \"$SOURCE_ICON\" ]; then\n  echo \"Error: Source icon not found at $SOURCE_ICON\"\n  exit 1\nfi\n\n# ============================================\n# PART 1: Compile Liquid Glass Icon Bundle\n# ============================================\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"📦 Part 1: Compiling Liquid Glass Icon Bundle\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\n\necho \"Compiling voicebox.icon with actool...\"\n# Remove old generated icons to force rebuild\nrm -rf tauri/src-tauri/gen/*.icns tauri/src-tauri/gen/Assets.car 2>/dev/null\n\ncd tauri/src-tauri\ncargo build 2>/dev/null || echo \"  ⚠ Cargo build had warnings (this is normal)\"\ncd ../..\n\nif [ -f \"tauri/src-tauri/gen/voicebox.icns\" ]; then\n  echo \"  ✓ voicebox.icns generated\"\nelse\n  echo \"  ⚠ Warning: voicebox.icns not generated (will use fallback)\"\nfi\n\necho \"\"\n\n# ============================================\n# PART 2: Generate Platform Fallback Icons\n# ============================================\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"🖼️  Part 2: Generating Platform Fallback Icons\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\n\nmkdir -p \"$ICONS_DIR\"\n\n# macOS & Desktop Icons\necho \"Generating macOS/Desktop icons...\"\nsips -s format png -z 32 32 \"$SOURCE_ICON\" --out \"$ICONS_DIR/32x32.png\" 2>/dev/null\nsips -s format png -z 64 64 \"$SOURCE_ICON\" --out \"$ICONS_DIR/64x64.png\" 2>/dev/null\nsips -s format png -z 128 128 \"$SOURCE_ICON\" --out \"$ICONS_DIR/128x128.png\" 2>/dev/null\nsips -s format png -z 256 256 \"$SOURCE_ICON\" --out \"$ICONS_DIR/128x128@2x.png\" 2>/dev/null\nsips -s format png -z 512 512 \"$SOURCE_ICON\" --out \"$ICONS_DIR/icon.png\" 2>/dev/null\n\n# Copy Liquid Glass compiled ICNS or generate fallback\necho \"Copying icon.icns...\"\nif [ -f \"tauri/src-tauri/gen/voicebox.icns\" ]; then\n  cp tauri/src-tauri/gen/voicebox.icns \"$ICONS_DIR/icon.icns\"\n  echo \"  ✓ Copied Liquid Glass compiled icon.icns\"\nelse\n  echo \"  ⚠ Liquid Glass icon not found, generating fallback icon.icns...\"\nmkdir -p /tmp/voicebox-iconset.iconset\nsips -s format png -z 16 16 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_16x16.png 2>/dev/null\nsips -s format png -z 32 32 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_16x16@2x.png 2>/dev/null\nsips -s format png -z 32 32 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_32x32.png 2>/dev/null\nsips -s format png -z 64 64 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_32x32@2x.png 2>/dev/null\nsips -s format png -z 128 128 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_128x128.png 2>/dev/null\nsips -s format png -z 256 256 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_128x128@2x.png 2>/dev/null\nsips -s format png -z 256 256 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_256x256.png 2>/dev/null\nsips -s format png -z 512 512 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_256x256@2x.png 2>/dev/null\nsips -s format png -z 512 512 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_512x512.png 2>/dev/null\n  sips -s format png -z 1024 1024 \"$SOURCE_ICON\" --out /tmp/voicebox-iconset.iconset/icon_512x512@2x.png 2>/dev/null\n  iconutil -c icns /tmp/voicebox-iconset.iconset -o \"$ICONS_DIR/icon.icns\"\n  rm -rf /tmp/voicebox-iconset.iconset\n  echo \"  ✓ Generated fallback icon.icns\"\nfi\n\n# Windows Square Logos\necho \"Generating Windows icons...\"\nfor size in 30 44 71 89 107 142 150 284 310; do\n  sips -s format png -z $size $size \"$SOURCE_ICON\" --out \"$ICONS_DIR/Square${size}x${size}Logo.png\" 2>/dev/null\ndone\nsips -s format png -z 50 50 \"$SOURCE_ICON\" --out \"$ICONS_DIR/StoreLogo.png\" 2>/dev/null\n\n# Windows icon.ico (multi-size ICO file)\necho \"Generating Windows icon.ico...\"\nif command -v convert &> /dev/null; then\n  # Create temporary PNG files at different sizes for ICO\n  # Windows typically uses: 16x16, 32x32, 48x48, 256x256\n  sips -s format png -z 16 16 \"$SOURCE_ICON\" --out /tmp/icon-16.png 2>/dev/null\n  sips -s format png -z 32 32 \"$SOURCE_ICON\" --out /tmp/icon-32.png 2>/dev/null\n  sips -s format png -z 48 48 \"$SOURCE_ICON\" --out /tmp/icon-48.png 2>/dev/null\n  sips -s format png -z 256 256 \"$SOURCE_ICON\" --out /tmp/icon-256.png 2>/dev/null\n  # Combine into proper multi-size ICO file\n  convert /tmp/icon-16.png /tmp/icon-32.png /tmp/icon-48.png /tmp/icon-256.png \"$ICONS_DIR/icon.ico\" 2>/dev/null\n  rm -f /tmp/icon-16.png /tmp/icon-32.png /tmp/icon-48.png /tmp/icon-256.png 2>/dev/null\n  echo \"  ✓ Generated Windows icon.ico\"\nelse\n  # Fallback: use sips to create a basic ICO (single size)\n  echo \"  ⚠ ImageMagick not found - generating basic icon.ico (single size)\"\n  sips -s format ico -z 256 256 \"$SOURCE_ICON\" --out \"$ICONS_DIR/icon.ico\" 2>/dev/null || echo \"  ⚠ Failed to generate icon.ico (sips may not support ICO format)\"\nfi\n\n# iOS Icons\necho \"Generating iOS icons...\"\nmkdir -p \"$ICONS_DIR/ios\"\n\ndeclare -A ios_sizes=(\n  [\"AppIcon-20x20@1x.png\"]=\"20\"\n  [\"AppIcon-20x20@2x.png\"]=\"40\"\n  [\"AppIcon-20x20@2x-1.png\"]=\"40\"\n  [\"AppIcon-20x20@3x.png\"]=\"60\"\n  [\"AppIcon-29x29@1x.png\"]=\"29\"\n  [\"AppIcon-29x29@2x.png\"]=\"58\"\n  [\"AppIcon-29x29@2x-1.png\"]=\"58\"\n  [\"AppIcon-29x29@3x.png\"]=\"87\"\n  [\"AppIcon-40x40@1x.png\"]=\"40\"\n  [\"AppIcon-40x40@2x.png\"]=\"80\"\n  [\"AppIcon-40x40@2x-1.png\"]=\"80\"\n  [\"AppIcon-40x40@3x.png\"]=\"120\"\n  [\"AppIcon-60x60@2x.png\"]=\"120\"\n  [\"AppIcon-60x60@3x.png\"]=\"180\"\n  [\"AppIcon-76x76@1x.png\"]=\"76\"\n  [\"AppIcon-76x76@2x.png\"]=\"152\"\n  [\"AppIcon-83.5x83.5@2x.png\"]=\"167\"\n  [\"AppIcon-512@2x.png\"]=\"1024\"\n)\n\nfor filename in \"${!ios_sizes[@]}\"; do\n  size=\"${ios_sizes[$filename]}\"\n  sips -s format png -z $size $size \"$SOURCE_ICON\" --out \"$ICONS_DIR/ios/$filename\" 2>/dev/null\ndone\n\n# Android Icons\necho \"Generating Android icons...\"\nmkdir -p \"$ICONS_DIR/android/mipmap-mdpi\"\nmkdir -p \"$ICONS_DIR/android/mipmap-hdpi\"\nmkdir -p \"$ICONS_DIR/android/mipmap-xhdpi\"\nmkdir -p \"$ICONS_DIR/android/mipmap-xxhdpi\"\nmkdir -p \"$ICONS_DIR/android/mipmap-xxxhdpi\"\n\nsips -s format png -z 48 48 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-mdpi/ic_launcher.png\" 2>/dev/null\nsips -s format png -z 48 48 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-mdpi/ic_launcher_round.png\" 2>/dev/null\nsips -s format png -z 48 48 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-mdpi/ic_launcher_foreground.png\" 2>/dev/null\n\nsips -s format png -z 72 72 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-hdpi/ic_launcher.png\" 2>/dev/null\nsips -s format png -z 72 72 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-hdpi/ic_launcher_round.png\" 2>/dev/null\nsips -s format png -z 72 72 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-hdpi/ic_launcher_foreground.png\" 2>/dev/null\n\nsips -s format png -z 96 96 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xhdpi/ic_launcher.png\" 2>/dev/null\nsips -s format png -z 96 96 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xhdpi/ic_launcher_round.png\" 2>/dev/null\nsips -s format png -z 96 96 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xhdpi/ic_launcher_foreground.png\" 2>/dev/null\n\nsips -s format png -z 144 144 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xxhdpi/ic_launcher.png\" 2>/dev/null\nsips -s format png -z 144 144 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xxhdpi/ic_launcher_round.png\" 2>/dev/null\nsips -s format png -z 144 144 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xxhdpi/ic_launcher_foreground.png\" 2>/dev/null\n\nsips -s format png -z 192 192 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xxxhdpi/ic_launcher.png\" 2>/dev/null\nsips -s format png -z 192 192 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xxxhdpi/ic_launcher_round.png\" 2>/dev/null\nsips -s format png -z 192 192 \"$SOURCE_ICON\" --out \"$ICONS_DIR/android/mipmap-xxxhdpi/ic_launcher_foreground.png\" 2>/dev/null\n\n# Landing Page Logo & Favicon\necho \"Generating landing page logo...\"\nmkdir -p \"$LANDING_PUBLIC\"\nsips -s format png -z 1024 1024 \"$SOURCE_ICON\" --out \"$LANDING_LOGO\" 2>/dev/null\n\necho \"Generating landing page favicon...\"\n# Generate favicon.png (32x32 is standard for favicons)\nsips -s format png -z 32 32 \"$SOURCE_ICON\" --out \"$LANDING_PUBLIC/favicon.png\" 2>/dev/null\n\n# Generate proper multi-size favicon.ico using ImageMagick if available\nif command -v convert &> /dev/null; then\n  # Create temporary PNG files at different sizes for ICO\n  sips -s format png -z 16 16 \"$SOURCE_ICON\" --out /tmp/favicon-16.png 2>/dev/null\n  sips -s format png -z 32 32 \"$SOURCE_ICON\" --out /tmp/favicon-32.png 2>/dev/null\n  # Combine into proper multi-size ICO file\n  convert /tmp/favicon-16.png /tmp/favicon-32.png \"$LANDING_PUBLIC/favicon.ico\" 2>/dev/null\n  rm -f /tmp/favicon-16.png /tmp/favicon-32.png 2>/dev/null\n  echo \"  ✓ Generated proper multi-size favicon.ico\"\nelse\n  # Fallback: skip ICO if ImageMagick not available (PNG will be used)\n  echo \"  ⚠ ImageMagick not found - skipping favicon.ico (using favicon.png instead)\"\nfi\n\n# Also generate apple-touch-icon (180x180 for iOS)\nsips -s format png -z 180 180 \"$SOURCE_ICON\" --out \"$LANDING_PUBLIC/apple-touch-icon.png\" 2>/dev/null\n\necho \"\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"✅ All icons updated successfully!\"\necho \"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\"\necho \"\"\necho \"Updated:\"\necho \"  ✓ Liquid Glass icon bundle with all appearance variants\"\necho \"  ✓ macOS/Desktop fallback icons\"\necho \"  ✓ Windows Square logos\"\necho \"  ✓ Windows icon.ico (multi-size)\"\necho \"  ✓ iOS AppIcons (18 sizes)\"\necho \"  ✓ Android mipmap icons (5 densities)\"\necho \"  ✓ Landing page logo\"\necho \"  ✓ Landing page favicon\"\necho \"\"\necho \"Next: Rebuild the app with 'cd tauri && bun run tauri build'\"\n"
  },
  {
    "path": "tauri/assets/voicebox.icon/icon.json",
    "content": "{\n  \"fill\": \"automatic\",\n  \"groups\": [\n    {\n      \"layers\": [\n        {\n          \"image-name\": \"Voicebox_Microphone.png\",\n          \"name\": \"Voicebox_Microphone\",\n          \"position\": {\n            \"scale\": 0.36,\n            \"translation-in-points\": [0.140625, 270.1875]\n          }\n        }\n      ],\n      \"shadow\": {\n        \"kind\": \"neutral\",\n        \"opacity\": 0.5\n      },\n      \"translucency\": {\n        \"enabled\": true,\n        \"value\": 0.5\n      }\n    }\n  ],\n  \"supported-platforms\": {\n    \"circles\": [\"watchOS\"],\n    \"squares\": \"shared\"\n  }\n}\n"
  },
  {
    "path": "tauri/index.html",
    "content": "<!doctype html>\n<html lang=\"en\" class=\"dark\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>voicebox</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "tauri/package.json",
    "content": "{\n  \"name\": \"@voicebox/tauri\",\n  \"private\": true,\n  \"version\": \"0.3.1\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"vite build\",\n    \"tauri\": \"tauri\"\n  },\n  \"dependencies\": {\n    \"@tauri-apps/api\": \"^2.0.0\",\n    \"@tauri-apps/plugin-dialog\": \"^2.0.0\",\n    \"@tauri-apps/plugin-fs\": \"^2.0.0\",\n    \"@tauri-apps/plugin-process\": \"^2.0.0\",\n    \"@tauri-apps/plugin-shell\": \"^2.0.0\",\n    \"@tauri-apps/plugin-updater\": \"^2.0.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/vite\": \"^4.1.18\",\n    \"@tauri-apps/cli\": \"^2.0.0\",\n    \"@types/react\": \"^18.3.0\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@vitejs/plugin-react\": \"^4.3.0\",\n    \"tailwindcss\": \"^4.1.18\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"typescript\": \"^5.6.0\",\n    \"vite\": \"^5.4.0\"\n  }\n}\n"
  },
  {
    "path": "tauri/src/main.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\n// import { ReactQueryDevtools } from '@tanstack/react-query-devtools';\nimport App from '@/App';\n// Import CSS from app directory using alias so Tailwind can scan the source files\nimport '@/index.css';\nimport { PlatformProvider } from '@/platform/PlatformContext';\nimport { tauriPlatform } from './platform';\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 1000 * 60 * 5, // 5 minutes\n      gcTime: 1000 * 60 * 10, // 10 minutes\n      retry: 1,\n      refetchOnWindowFocus: false,\n    },\n  },\n});\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <React.StrictMode>\n    <QueryClientProvider client={queryClient}>\n      <PlatformProvider platform={tauriPlatform}>\n        <App />\n        {/* <ReactQueryDevtools initialIsOpen={false} /> */}\n      </PlatformProvider>\n    </QueryClientProvider>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "tauri/src/platform/audio.ts",
    "content": "import { invoke } from '@tauri-apps/api/core';\nimport type { PlatformAudio, AudioDevice } from '@/platform/types';\n\nexport const tauriAudio: PlatformAudio = {\n  isSystemAudioSupported(): boolean {\n    // This will be checked dynamically via invoke\n    return true; // Tauri supports it, but actual support depends on platform\n  },\n\n  async startSystemAudioCapture(maxDurationSecs: number): Promise<void> {\n    await invoke('start_system_audio_capture', {\n      maxDurationSecs,\n    });\n  },\n\n  async stopSystemAudioCapture(): Promise<Blob> {\n    const base64Data = await invoke<string>('stop_system_audio_capture');\n\n    // Convert base64 to Blob\n    const binaryString = atob(base64Data);\n    const bytes = new Uint8Array(binaryString.length);\n    for (let i = 0; i < binaryString.length; i++) {\n      bytes[i] = binaryString.charCodeAt(i);\n    }\n\n    return new Blob([bytes], { type: 'audio/wav' });\n  },\n\n  async listOutputDevices(): Promise<AudioDevice[]> {\n    return await invoke<AudioDevice[]>('list_audio_output_devices');\n  },\n\n  async playToDevices(audioData: Uint8Array, deviceIds: string[]): Promise<void> {\n    await invoke('play_audio_to_devices', {\n      audioData: Array.from(audioData),\n      deviceIds,\n    });\n  },\n\n  stopPlayback(): void {\n    invoke('stop_audio_playback').catch((error) => {\n      console.error('Failed to stop audio playback:', error);\n    });\n  },\n};\n"
  },
  {
    "path": "tauri/src/platform/filesystem.ts",
    "content": "import type { FileFilter, PlatformFilesystem } from '@/platform/types';\n\nexport const tauriFilesystem: PlatformFilesystem = {\n  async saveFile(filename: string, blob: Blob, filters?: FileFilter[]) {\n    const { save } = await import('@tauri-apps/plugin-dialog');\n    const { writeFile } = await import('@tauri-apps/plugin-fs');\n\n    const filePath = await save({\n      defaultPath: filename,\n      filters: filters || [],\n    });\n\n    if (!filePath) return; // User cancelled the dialog\n\n    const resolvedPath =\n      typeof filePath === 'string' ? filePath : (filePath as { path: string }).path;\n\n    if (!resolvedPath) {\n      throw new Error('Failed to resolve save path from dialog');\n    }\n\n    const arrayBuffer = await blob.arrayBuffer();\n    await writeFile(resolvedPath, new Uint8Array(arrayBuffer));\n  },\n\n  async openPath(path: string) {\n    const { open } = await import('@tauri-apps/plugin-shell');\n    await open(path);\n  },\n\n  async pickDirectory(title: string) {\n    const { open } = await import('@tauri-apps/plugin-dialog');\n    const selected = await open({ directory: true, title });\n    if (!selected) return null;\n    const dir = typeof selected === 'string' ? selected : (selected as { path: string }).path;\n    return dir || null;\n  },\n};\n"
  },
  {
    "path": "tauri/src/platform/index.ts",
    "content": "import type { Platform } from '@/platform/types';\nimport { tauriFilesystem } from './filesystem';\nimport { tauriUpdater } from './updater';\nimport { tauriAudio } from './audio';\nimport { tauriLifecycle } from './lifecycle';\nimport { tauriMetadata } from './metadata';\n\nexport const tauriPlatform: Platform = {\n  filesystem: tauriFilesystem,\n  updater: tauriUpdater,\n  audio: tauriAudio,\n  lifecycle: tauriLifecycle,\n  metadata: tauriMetadata,\n};\n"
  },
  {
    "path": "tauri/src/platform/lifecycle.ts",
    "content": "import { invoke } from '@tauri-apps/api/core';\nimport { emit, listen } from '@tauri-apps/api/event';\nimport type { PlatformLifecycle, ServerLogEntry } from '@/platform/types';\n\nclass TauriLifecycle implements PlatformLifecycle {\n  onServerReady?: () => void;\n\n  async startServer(remote = false, modelsDir?: string | null): Promise<string> {\n    try {\n      const result = await invoke<string>('start_server', {\n        remote,\n        modelsDir: modelsDir ?? undefined,\n      });\n      console.log('Server started:', result);\n      this.onServerReady?.();\n      return result;\n    } catch (error) {\n      console.error('Failed to start server:', error);\n      throw error;\n    }\n  }\n\n  async stopServer(): Promise<void> {\n    try {\n      await invoke('stop_server');\n      console.log('Server stopped');\n    } catch (error) {\n      console.error('Failed to stop server:', error);\n      throw error;\n    }\n  }\n\n  async restartServer(modelsDir?: string | null): Promise<string> {\n    try {\n      const result = await invoke<string>('restart_server', {\n        modelsDir: modelsDir ?? undefined,\n      });\n      console.log('Server restarted:', result);\n      this.onServerReady?.();\n      return result;\n    } catch (error) {\n      console.error('Failed to restart server:', error);\n      throw error;\n    }\n  }\n\n  async setKeepServerRunning(keepRunning: boolean): Promise<void> {\n    try {\n      await invoke('set_keep_server_running', { keepRunning });\n    } catch (error) {\n      console.error('Failed to set keep server running setting:', error);\n    }\n  }\n\n  async setupWindowCloseHandler(): Promise<void> {\n    try {\n      // Listen for window close request from Rust\n      await listen<null>('window-close-requested', async () => {\n        // Import store here to avoid circular dependency\n        const { useServerStore } = await import('@/stores/serverStore');\n        const keepRunning = useServerStore.getState().keepServerRunningOnClose;\n\n        // Check if server was started by this app instance\n        // @ts-expect-error - accessing module-level variable from another module\n        const serverStartedByApp = window.__voiceboxServerStartedByApp ?? false;\n\n        console.log(\n          '[lifecycle] window-close-requested: keepRunning=%s, serverStartedByApp=%s',\n          keepRunning,\n          serverStartedByApp,\n        );\n\n        if (!keepRunning && serverStartedByApp) {\n          // Stop server before closing (only if we started it)\n          try {\n            await this.stopServer();\n          } catch (error) {\n            console.error('Failed to stop server on close:', error);\n          }\n        }\n\n        // Emit event back to Rust to allow close\n        await emit('window-close-allowed');\n      });\n    } catch (error) {\n      console.error('Failed to setup window close handler:', error);\n    }\n  }\n\n  subscribeToServerLogs(callback: (entry: ServerLogEntry) => void): () => void {\n    let disposed = false;\n    let unlisten: (() => void) | null = null;\n\n    void listen<ServerLogEntry>('server-log', (event) => {\n      callback(event.payload);\n    })\n      .then((fn) => {\n        if (disposed) {\n          fn();\n          return;\n        }\n        unlisten = fn;\n      })\n      .catch((error) => {\n        console.error('Failed to subscribe to server logs:', error);\n      });\n\n    return () => {\n      disposed = true;\n      unlisten?.();\n      unlisten = null;\n    };\n  }\n}\n\nexport const tauriLifecycle = new TauriLifecycle();\n"
  },
  {
    "path": "tauri/src/platform/metadata.ts",
    "content": "import { getVersion } from '@tauri-apps/api/app';\nimport type { PlatformMetadata } from '@/platform/types';\n\nexport const tauriMetadata: PlatformMetadata = {\n  async getVersion(): Promise<string> {\n    try {\n      return await getVersion();\n    } catch (error) {\n      console.error('Failed to get version:', error);\n      return '0.1.0';\n    }\n  },\n  isTauri: true,\n};\n"
  },
  {
    "path": "tauri/src/platform/updater.ts",
    "content": "import { relaunch } from '@tauri-apps/plugin-process';\nimport { check, type Update } from '@tauri-apps/plugin-updater';\nimport type { PlatformUpdater, UpdateStatus } from '@/platform/types';\n\n// Check if we're on Windows (NSIS installer handles restart automatically)\nconst isWindows = () => {\n  return navigator.userAgent.includes('Windows');\n};\n\nclass TauriUpdater implements PlatformUpdater {\n  private status: UpdateStatus = {\n    checking: false,\n    available: false,\n    downloading: false,\n    installing: false,\n    readyToInstall: false,\n  };\n\n  private update: Update | null = null;\n  private subscribers: Set<(status: UpdateStatus) => void> = new Set();\n\n  private notifySubscribers() {\n    this.subscribers.forEach((callback) => callback(this.status));\n  }\n\n  subscribe(callback: (status: UpdateStatus) => void): () => void {\n    this.subscribers.add(callback);\n    // Immediately call with current status\n    callback(this.status);\n    return () => {\n      this.subscribers.delete(callback);\n    };\n  }\n\n  getStatus(): UpdateStatus {\n    return { ...this.status };\n  }\n\n  async checkForUpdates(): Promise<void> {\n    try {\n      this.status = { ...this.status, checking: true, error: undefined };\n      this.notifySubscribers();\n\n      const foundUpdate = await check();\n\n      if (foundUpdate?.available) {\n        this.update = foundUpdate;\n        this.status = {\n          checking: false,\n          available: true,\n          version: foundUpdate.version,\n          downloading: false,\n          installing: false,\n          readyToInstall: false,\n        };\n      } else {\n        this.status = {\n          checking: false,\n          available: false,\n          downloading: false,\n          installing: false,\n          readyToInstall: false,\n        };\n      }\n      this.notifySubscribers();\n    } catch (error) {\n      const message = error instanceof Error ? error.message : String(error);\n      // Tauri updater throws on 404 / no published release / network errors.\n      // Treat \"no update available\" style errors as up-to-date, not failures.\n      const isNoUpdate = /404|not found|no update|up.to.date/i.test(message);\n      this.status = {\n        checking: false,\n        available: false,\n        downloading: false,\n        installing: false,\n        readyToInstall: false,\n        error: isNoUpdate ? undefined : message,\n      };\n      this.notifySubscribers();\n    }\n  }\n\n  async downloadAndInstall(): Promise<void> {\n    if (!this.update) return;\n\n    try {\n      this.status = { ...this.status, downloading: true, error: undefined };\n      this.notifySubscribers();\n\n      let downloadedBytes = 0;\n      let totalBytes = 0;\n\n      await this.update.download((event) => {\n        switch (event.event) {\n          case 'Started':\n            totalBytes = event.data.contentLength || 0;\n            downloadedBytes = 0;\n            this.status = {\n              ...this.status,\n              downloading: true,\n              totalBytes,\n              downloadedBytes: 0,\n              downloadProgress: 0,\n            };\n            this.notifySubscribers();\n            break;\n          case 'Progress': {\n            downloadedBytes += event.data.chunkLength;\n            const progress =\n              totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : undefined;\n            this.status = {\n              ...this.status,\n              downloadedBytes,\n              downloadProgress: progress,\n            };\n            this.notifySubscribers();\n            break;\n          }\n          case 'Finished':\n            this.status = {\n              ...this.status,\n              downloading: false,\n              readyToInstall: true,\n              downloadProgress: 100,\n            };\n            this.notifySubscribers();\n            break;\n        }\n      });\n    } catch (error) {\n      this.status = {\n        ...this.status,\n        downloading: false,\n        installing: false,\n        readyToInstall: false,\n        downloadProgress: undefined,\n        downloadedBytes: undefined,\n        totalBytes: undefined,\n        error: error instanceof Error ? error.message : 'Failed to download update',\n      };\n      this.notifySubscribers();\n    }\n  }\n\n  async restartAndInstall(): Promise<void> {\n    if (!this.update) return;\n\n    try {\n      this.status = { ...this.status, installing: true, error: undefined };\n      this.notifySubscribers();\n\n      await this.update.install();\n\n      // On Windows with NSIS, the installer handles the restart automatically.\n      // On macOS/Linux, we need to manually relaunch.\n      if (!isWindows()) {\n        await relaunch();\n      }\n    } catch (error) {\n      this.status = {\n        ...this.status,\n        installing: false,\n        error: error instanceof Error ? error.message : 'Failed to install update',\n      };\n      this.notifySubscribers();\n    }\n  }\n}\n\nexport const tauriUpdater = new TauriUpdater();\n"
  },
  {
    "path": "tauri/src-tauri/Cargo.toml",
    "content": "[package]\nname = \"voicebox\"\nversion = \"0.3.1\"\ndescription = \"A production-quality desktop app for Qwen3-TTS voice cloning and generation\"\nauthors = [\"you\"]\nlicense = \"\"\nrepository = \"\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[build-dependencies]\ntauri-build = { version = \"2.0\", features = [] }\n\n[dependencies]\ntauri = { version = \"2.0\", features = [] }\ntauri-plugin-dialog = \"2.0\"\ntauri-plugin-fs = \"2.0\"\ntauri-plugin-shell = \"2.0\"\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1.0\"\ntokio = { version = \"1\", features = [\"full\"] }\nreqwest = { version = \"0.12\", features = [\"blocking\", \"json\"] }\nhound = \"3.5\"\nbase64 = \"0.22\"\ncpal = \"0.15\"\nsymphonia = { version = \"0.5\", features = [\"all\"] }\nscopeguard = \"1.2.0\"\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\nscreencapturekit = { version = \"1\", features = [\"async\"] }\ncoreaudio-sys = \"0.2\"\nobjc = \"0.2\"\ncore-foundation-sys = \"0.8\"\n\n[target.'cfg(target_os = \"windows\")'.dependencies]\nwasapi = \"0.22\"\nwindows = { version = \"0.62\", features = [\"Win32_Foundation\", \"Win32_UI_WindowsAndMessaging\", \"Win32_System_Com\"] }\n\n[target.'cfg(target_os = \"linux\")'.dependencies]\nwebkit2gtk = \"2.0\"\n\n[target.'cfg(not(any(target_os = \"android\", target_os = \"ios\")))'.dependencies]\ntauri-plugin-updater = \"2.0\"\ntauri-plugin-process = \"2.0\"\n\n[features]\n# This feature is used for production builds or when `devPath` points to the filesystem\ncustom-protocol = [\"tauri/custom-protocol\"]\n"
  },
  {
    "path": "tauri/src-tauri/Entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.disable-library-validation</key>\n    <true/>\n    <key>com.apple.security.device.audio-input</key>\n    <true/>\n    <key>com.apple.security.files.user-selected.read-write</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "tauri/src-tauri/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleIconFile</key>\n\t<string>voicebox</string>\n\t<key>CFBundleIconName</key>\n\t<string>voicebox</string>\n\t<key>NSMicrophoneUsageDescription</key>\n\t<string>voicebox needs microphone access to record voice samples for voice cloning.</string>\n\t<key>NSScreenCaptureUsageDescription</key>\n\t<string>Voicebox needs screen capture access to record system audio for voice samples.</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "tauri/src-tauri/build.rs",
    "content": "#[cfg(target_os = \"macos\")]\nuse std::process::Command;\n\nfn main() {\n    // Link Swift runtime libraries for screencapturekit crate\n    #[cfg(target_os = \"macos\")]\n    {\n        // Add Swift runtime library paths to RPATH\n        println!(\"cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift\");\n        println!(\"cargo:rustc-link-arg=-L/usr/lib/swift\");\n\n        // Also try Xcode's Swift libraries\n        if let Ok(output) = Command::new(\"xcode-select\").arg(\"-p\").output() {\n            if output.status.success() {\n                let xcode_path = String::from_utf8_lossy(&output.stdout).trim().to_string();\n                let swift_lib_path = format!(\n                    \"{}/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx\",\n                    xcode_path\n                );\n                println!(\"cargo:rustc-link-arg=-Wl,-rpath,{}\", swift_lib_path);\n                println!(\"cargo:rustc-link-arg=-L{}\", swift_lib_path);\n            }\n        }\n    }\n\n    let project_root = env!(\"CARGO_MANIFEST_DIR\");\n    let gen_dir = format!(\"{}/gen\", project_root);\n    std::fs::create_dir_all(&gen_dir).expect(\"Failed to create gen directory\");\n\n    // Compile macOS Liquid Glass icon\n    #[cfg(target_os = \"macos\")]\n    {\n        // voicebox.icon is in tauri/assets/voicebox.icon (one level up from src-tauri)\n        let icon_source = format!(\"{}/../assets/voicebox.icon\", project_root);\n\n        if std::path::Path::new(&icon_source).exists() {\n            println!(\"cargo:rerun-if-changed={}\", icon_source);\n            println!(\"cargo:rerun-if-changed={}/icon.json\", icon_source);\n            println!(\"cargo:rerun-if-changed={}/Assets\", icon_source);\n\n            let partial_plist = format!(\"{}/partial.plist\", gen_dir);\n            let output = Command::new(\"xcrun\")\n                .args([\n                    \"actool\",\n                    \"--compile\",\n                    &gen_dir,\n                    \"--output-format\",\n                    \"human-readable-text\",\n                    \"--output-partial-info-plist\",\n                    &partial_plist,\n                    \"--app-icon\",\n                    \"voicebox\",\n                    \"--include-all-app-icons\",\n                    \"--target-device\",\n                    \"mac\",\n                    \"--minimum-deployment-target\",\n                    \"11.0\",\n                    \"--platform\",\n                    \"macosx\",\n                    &icon_source,\n                ])\n                .output();\n\n            match output {\n                Ok(output) => {\n                    if !output.status.success() {\n                        eprintln!(\"actool stderr: {}\", String::from_utf8_lossy(&output.stderr));\n                        eprintln!(\"actool stdout: {}\", String::from_utf8_lossy(&output.stdout));\n                        panic!(\"actool failed to compile icon\");\n                    }\n                    println!(\"Successfully compiled icon to {}\", gen_dir);\n                }\n                Err(e) => {\n                    eprintln!(\"Failed to execute xcrun actool: {}\", e);\n                    eprintln!(\"Make sure you have Xcode Command Line Tools installed\");\n                    panic!(\"Icon compilation failed\");\n                }\n            }\n\n            // Generate voicebox.icns from the source PNG via sips + iconutil\n            let icns_path = format!(\"{}/voicebox.icns\", gen_dir);\n            if !std::path::Path::new(&icns_path).exists() {\n                let source_png = format!(\"{}/Assets/Voicebox.png\", icon_source);\n                if std::path::Path::new(&source_png).exists() {\n                    let iconset_dir = format!(\"{}/voicebox.iconset\", gen_dir);\n                    std::fs::create_dir_all(&iconset_dir).ok();\n\n                    let sizes: &[(u32, &str)] = &[\n                        (16, \"icon_16x16.png\"),\n                        (32, \"icon_16x16@2x.png\"),\n                        (32, \"icon_32x32.png\"),\n                        (64, \"icon_32x32@2x.png\"),\n                        (128, \"icon_128x128.png\"),\n                        (256, \"icon_128x128@2x.png\"),\n                        (256, \"icon_256x256.png\"),\n                        (512, \"icon_256x256@2x.png\"),\n                        (512, \"icon_512x512.png\"),\n                        (1024, \"icon_512x512@2x.png\"),\n                    ];\n\n                    for (size, name) in sizes {\n                        let dest = format!(\"{}/{}\", iconset_dir, name);\n                        let status = Command::new(\"sips\")\n                            .args([\n                                \"-z\",\n                                &size.to_string(),\n                                &size.to_string(),\n                                &source_png,\n                                \"--out\",\n                                &dest,\n                            ])\n                            .output();\n                        if let Ok(out) = status {\n                            if !out.status.success() {\n                                eprintln!(\n                                    \"sips failed for {}: {}\",\n                                    name,\n                                    String::from_utf8_lossy(&out.stderr)\n                                );\n                            }\n                        }\n                    }\n\n                    let iconutil_output = Command::new(\"iconutil\")\n                        .args([\"-c\", \"icns\", \"-o\", &icns_path, &iconset_dir])\n                        .output();\n\n                    match iconutil_output {\n                        Ok(out) if out.status.success() => {\n                            println!(\"Generated voicebox.icns\");\n                        }\n                        Ok(out) => {\n                            eprintln!(\"iconutil failed: {}\", String::from_utf8_lossy(&out.stderr));\n                        }\n                        Err(e) => {\n                            eprintln!(\"Failed to run iconutil: {}\", e);\n                        }\n                    }\n\n                    // Clean up iconset directory\n                    std::fs::remove_dir_all(&iconset_dir).ok();\n                }\n            }\n        } else {\n            println!(\n                \"cargo:warning=Icon source not found at {}, skipping icon compilation\",\n                icon_source\n            );\n        }\n    }\n\n    // Ensure all resource files exist so Tauri's bundler doesn't fail.\n    // On non-macOS these are always stubs. On macOS, actool may not produce\n    // Assets.car if the Xcode version doesn't support the .icon format.\n    {\n        let required = [\"Assets.car\", \"voicebox.icns\", \"partial.plist\"];\n        for name in required {\n            let path = format!(\"{}/{}\", gen_dir, name);\n            if !std::path::Path::new(&path).exists() {\n                std::fs::write(&path, b\"\").ok();\n            }\n        }\n    }\n\n    tauri_build::build()\n}\n"
  },
  {
    "path": "tauri/src-tauri/capabilities/default.json",
    "content": "{\n  \"$schema\": \"https://schema.tauri.app/config/2\",\n  \"identifier\": \"default\",\n  \"description\": \"Default permissions for voicebox\",\n  \"platforms\": [\"linux\", \"macOS\", \"windows\"],\n  \"windows\": [\"main\"],\n  \"remote\": {\n    \"urls\": [\"http://localhost:*\"]\n  },\n  \"permissions\": [\n    \"core:default\",\n    \"core:window:default\",\n    \"core:window:allow-start-dragging\",\n    \"core:webview:default\",\n    \"core:webview:allow-internal-toggle-devtools\",\n    \"shell:allow-open\",\n    \"shell:allow-execute\",\n    \"shell:allow-spawn\",\n    \"updater:default\",\n    \"process:default\",\n    \"dialog:default\",\n    \"dialog:allow-save\",\n    \"dialog:allow-open\",\n    \"fs:default\",\n    \"fs:read-all\",\n    \"fs:write-all\"\n  ]\n}\n"
  },
  {
    "path": "tauri/src-tauri/gen/schemas/acl-manifests.json",
    "content": "{\"core\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default core plugins set.\",\"permissions\":[\"core:path:default\",\"core:event:default\",\"core:window:default\",\"core:webview:default\",\"core:app:default\",\"core:image:default\",\"core:resources:default\",\"core:menu:default\",\"core:tray:default\"]},\"permissions\":{},\"permission_sets\":{},\"global_scope_schema\":null},\"core:app\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin.\",\"permissions\":[\"allow-version\",\"allow-name\",\"allow-tauri-version\",\"allow-identifier\",\"allow-bundle-type\",\"allow-register-listener\",\"allow-remove-listener\"]},\"permissions\":{\"allow-app-hide\":{\"identifier\":\"allow-app-hide\",\"description\":\"Enables the app_hide command without any pre-configured scope.\",\"commands\":{\"allow\":[\"app_hide\"],\"deny\":[]}},\"allow-app-show\":{\"identifier\":\"allow-app-show\",\"description\":\"Enables the app_show command without any pre-configured scope.\",\"commands\":{\"allow\":[\"app_show\"],\"deny\":[]}},\"allow-bundle-type\":{\"identifier\":\"allow-bundle-type\",\"description\":\"Enables the bundle_type command without any pre-configured scope.\",\"commands\":{\"allow\":[\"bundle_type\"],\"deny\":[]}},\"allow-default-window-icon\":{\"identifier\":\"allow-default-window-icon\",\"description\":\"Enables the default_window_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"default_window_icon\"],\"deny\":[]}},\"allow-fetch-data-store-identifiers\":{\"identifier\":\"allow-fetch-data-store-identifiers\",\"description\":\"Enables the fetch_data_store_identifiers command without any pre-configured scope.\",\"commands\":{\"allow\":[\"fetch_data_store_identifiers\"],\"deny\":[]}},\"allow-identifier\":{\"identifier\":\"allow-identifier\",\"description\":\"Enables the identifier command without any pre-configured scope.\",\"commands\":{\"allow\":[\"identifier\"],\"deny\":[]}},\"allow-name\":{\"identifier\":\"allow-name\",\"description\":\"Enables the name command without any pre-configured scope.\",\"commands\":{\"allow\":[\"name\"],\"deny\":[]}},\"allow-register-listener\":{\"identifier\":\"allow-register-listener\",\"description\":\"Enables the register_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[\"register_listener\"],\"deny\":[]}},\"allow-remove-data-store\":{\"identifier\":\"allow-remove-data-store\",\"description\":\"Enables the remove_data_store command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_data_store\"],\"deny\":[]}},\"allow-remove-listener\":{\"identifier\":\"allow-remove-listener\",\"description\":\"Enables the remove_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_listener\"],\"deny\":[]}},\"allow-set-app-theme\":{\"identifier\":\"allow-set-app-theme\",\"description\":\"Enables the set_app_theme command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_app_theme\"],\"deny\":[]}},\"allow-set-dock-visibility\":{\"identifier\":\"allow-set-dock-visibility\",\"description\":\"Enables the set_dock_visibility command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_dock_visibility\"],\"deny\":[]}},\"allow-tauri-version\":{\"identifier\":\"allow-tauri-version\",\"description\":\"Enables the tauri_version command without any pre-configured scope.\",\"commands\":{\"allow\":[\"tauri_version\"],\"deny\":[]}},\"allow-version\":{\"identifier\":\"allow-version\",\"description\":\"Enables the version command without any pre-configured scope.\",\"commands\":{\"allow\":[\"version\"],\"deny\":[]}},\"deny-app-hide\":{\"identifier\":\"deny-app-hide\",\"description\":\"Denies the app_hide command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"app_hide\"]}},\"deny-app-show\":{\"identifier\":\"deny-app-show\",\"description\":\"Denies the app_show command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"app_show\"]}},\"deny-bundle-type\":{\"identifier\":\"deny-bundle-type\",\"description\":\"Denies the bundle_type command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"bundle_type\"]}},\"deny-default-window-icon\":{\"identifier\":\"deny-default-window-icon\",\"description\":\"Denies the default_window_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"default_window_icon\"]}},\"deny-fetch-data-store-identifiers\":{\"identifier\":\"deny-fetch-data-store-identifiers\",\"description\":\"Denies the fetch_data_store_identifiers command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"fetch_data_store_identifiers\"]}},\"deny-identifier\":{\"identifier\":\"deny-identifier\",\"description\":\"Denies the identifier command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"identifier\"]}},\"deny-name\":{\"identifier\":\"deny-name\",\"description\":\"Denies the name command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"name\"]}},\"deny-register-listener\":{\"identifier\":\"deny-register-listener\",\"description\":\"Denies the register_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"register_listener\"]}},\"deny-remove-data-store\":{\"identifier\":\"deny-remove-data-store\",\"description\":\"Denies the remove_data_store command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_data_store\"]}},\"deny-remove-listener\":{\"identifier\":\"deny-remove-listener\",\"description\":\"Denies the remove_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_listener\"]}},\"deny-set-app-theme\":{\"identifier\":\"deny-set-app-theme\",\"description\":\"Denies the set_app_theme command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_app_theme\"]}},\"deny-set-dock-visibility\":{\"identifier\":\"deny-set-dock-visibility\",\"description\":\"Denies the set_dock_visibility command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_dock_visibility\"]}},\"deny-tauri-version\":{\"identifier\":\"deny-tauri-version\",\"description\":\"Denies the tauri_version command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"tauri_version\"]}},\"deny-version\":{\"identifier\":\"deny-version\",\"description\":\"Denies the version command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"version\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:event\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-listen\",\"allow-unlisten\",\"allow-emit\",\"allow-emit-to\"]},\"permissions\":{\"allow-emit\":{\"identifier\":\"allow-emit\",\"description\":\"Enables the emit command without any pre-configured scope.\",\"commands\":{\"allow\":[\"emit\"],\"deny\":[]}},\"allow-emit-to\":{\"identifier\":\"allow-emit-to\",\"description\":\"Enables the emit_to command without any pre-configured scope.\",\"commands\":{\"allow\":[\"emit_to\"],\"deny\":[]}},\"allow-listen\":{\"identifier\":\"allow-listen\",\"description\":\"Enables the listen command without any pre-configured scope.\",\"commands\":{\"allow\":[\"listen\"],\"deny\":[]}},\"allow-unlisten\":{\"identifier\":\"allow-unlisten\",\"description\":\"Enables the unlisten command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unlisten\"],\"deny\":[]}},\"deny-emit\":{\"identifier\":\"deny-emit\",\"description\":\"Denies the emit command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"emit\"]}},\"deny-emit-to\":{\"identifier\":\"deny-emit-to\",\"description\":\"Denies the emit_to command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"emit_to\"]}},\"deny-listen\":{\"identifier\":\"deny-listen\",\"description\":\"Denies the listen command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"listen\"]}},\"deny-unlisten\":{\"identifier\":\"deny-unlisten\",\"description\":\"Denies the unlisten command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unlisten\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:image\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-new\",\"allow-from-bytes\",\"allow-from-path\",\"allow-rgba\",\"allow-size\"]},\"permissions\":{\"allow-from-bytes\":{\"identifier\":\"allow-from-bytes\",\"description\":\"Enables the from_bytes command without any pre-configured scope.\",\"commands\":{\"allow\":[\"from_bytes\"],\"deny\":[]}},\"allow-from-path\":{\"identifier\":\"allow-from-path\",\"description\":\"Enables the from_path command without any pre-configured scope.\",\"commands\":{\"allow\":[\"from_path\"],\"deny\":[]}},\"allow-new\":{\"identifier\":\"allow-new\",\"description\":\"Enables the new command without any pre-configured scope.\",\"commands\":{\"allow\":[\"new\"],\"deny\":[]}},\"allow-rgba\":{\"identifier\":\"allow-rgba\",\"description\":\"Enables the rgba command without any pre-configured scope.\",\"commands\":{\"allow\":[\"rgba\"],\"deny\":[]}},\"allow-size\":{\"identifier\":\"allow-size\",\"description\":\"Enables the size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"size\"],\"deny\":[]}},\"deny-from-bytes\":{\"identifier\":\"deny-from-bytes\",\"description\":\"Denies the from_bytes command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"from_bytes\"]}},\"deny-from-path\":{\"identifier\":\"deny-from-path\",\"description\":\"Denies the from_path command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"from_path\"]}},\"deny-new\":{\"identifier\":\"deny-new\",\"description\":\"Denies the new command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"new\"]}},\"deny-rgba\":{\"identifier\":\"deny-rgba\",\"description\":\"Denies the rgba command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"rgba\"]}},\"deny-size\":{\"identifier\":\"deny-size\",\"description\":\"Denies the size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"size\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:menu\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-new\",\"allow-append\",\"allow-prepend\",\"allow-insert\",\"allow-remove\",\"allow-remove-at\",\"allow-items\",\"allow-get\",\"allow-popup\",\"allow-create-default\",\"allow-set-as-app-menu\",\"allow-set-as-window-menu\",\"allow-text\",\"allow-set-text\",\"allow-is-enabled\",\"allow-set-enabled\",\"allow-set-accelerator\",\"allow-set-as-windows-menu-for-nsapp\",\"allow-set-as-help-menu-for-nsapp\",\"allow-is-checked\",\"allow-set-checked\",\"allow-set-icon\"]},\"permissions\":{\"allow-append\":{\"identifier\":\"allow-append\",\"description\":\"Enables the append command without any pre-configured scope.\",\"commands\":{\"allow\":[\"append\"],\"deny\":[]}},\"allow-create-default\":{\"identifier\":\"allow-create-default\",\"description\":\"Enables the create_default command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create_default\"],\"deny\":[]}},\"allow-get\":{\"identifier\":\"allow-get\",\"description\":\"Enables the get command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get\"],\"deny\":[]}},\"allow-insert\":{\"identifier\":\"allow-insert\",\"description\":\"Enables the insert command without any pre-configured scope.\",\"commands\":{\"allow\":[\"insert\"],\"deny\":[]}},\"allow-is-checked\":{\"identifier\":\"allow-is-checked\",\"description\":\"Enables the is_checked command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_checked\"],\"deny\":[]}},\"allow-is-enabled\":{\"identifier\":\"allow-is-enabled\",\"description\":\"Enables the is_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_enabled\"],\"deny\":[]}},\"allow-items\":{\"identifier\":\"allow-items\",\"description\":\"Enables the items command without any pre-configured scope.\",\"commands\":{\"allow\":[\"items\"],\"deny\":[]}},\"allow-new\":{\"identifier\":\"allow-new\",\"description\":\"Enables the new command without any pre-configured scope.\",\"commands\":{\"allow\":[\"new\"],\"deny\":[]}},\"allow-popup\":{\"identifier\":\"allow-popup\",\"description\":\"Enables the popup command without any pre-configured scope.\",\"commands\":{\"allow\":[\"popup\"],\"deny\":[]}},\"allow-prepend\":{\"identifier\":\"allow-prepend\",\"description\":\"Enables the prepend command without any pre-configured scope.\",\"commands\":{\"allow\":[\"prepend\"],\"deny\":[]}},\"allow-remove\":{\"identifier\":\"allow-remove\",\"description\":\"Enables the remove command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove\"],\"deny\":[]}},\"allow-remove-at\":{\"identifier\":\"allow-remove-at\",\"description\":\"Enables the remove_at command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_at\"],\"deny\":[]}},\"allow-set-accelerator\":{\"identifier\":\"allow-set-accelerator\",\"description\":\"Enables the set_accelerator command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_accelerator\"],\"deny\":[]}},\"allow-set-as-app-menu\":{\"identifier\":\"allow-set-as-app-menu\",\"description\":\"Enables the set_as_app_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_as_app_menu\"],\"deny\":[]}},\"allow-set-as-help-menu-for-nsapp\":{\"identifier\":\"allow-set-as-help-menu-for-nsapp\",\"description\":\"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_as_help_menu_for_nsapp\"],\"deny\":[]}},\"allow-set-as-window-menu\":{\"identifier\":\"allow-set-as-window-menu\",\"description\":\"Enables the set_as_window_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_as_window_menu\"],\"deny\":[]}},\"allow-set-as-windows-menu-for-nsapp\":{\"identifier\":\"allow-set-as-windows-menu-for-nsapp\",\"description\":\"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_as_windows_menu_for_nsapp\"],\"deny\":[]}},\"allow-set-checked\":{\"identifier\":\"allow-set-checked\",\"description\":\"Enables the set_checked command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_checked\"],\"deny\":[]}},\"allow-set-enabled\":{\"identifier\":\"allow-set-enabled\",\"description\":\"Enables the set_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_enabled\"],\"deny\":[]}},\"allow-set-icon\":{\"identifier\":\"allow-set-icon\",\"description\":\"Enables the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_icon\"],\"deny\":[]}},\"allow-set-text\":{\"identifier\":\"allow-set-text\",\"description\":\"Enables the set_text command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_text\"],\"deny\":[]}},\"allow-text\":{\"identifier\":\"allow-text\",\"description\":\"Enables the text command without any pre-configured scope.\",\"commands\":{\"allow\":[\"text\"],\"deny\":[]}},\"deny-append\":{\"identifier\":\"deny-append\",\"description\":\"Denies the append command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"append\"]}},\"deny-create-default\":{\"identifier\":\"deny-create-default\",\"description\":\"Denies the create_default command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create_default\"]}},\"deny-get\":{\"identifier\":\"deny-get\",\"description\":\"Denies the get command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get\"]}},\"deny-insert\":{\"identifier\":\"deny-insert\",\"description\":\"Denies the insert command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"insert\"]}},\"deny-is-checked\":{\"identifier\":\"deny-is-checked\",\"description\":\"Denies the is_checked command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_checked\"]}},\"deny-is-enabled\":{\"identifier\":\"deny-is-enabled\",\"description\":\"Denies the is_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_enabled\"]}},\"deny-items\":{\"identifier\":\"deny-items\",\"description\":\"Denies the items command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"items\"]}},\"deny-new\":{\"identifier\":\"deny-new\",\"description\":\"Denies the new command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"new\"]}},\"deny-popup\":{\"identifier\":\"deny-popup\",\"description\":\"Denies the popup command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"popup\"]}},\"deny-prepend\":{\"identifier\":\"deny-prepend\",\"description\":\"Denies the prepend command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"prepend\"]}},\"deny-remove\":{\"identifier\":\"deny-remove\",\"description\":\"Denies the remove command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove\"]}},\"deny-remove-at\":{\"identifier\":\"deny-remove-at\",\"description\":\"Denies the remove_at command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_at\"]}},\"deny-set-accelerator\":{\"identifier\":\"deny-set-accelerator\",\"description\":\"Denies the set_accelerator command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_accelerator\"]}},\"deny-set-as-app-menu\":{\"identifier\":\"deny-set-as-app-menu\",\"description\":\"Denies the set_as_app_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_as_app_menu\"]}},\"deny-set-as-help-menu-for-nsapp\":{\"identifier\":\"deny-set-as-help-menu-for-nsapp\",\"description\":\"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_as_help_menu_for_nsapp\"]}},\"deny-set-as-window-menu\":{\"identifier\":\"deny-set-as-window-menu\",\"description\":\"Denies the set_as_window_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_as_window_menu\"]}},\"deny-set-as-windows-menu-for-nsapp\":{\"identifier\":\"deny-set-as-windows-menu-for-nsapp\",\"description\":\"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_as_windows_menu_for_nsapp\"]}},\"deny-set-checked\":{\"identifier\":\"deny-set-checked\",\"description\":\"Denies the set_checked command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_checked\"]}},\"deny-set-enabled\":{\"identifier\":\"deny-set-enabled\",\"description\":\"Denies the set_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_enabled\"]}},\"deny-set-icon\":{\"identifier\":\"deny-set-icon\",\"description\":\"Denies the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_icon\"]}},\"deny-set-text\":{\"identifier\":\"deny-set-text\",\"description\":\"Denies the set_text command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_text\"]}},\"deny-text\":{\"identifier\":\"deny-text\",\"description\":\"Denies the text command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"text\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:path\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-resolve-directory\",\"allow-resolve\",\"allow-normalize\",\"allow-join\",\"allow-dirname\",\"allow-extname\",\"allow-basename\",\"allow-is-absolute\"]},\"permissions\":{\"allow-basename\":{\"identifier\":\"allow-basename\",\"description\":\"Enables the basename command without any pre-configured scope.\",\"commands\":{\"allow\":[\"basename\"],\"deny\":[]}},\"allow-dirname\":{\"identifier\":\"allow-dirname\",\"description\":\"Enables the dirname command without any pre-configured scope.\",\"commands\":{\"allow\":[\"dirname\"],\"deny\":[]}},\"allow-extname\":{\"identifier\":\"allow-extname\",\"description\":\"Enables the extname command without any pre-configured scope.\",\"commands\":{\"allow\":[\"extname\"],\"deny\":[]}},\"allow-is-absolute\":{\"identifier\":\"allow-is-absolute\",\"description\":\"Enables the is_absolute command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_absolute\"],\"deny\":[]}},\"allow-join\":{\"identifier\":\"allow-join\",\"description\":\"Enables the join command without any pre-configured scope.\",\"commands\":{\"allow\":[\"join\"],\"deny\":[]}},\"allow-normalize\":{\"identifier\":\"allow-normalize\",\"description\":\"Enables the normalize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"normalize\"],\"deny\":[]}},\"allow-resolve\":{\"identifier\":\"allow-resolve\",\"description\":\"Enables the resolve command without any pre-configured scope.\",\"commands\":{\"allow\":[\"resolve\"],\"deny\":[]}},\"allow-resolve-directory\":{\"identifier\":\"allow-resolve-directory\",\"description\":\"Enables the resolve_directory command without any pre-configured scope.\",\"commands\":{\"allow\":[\"resolve_directory\"],\"deny\":[]}},\"deny-basename\":{\"identifier\":\"deny-basename\",\"description\":\"Denies the basename command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"basename\"]}},\"deny-dirname\":{\"identifier\":\"deny-dirname\",\"description\":\"Denies the dirname command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"dirname\"]}},\"deny-extname\":{\"identifier\":\"deny-extname\",\"description\":\"Denies the extname command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"extname\"]}},\"deny-is-absolute\":{\"identifier\":\"deny-is-absolute\",\"description\":\"Denies the is_absolute command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_absolute\"]}},\"deny-join\":{\"identifier\":\"deny-join\",\"description\":\"Denies the join command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"join\"]}},\"deny-normalize\":{\"identifier\":\"deny-normalize\",\"description\":\"Denies the normalize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"normalize\"]}},\"deny-resolve\":{\"identifier\":\"deny-resolve\",\"description\":\"Denies the resolve command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"resolve\"]}},\"deny-resolve-directory\":{\"identifier\":\"deny-resolve-directory\",\"description\":\"Denies the resolve_directory command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"resolve_directory\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:resources\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-close\"]},\"permissions\":{\"allow-close\":{\"identifier\":\"allow-close\",\"description\":\"Enables the close command without any pre-configured scope.\",\"commands\":{\"allow\":[\"close\"],\"deny\":[]}},\"deny-close\":{\"identifier\":\"deny-close\",\"description\":\"Denies the close command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"close\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:tray\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-new\",\"allow-get-by-id\",\"allow-remove-by-id\",\"allow-set-icon\",\"allow-set-menu\",\"allow-set-tooltip\",\"allow-set-title\",\"allow-set-visible\",\"allow-set-temp-dir-path\",\"allow-set-icon-as-template\",\"allow-set-show-menu-on-left-click\"]},\"permissions\":{\"allow-get-by-id\":{\"identifier\":\"allow-get-by-id\",\"description\":\"Enables the get_by_id command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_by_id\"],\"deny\":[]}},\"allow-new\":{\"identifier\":\"allow-new\",\"description\":\"Enables the new command without any pre-configured scope.\",\"commands\":{\"allow\":[\"new\"],\"deny\":[]}},\"allow-remove-by-id\":{\"identifier\":\"allow-remove-by-id\",\"description\":\"Enables the remove_by_id command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_by_id\"],\"deny\":[]}},\"allow-set-icon\":{\"identifier\":\"allow-set-icon\",\"description\":\"Enables the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_icon\"],\"deny\":[]}},\"allow-set-icon-as-template\":{\"identifier\":\"allow-set-icon-as-template\",\"description\":\"Enables the set_icon_as_template command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_icon_as_template\"],\"deny\":[]}},\"allow-set-menu\":{\"identifier\":\"allow-set-menu\",\"description\":\"Enables the set_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_menu\"],\"deny\":[]}},\"allow-set-show-menu-on-left-click\":{\"identifier\":\"allow-set-show-menu-on-left-click\",\"description\":\"Enables the set_show_menu_on_left_click command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_show_menu_on_left_click\"],\"deny\":[]}},\"allow-set-temp-dir-path\":{\"identifier\":\"allow-set-temp-dir-path\",\"description\":\"Enables the set_temp_dir_path command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_temp_dir_path\"],\"deny\":[]}},\"allow-set-title\":{\"identifier\":\"allow-set-title\",\"description\":\"Enables the set_title command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_title\"],\"deny\":[]}},\"allow-set-tooltip\":{\"identifier\":\"allow-set-tooltip\",\"description\":\"Enables the set_tooltip command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_tooltip\"],\"deny\":[]}},\"allow-set-visible\":{\"identifier\":\"allow-set-visible\",\"description\":\"Enables the set_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_visible\"],\"deny\":[]}},\"deny-get-by-id\":{\"identifier\":\"deny-get-by-id\",\"description\":\"Denies the get_by_id command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_by_id\"]}},\"deny-new\":{\"identifier\":\"deny-new\",\"description\":\"Denies the new command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"new\"]}},\"deny-remove-by-id\":{\"identifier\":\"deny-remove-by-id\",\"description\":\"Denies the remove_by_id command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_by_id\"]}},\"deny-set-icon\":{\"identifier\":\"deny-set-icon\",\"description\":\"Denies the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_icon\"]}},\"deny-set-icon-as-template\":{\"identifier\":\"deny-set-icon-as-template\",\"description\":\"Denies the set_icon_as_template command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_icon_as_template\"]}},\"deny-set-menu\":{\"identifier\":\"deny-set-menu\",\"description\":\"Denies the set_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_menu\"]}},\"deny-set-show-menu-on-left-click\":{\"identifier\":\"deny-set-show-menu-on-left-click\",\"description\":\"Denies the set_show_menu_on_left_click command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_show_menu_on_left_click\"]}},\"deny-set-temp-dir-path\":{\"identifier\":\"deny-set-temp-dir-path\",\"description\":\"Denies the set_temp_dir_path command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_temp_dir_path\"]}},\"deny-set-title\":{\"identifier\":\"deny-set-title\",\"description\":\"Denies the set_title command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_title\"]}},\"deny-set-tooltip\":{\"identifier\":\"deny-set-tooltip\",\"description\":\"Denies the set_tooltip command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_tooltip\"]}},\"deny-set-visible\":{\"identifier\":\"deny-set-visible\",\"description\":\"Denies the set_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_visible\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:webview\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin.\",\"permissions\":[\"allow-get-all-webviews\",\"allow-webview-position\",\"allow-webview-size\",\"allow-internal-toggle-devtools\"]},\"permissions\":{\"allow-clear-all-browsing-data\":{\"identifier\":\"allow-clear-all-browsing-data\",\"description\":\"Enables the clear_all_browsing_data command without any pre-configured scope.\",\"commands\":{\"allow\":[\"clear_all_browsing_data\"],\"deny\":[]}},\"allow-create-webview\":{\"identifier\":\"allow-create-webview\",\"description\":\"Enables the create_webview command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create_webview\"],\"deny\":[]}},\"allow-create-webview-window\":{\"identifier\":\"allow-create-webview-window\",\"description\":\"Enables the create_webview_window command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create_webview_window\"],\"deny\":[]}},\"allow-get-all-webviews\":{\"identifier\":\"allow-get-all-webviews\",\"description\":\"Enables the get_all_webviews command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_all_webviews\"],\"deny\":[]}},\"allow-internal-toggle-devtools\":{\"identifier\":\"allow-internal-toggle-devtools\",\"description\":\"Enables the internal_toggle_devtools command without any pre-configured scope.\",\"commands\":{\"allow\":[\"internal_toggle_devtools\"],\"deny\":[]}},\"allow-print\":{\"identifier\":\"allow-print\",\"description\":\"Enables the print command without any pre-configured scope.\",\"commands\":{\"allow\":[\"print\"],\"deny\":[]}},\"allow-reparent\":{\"identifier\":\"allow-reparent\",\"description\":\"Enables the reparent command without any pre-configured scope.\",\"commands\":{\"allow\":[\"reparent\"],\"deny\":[]}},\"allow-set-webview-auto-resize\":{\"identifier\":\"allow-set-webview-auto-resize\",\"description\":\"Enables the set_webview_auto_resize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_auto_resize\"],\"deny\":[]}},\"allow-set-webview-background-color\":{\"identifier\":\"allow-set-webview-background-color\",\"description\":\"Enables the set_webview_background_color command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_background_color\"],\"deny\":[]}},\"allow-set-webview-focus\":{\"identifier\":\"allow-set-webview-focus\",\"description\":\"Enables the set_webview_focus command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_focus\"],\"deny\":[]}},\"allow-set-webview-position\":{\"identifier\":\"allow-set-webview-position\",\"description\":\"Enables the set_webview_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_position\"],\"deny\":[]}},\"allow-set-webview-size\":{\"identifier\":\"allow-set-webview-size\",\"description\":\"Enables the set_webview_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_size\"],\"deny\":[]}},\"allow-set-webview-zoom\":{\"identifier\":\"allow-set-webview-zoom\",\"description\":\"Enables the set_webview_zoom command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_zoom\"],\"deny\":[]}},\"allow-webview-close\":{\"identifier\":\"allow-webview-close\",\"description\":\"Enables the webview_close command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_close\"],\"deny\":[]}},\"allow-webview-hide\":{\"identifier\":\"allow-webview-hide\",\"description\":\"Enables the webview_hide command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_hide\"],\"deny\":[]}},\"allow-webview-position\":{\"identifier\":\"allow-webview-position\",\"description\":\"Enables the webview_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_position\"],\"deny\":[]}},\"allow-webview-show\":{\"identifier\":\"allow-webview-show\",\"description\":\"Enables the webview_show command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_show\"],\"deny\":[]}},\"allow-webview-size\":{\"identifier\":\"allow-webview-size\",\"description\":\"Enables the webview_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_size\"],\"deny\":[]}},\"deny-clear-all-browsing-data\":{\"identifier\":\"deny-clear-all-browsing-data\",\"description\":\"Denies the clear_all_browsing_data command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"clear_all_browsing_data\"]}},\"deny-create-webview\":{\"identifier\":\"deny-create-webview\",\"description\":\"Denies the create_webview command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create_webview\"]}},\"deny-create-webview-window\":{\"identifier\":\"deny-create-webview-window\",\"description\":\"Denies the create_webview_window command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create_webview_window\"]}},\"deny-get-all-webviews\":{\"identifier\":\"deny-get-all-webviews\",\"description\":\"Denies the get_all_webviews command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_all_webviews\"]}},\"deny-internal-toggle-devtools\":{\"identifier\":\"deny-internal-toggle-devtools\",\"description\":\"Denies the internal_toggle_devtools command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"internal_toggle_devtools\"]}},\"deny-print\":{\"identifier\":\"deny-print\",\"description\":\"Denies the print command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"print\"]}},\"deny-reparent\":{\"identifier\":\"deny-reparent\",\"description\":\"Denies the reparent command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"reparent\"]}},\"deny-set-webview-auto-resize\":{\"identifier\":\"deny-set-webview-auto-resize\",\"description\":\"Denies the set_webview_auto_resize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_auto_resize\"]}},\"deny-set-webview-background-color\":{\"identifier\":\"deny-set-webview-background-color\",\"description\":\"Denies the set_webview_background_color command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_background_color\"]}},\"deny-set-webview-focus\":{\"identifier\":\"deny-set-webview-focus\",\"description\":\"Denies the set_webview_focus command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_focus\"]}},\"deny-set-webview-position\":{\"identifier\":\"deny-set-webview-position\",\"description\":\"Denies the set_webview_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_position\"]}},\"deny-set-webview-size\":{\"identifier\":\"deny-set-webview-size\",\"description\":\"Denies the set_webview_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_size\"]}},\"deny-set-webview-zoom\":{\"identifier\":\"deny-set-webview-zoom\",\"description\":\"Denies the set_webview_zoom command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_zoom\"]}},\"deny-webview-close\":{\"identifier\":\"deny-webview-close\",\"description\":\"Denies the webview_close command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_close\"]}},\"deny-webview-hide\":{\"identifier\":\"deny-webview-hide\",\"description\":\"Denies the webview_hide command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_hide\"]}},\"deny-webview-position\":{\"identifier\":\"deny-webview-position\",\"description\":\"Denies the webview_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_position\"]}},\"deny-webview-show\":{\"identifier\":\"deny-webview-show\",\"description\":\"Denies the webview_show command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_show\"]}},\"deny-webview-size\":{\"identifier\":\"deny-webview-size\",\"description\":\"Denies the webview_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_size\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:window\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin.\",\"permissions\":[\"allow-get-all-windows\",\"allow-scale-factor\",\"allow-inner-position\",\"allow-outer-position\",\"allow-inner-size\",\"allow-outer-size\",\"allow-is-fullscreen\",\"allow-is-minimized\",\"allow-is-maximized\",\"allow-is-focused\",\"allow-is-decorated\",\"allow-is-resizable\",\"allow-is-maximizable\",\"allow-is-minimizable\",\"allow-is-closable\",\"allow-is-visible\",\"allow-is-enabled\",\"allow-title\",\"allow-current-monitor\",\"allow-primary-monitor\",\"allow-monitor-from-point\",\"allow-available-monitors\",\"allow-cursor-position\",\"allow-theme\",\"allow-is-always-on-top\",\"allow-internal-toggle-maximize\"]},\"permissions\":{\"allow-available-monitors\":{\"identifier\":\"allow-available-monitors\",\"description\":\"Enables the available_monitors command without any pre-configured scope.\",\"commands\":{\"allow\":[\"available_monitors\"],\"deny\":[]}},\"allow-center\":{\"identifier\":\"allow-center\",\"description\":\"Enables the center command without any pre-configured scope.\",\"commands\":{\"allow\":[\"center\"],\"deny\":[]}},\"allow-close\":{\"identifier\":\"allow-close\",\"description\":\"Enables the close command without any pre-configured scope.\",\"commands\":{\"allow\":[\"close\"],\"deny\":[]}},\"allow-create\":{\"identifier\":\"allow-create\",\"description\":\"Enables the create command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create\"],\"deny\":[]}},\"allow-current-monitor\":{\"identifier\":\"allow-current-monitor\",\"description\":\"Enables the current_monitor command without any pre-configured scope.\",\"commands\":{\"allow\":[\"current_monitor\"],\"deny\":[]}},\"allow-cursor-position\":{\"identifier\":\"allow-cursor-position\",\"description\":\"Enables the cursor_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"cursor_position\"],\"deny\":[]}},\"allow-destroy\":{\"identifier\":\"allow-destroy\",\"description\":\"Enables the destroy command without any pre-configured scope.\",\"commands\":{\"allow\":[\"destroy\"],\"deny\":[]}},\"allow-get-all-windows\":{\"identifier\":\"allow-get-all-windows\",\"description\":\"Enables the get_all_windows command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_all_windows\"],\"deny\":[]}},\"allow-hide\":{\"identifier\":\"allow-hide\",\"description\":\"Enables the hide command without any pre-configured scope.\",\"commands\":{\"allow\":[\"hide\"],\"deny\":[]}},\"allow-inner-position\":{\"identifier\":\"allow-inner-position\",\"description\":\"Enables the inner_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"inner_position\"],\"deny\":[]}},\"allow-inner-size\":{\"identifier\":\"allow-inner-size\",\"description\":\"Enables the inner_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"inner_size\"],\"deny\":[]}},\"allow-internal-toggle-maximize\":{\"identifier\":\"allow-internal-toggle-maximize\",\"description\":\"Enables the internal_toggle_maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"internal_toggle_maximize\"],\"deny\":[]}},\"allow-is-always-on-top\":{\"identifier\":\"allow-is-always-on-top\",\"description\":\"Enables the is_always_on_top command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_always_on_top\"],\"deny\":[]}},\"allow-is-closable\":{\"identifier\":\"allow-is-closable\",\"description\":\"Enables the is_closable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_closable\"],\"deny\":[]}},\"allow-is-decorated\":{\"identifier\":\"allow-is-decorated\",\"description\":\"Enables the is_decorated command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_decorated\"],\"deny\":[]}},\"allow-is-enabled\":{\"identifier\":\"allow-is-enabled\",\"description\":\"Enables the is_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_enabled\"],\"deny\":[]}},\"allow-is-focused\":{\"identifier\":\"allow-is-focused\",\"description\":\"Enables the is_focused command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_focused\"],\"deny\":[]}},\"allow-is-fullscreen\":{\"identifier\":\"allow-is-fullscreen\",\"description\":\"Enables the is_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_fullscreen\"],\"deny\":[]}},\"allow-is-maximizable\":{\"identifier\":\"allow-is-maximizable\",\"description\":\"Enables the is_maximizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_maximizable\"],\"deny\":[]}},\"allow-is-maximized\":{\"identifier\":\"allow-is-maximized\",\"description\":\"Enables the is_maximized command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_maximized\"],\"deny\":[]}},\"allow-is-minimizable\":{\"identifier\":\"allow-is-minimizable\",\"description\":\"Enables the is_minimizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_minimizable\"],\"deny\":[]}},\"allow-is-minimized\":{\"identifier\":\"allow-is-minimized\",\"description\":\"Enables the is_minimized command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_minimized\"],\"deny\":[]}},\"allow-is-resizable\":{\"identifier\":\"allow-is-resizable\",\"description\":\"Enables the is_resizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_resizable\"],\"deny\":[]}},\"allow-is-visible\":{\"identifier\":\"allow-is-visible\",\"description\":\"Enables the is_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_visible\"],\"deny\":[]}},\"allow-maximize\":{\"identifier\":\"allow-maximize\",\"description\":\"Enables the maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"maximize\"],\"deny\":[]}},\"allow-minimize\":{\"identifier\":\"allow-minimize\",\"description\":\"Enables the minimize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"minimize\"],\"deny\":[]}},\"allow-monitor-from-point\":{\"identifier\":\"allow-monitor-from-point\",\"description\":\"Enables the monitor_from_point command without any pre-configured scope.\",\"commands\":{\"allow\":[\"monitor_from_point\"],\"deny\":[]}},\"allow-outer-position\":{\"identifier\":\"allow-outer-position\",\"description\":\"Enables the outer_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"outer_position\"],\"deny\":[]}},\"allow-outer-size\":{\"identifier\":\"allow-outer-size\",\"description\":\"Enables the outer_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"outer_size\"],\"deny\":[]}},\"allow-primary-monitor\":{\"identifier\":\"allow-primary-monitor\",\"description\":\"Enables the primary_monitor command without any pre-configured scope.\",\"commands\":{\"allow\":[\"primary_monitor\"],\"deny\":[]}},\"allow-request-user-attention\":{\"identifier\":\"allow-request-user-attention\",\"description\":\"Enables the request_user_attention command without any pre-configured scope.\",\"commands\":{\"allow\":[\"request_user_attention\"],\"deny\":[]}},\"allow-scale-factor\":{\"identifier\":\"allow-scale-factor\",\"description\":\"Enables the scale_factor command without any pre-configured scope.\",\"commands\":{\"allow\":[\"scale_factor\"],\"deny\":[]}},\"allow-set-always-on-bottom\":{\"identifier\":\"allow-set-always-on-bottom\",\"description\":\"Enables the set_always_on_bottom command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_always_on_bottom\"],\"deny\":[]}},\"allow-set-always-on-top\":{\"identifier\":\"allow-set-always-on-top\",\"description\":\"Enables the set_always_on_top command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_always_on_top\"],\"deny\":[]}},\"allow-set-background-color\":{\"identifier\":\"allow-set-background-color\",\"description\":\"Enables the set_background_color command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_background_color\"],\"deny\":[]}},\"allow-set-badge-count\":{\"identifier\":\"allow-set-badge-count\",\"description\":\"Enables the set_badge_count command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_badge_count\"],\"deny\":[]}},\"allow-set-badge-label\":{\"identifier\":\"allow-set-badge-label\",\"description\":\"Enables the set_badge_label command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_badge_label\"],\"deny\":[]}},\"allow-set-closable\":{\"identifier\":\"allow-set-closable\",\"description\":\"Enables the set_closable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_closable\"],\"deny\":[]}},\"allow-set-content-protected\":{\"identifier\":\"allow-set-content-protected\",\"description\":\"Enables the set_content_protected command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_content_protected\"],\"deny\":[]}},\"allow-set-cursor-grab\":{\"identifier\":\"allow-set-cursor-grab\",\"description\":\"Enables the set_cursor_grab command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_cursor_grab\"],\"deny\":[]}},\"allow-set-cursor-icon\":{\"identifier\":\"allow-set-cursor-icon\",\"description\":\"Enables the set_cursor_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_cursor_icon\"],\"deny\":[]}},\"allow-set-cursor-position\":{\"identifier\":\"allow-set-cursor-position\",\"description\":\"Enables the set_cursor_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_cursor_position\"],\"deny\":[]}},\"allow-set-cursor-visible\":{\"identifier\":\"allow-set-cursor-visible\",\"description\":\"Enables the set_cursor_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_cursor_visible\"],\"deny\":[]}},\"allow-set-decorations\":{\"identifier\":\"allow-set-decorations\",\"description\":\"Enables the set_decorations command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_decorations\"],\"deny\":[]}},\"allow-set-effects\":{\"identifier\":\"allow-set-effects\",\"description\":\"Enables the set_effects command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_effects\"],\"deny\":[]}},\"allow-set-enabled\":{\"identifier\":\"allow-set-enabled\",\"description\":\"Enables the set_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_enabled\"],\"deny\":[]}},\"allow-set-focus\":{\"identifier\":\"allow-set-focus\",\"description\":\"Enables the set_focus command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_focus\"],\"deny\":[]}},\"allow-set-focusable\":{\"identifier\":\"allow-set-focusable\",\"description\":\"Enables the set_focusable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_focusable\"],\"deny\":[]}},\"allow-set-fullscreen\":{\"identifier\":\"allow-set-fullscreen\",\"description\":\"Enables the set_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_fullscreen\"],\"deny\":[]}},\"allow-set-icon\":{\"identifier\":\"allow-set-icon\",\"description\":\"Enables the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_icon\"],\"deny\":[]}},\"allow-set-ignore-cursor-events\":{\"identifier\":\"allow-set-ignore-cursor-events\",\"description\":\"Enables the set_ignore_cursor_events command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_ignore_cursor_events\"],\"deny\":[]}},\"allow-set-max-size\":{\"identifier\":\"allow-set-max-size\",\"description\":\"Enables the set_max_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_max_size\"],\"deny\":[]}},\"allow-set-maximizable\":{\"identifier\":\"allow-set-maximizable\",\"description\":\"Enables the set_maximizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_maximizable\"],\"deny\":[]}},\"allow-set-min-size\":{\"identifier\":\"allow-set-min-size\",\"description\":\"Enables the set_min_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_min_size\"],\"deny\":[]}},\"allow-set-minimizable\":{\"identifier\":\"allow-set-minimizable\",\"description\":\"Enables the set_minimizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_minimizable\"],\"deny\":[]}},\"allow-set-overlay-icon\":{\"identifier\":\"allow-set-overlay-icon\",\"description\":\"Enables the set_overlay_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_overlay_icon\"],\"deny\":[]}},\"allow-set-position\":{\"identifier\":\"allow-set-position\",\"description\":\"Enables the set_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_position\"],\"deny\":[]}},\"allow-set-progress-bar\":{\"identifier\":\"allow-set-progress-bar\",\"description\":\"Enables the set_progress_bar command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_progress_bar\"],\"deny\":[]}},\"allow-set-resizable\":{\"identifier\":\"allow-set-resizable\",\"description\":\"Enables the set_resizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_resizable\"],\"deny\":[]}},\"allow-set-shadow\":{\"identifier\":\"allow-set-shadow\",\"description\":\"Enables the set_shadow command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_shadow\"],\"deny\":[]}},\"allow-set-simple-fullscreen\":{\"identifier\":\"allow-set-simple-fullscreen\",\"description\":\"Enables the set_simple_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_simple_fullscreen\"],\"deny\":[]}},\"allow-set-size\":{\"identifier\":\"allow-set-size\",\"description\":\"Enables the set_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_size\"],\"deny\":[]}},\"allow-set-size-constraints\":{\"identifier\":\"allow-set-size-constraints\",\"description\":\"Enables the set_size_constraints command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_size_constraints\"],\"deny\":[]}},\"allow-set-skip-taskbar\":{\"identifier\":\"allow-set-skip-taskbar\",\"description\":\"Enables the set_skip_taskbar command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_skip_taskbar\"],\"deny\":[]}},\"allow-set-theme\":{\"identifier\":\"allow-set-theme\",\"description\":\"Enables the set_theme command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_theme\"],\"deny\":[]}},\"allow-set-title\":{\"identifier\":\"allow-set-title\",\"description\":\"Enables the set_title command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_title\"],\"deny\":[]}},\"allow-set-title-bar-style\":{\"identifier\":\"allow-set-title-bar-style\",\"description\":\"Enables the set_title_bar_style command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_title_bar_style\"],\"deny\":[]}},\"allow-set-visible-on-all-workspaces\":{\"identifier\":\"allow-set-visible-on-all-workspaces\",\"description\":\"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_visible_on_all_workspaces\"],\"deny\":[]}},\"allow-show\":{\"identifier\":\"allow-show\",\"description\":\"Enables the show command without any pre-configured scope.\",\"commands\":{\"allow\":[\"show\"],\"deny\":[]}},\"allow-start-dragging\":{\"identifier\":\"allow-start-dragging\",\"description\":\"Enables the start_dragging command without any pre-configured scope.\",\"commands\":{\"allow\":[\"start_dragging\"],\"deny\":[]}},\"allow-start-resize-dragging\":{\"identifier\":\"allow-start-resize-dragging\",\"description\":\"Enables the start_resize_dragging command without any pre-configured scope.\",\"commands\":{\"allow\":[\"start_resize_dragging\"],\"deny\":[]}},\"allow-theme\":{\"identifier\":\"allow-theme\",\"description\":\"Enables the theme command without any pre-configured scope.\",\"commands\":{\"allow\":[\"theme\"],\"deny\":[]}},\"allow-title\":{\"identifier\":\"allow-title\",\"description\":\"Enables the title command without any pre-configured scope.\",\"commands\":{\"allow\":[\"title\"],\"deny\":[]}},\"allow-toggle-maximize\":{\"identifier\":\"allow-toggle-maximize\",\"description\":\"Enables the toggle_maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"toggle_maximize\"],\"deny\":[]}},\"allow-unmaximize\":{\"identifier\":\"allow-unmaximize\",\"description\":\"Enables the unmaximize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unmaximize\"],\"deny\":[]}},\"allow-unminimize\":{\"identifier\":\"allow-unminimize\",\"description\":\"Enables the unminimize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unminimize\"],\"deny\":[]}},\"deny-available-monitors\":{\"identifier\":\"deny-available-monitors\",\"description\":\"Denies the available_monitors command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"available_monitors\"]}},\"deny-center\":{\"identifier\":\"deny-center\",\"description\":\"Denies the center command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"center\"]}},\"deny-close\":{\"identifier\":\"deny-close\",\"description\":\"Denies the close command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"close\"]}},\"deny-create\":{\"identifier\":\"deny-create\",\"description\":\"Denies the create command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create\"]}},\"deny-current-monitor\":{\"identifier\":\"deny-current-monitor\",\"description\":\"Denies the current_monitor command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"current_monitor\"]}},\"deny-cursor-position\":{\"identifier\":\"deny-cursor-position\",\"description\":\"Denies the cursor_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"cursor_position\"]}},\"deny-destroy\":{\"identifier\":\"deny-destroy\",\"description\":\"Denies the destroy command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"destroy\"]}},\"deny-get-all-windows\":{\"identifier\":\"deny-get-all-windows\",\"description\":\"Denies the get_all_windows command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_all_windows\"]}},\"deny-hide\":{\"identifier\":\"deny-hide\",\"description\":\"Denies the hide command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"hide\"]}},\"deny-inner-position\":{\"identifier\":\"deny-inner-position\",\"description\":\"Denies the inner_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"inner_position\"]}},\"deny-inner-size\":{\"identifier\":\"deny-inner-size\",\"description\":\"Denies the inner_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"inner_size\"]}},\"deny-internal-toggle-maximize\":{\"identifier\":\"deny-internal-toggle-maximize\",\"description\":\"Denies the internal_toggle_maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"internal_toggle_maximize\"]}},\"deny-is-always-on-top\":{\"identifier\":\"deny-is-always-on-top\",\"description\":\"Denies the is_always_on_top command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_always_on_top\"]}},\"deny-is-closable\":{\"identifier\":\"deny-is-closable\",\"description\":\"Denies the is_closable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_closable\"]}},\"deny-is-decorated\":{\"identifier\":\"deny-is-decorated\",\"description\":\"Denies the is_decorated command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_decorated\"]}},\"deny-is-enabled\":{\"identifier\":\"deny-is-enabled\",\"description\":\"Denies the is_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_enabled\"]}},\"deny-is-focused\":{\"identifier\":\"deny-is-focused\",\"description\":\"Denies the is_focused command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_focused\"]}},\"deny-is-fullscreen\":{\"identifier\":\"deny-is-fullscreen\",\"description\":\"Denies the is_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_fullscreen\"]}},\"deny-is-maximizable\":{\"identifier\":\"deny-is-maximizable\",\"description\":\"Denies the is_maximizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_maximizable\"]}},\"deny-is-maximized\":{\"identifier\":\"deny-is-maximized\",\"description\":\"Denies the is_maximized command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_maximized\"]}},\"deny-is-minimizable\":{\"identifier\":\"deny-is-minimizable\",\"description\":\"Denies the is_minimizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_minimizable\"]}},\"deny-is-minimized\":{\"identifier\":\"deny-is-minimized\",\"description\":\"Denies the is_minimized command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_minimized\"]}},\"deny-is-resizable\":{\"identifier\":\"deny-is-resizable\",\"description\":\"Denies the is_resizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_resizable\"]}},\"deny-is-visible\":{\"identifier\":\"deny-is-visible\",\"description\":\"Denies the is_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_visible\"]}},\"deny-maximize\":{\"identifier\":\"deny-maximize\",\"description\":\"Denies the maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"maximize\"]}},\"deny-minimize\":{\"identifier\":\"deny-minimize\",\"description\":\"Denies the minimize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"minimize\"]}},\"deny-monitor-from-point\":{\"identifier\":\"deny-monitor-from-point\",\"description\":\"Denies the monitor_from_point command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"monitor_from_point\"]}},\"deny-outer-position\":{\"identifier\":\"deny-outer-position\",\"description\":\"Denies the outer_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"outer_position\"]}},\"deny-outer-size\":{\"identifier\":\"deny-outer-size\",\"description\":\"Denies the outer_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"outer_size\"]}},\"deny-primary-monitor\":{\"identifier\":\"deny-primary-monitor\",\"description\":\"Denies the primary_monitor command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"primary_monitor\"]}},\"deny-request-user-attention\":{\"identifier\":\"deny-request-user-attention\",\"description\":\"Denies the request_user_attention command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"request_user_attention\"]}},\"deny-scale-factor\":{\"identifier\":\"deny-scale-factor\",\"description\":\"Denies the scale_factor command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"scale_factor\"]}},\"deny-set-always-on-bottom\":{\"identifier\":\"deny-set-always-on-bottom\",\"description\":\"Denies the set_always_on_bottom command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_always_on_bottom\"]}},\"deny-set-always-on-top\":{\"identifier\":\"deny-set-always-on-top\",\"description\":\"Denies the set_always_on_top command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_always_on_top\"]}},\"deny-set-background-color\":{\"identifier\":\"deny-set-background-color\",\"description\":\"Denies the set_background_color command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_background_color\"]}},\"deny-set-badge-count\":{\"identifier\":\"deny-set-badge-count\",\"description\":\"Denies the set_badge_count command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_badge_count\"]}},\"deny-set-badge-label\":{\"identifier\":\"deny-set-badge-label\",\"description\":\"Denies the set_badge_label command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_badge_label\"]}},\"deny-set-closable\":{\"identifier\":\"deny-set-closable\",\"description\":\"Denies the set_closable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_closable\"]}},\"deny-set-content-protected\":{\"identifier\":\"deny-set-content-protected\",\"description\":\"Denies the set_content_protected command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_content_protected\"]}},\"deny-set-cursor-grab\":{\"identifier\":\"deny-set-cursor-grab\",\"description\":\"Denies the set_cursor_grab command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_cursor_grab\"]}},\"deny-set-cursor-icon\":{\"identifier\":\"deny-set-cursor-icon\",\"description\":\"Denies the set_cursor_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_cursor_icon\"]}},\"deny-set-cursor-position\":{\"identifier\":\"deny-set-cursor-position\",\"description\":\"Denies the set_cursor_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_cursor_position\"]}},\"deny-set-cursor-visible\":{\"identifier\":\"deny-set-cursor-visible\",\"description\":\"Denies the set_cursor_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_cursor_visible\"]}},\"deny-set-decorations\":{\"identifier\":\"deny-set-decorations\",\"description\":\"Denies the set_decorations command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_decorations\"]}},\"deny-set-effects\":{\"identifier\":\"deny-set-effects\",\"description\":\"Denies the set_effects command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_effects\"]}},\"deny-set-enabled\":{\"identifier\":\"deny-set-enabled\",\"description\":\"Denies the set_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_enabled\"]}},\"deny-set-focus\":{\"identifier\":\"deny-set-focus\",\"description\":\"Denies the set_focus command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_focus\"]}},\"deny-set-focusable\":{\"identifier\":\"deny-set-focusable\",\"description\":\"Denies the set_focusable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_focusable\"]}},\"deny-set-fullscreen\":{\"identifier\":\"deny-set-fullscreen\",\"description\":\"Denies the set_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_fullscreen\"]}},\"deny-set-icon\":{\"identifier\":\"deny-set-icon\",\"description\":\"Denies the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_icon\"]}},\"deny-set-ignore-cursor-events\":{\"identifier\":\"deny-set-ignore-cursor-events\",\"description\":\"Denies the set_ignore_cursor_events command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_ignore_cursor_events\"]}},\"deny-set-max-size\":{\"identifier\":\"deny-set-max-size\",\"description\":\"Denies the set_max_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_max_size\"]}},\"deny-set-maximizable\":{\"identifier\":\"deny-set-maximizable\",\"description\":\"Denies the set_maximizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_maximizable\"]}},\"deny-set-min-size\":{\"identifier\":\"deny-set-min-size\",\"description\":\"Denies the set_min_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_min_size\"]}},\"deny-set-minimizable\":{\"identifier\":\"deny-set-minimizable\",\"description\":\"Denies the set_minimizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_minimizable\"]}},\"deny-set-overlay-icon\":{\"identifier\":\"deny-set-overlay-icon\",\"description\":\"Denies the set_overlay_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_overlay_icon\"]}},\"deny-set-position\":{\"identifier\":\"deny-set-position\",\"description\":\"Denies the set_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_position\"]}},\"deny-set-progress-bar\":{\"identifier\":\"deny-set-progress-bar\",\"description\":\"Denies the set_progress_bar command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_progress_bar\"]}},\"deny-set-resizable\":{\"identifier\":\"deny-set-resizable\",\"description\":\"Denies the set_resizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_resizable\"]}},\"deny-set-shadow\":{\"identifier\":\"deny-set-shadow\",\"description\":\"Denies the set_shadow command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_shadow\"]}},\"deny-set-simple-fullscreen\":{\"identifier\":\"deny-set-simple-fullscreen\",\"description\":\"Denies the set_simple_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_simple_fullscreen\"]}},\"deny-set-size\":{\"identifier\":\"deny-set-size\",\"description\":\"Denies the set_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_size\"]}},\"deny-set-size-constraints\":{\"identifier\":\"deny-set-size-constraints\",\"description\":\"Denies the set_size_constraints command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_size_constraints\"]}},\"deny-set-skip-taskbar\":{\"identifier\":\"deny-set-skip-taskbar\",\"description\":\"Denies the set_skip_taskbar command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_skip_taskbar\"]}},\"deny-set-theme\":{\"identifier\":\"deny-set-theme\",\"description\":\"Denies the set_theme command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_theme\"]}},\"deny-set-title\":{\"identifier\":\"deny-set-title\",\"description\":\"Denies the set_title command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_title\"]}},\"deny-set-title-bar-style\":{\"identifier\":\"deny-set-title-bar-style\",\"description\":\"Denies the set_title_bar_style command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_title_bar_style\"]}},\"deny-set-visible-on-all-workspaces\":{\"identifier\":\"deny-set-visible-on-all-workspaces\",\"description\":\"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_visible_on_all_workspaces\"]}},\"deny-show\":{\"identifier\":\"deny-show\",\"description\":\"Denies the show command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"show\"]}},\"deny-start-dragging\":{\"identifier\":\"deny-start-dragging\",\"description\":\"Denies the start_dragging command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"start_dragging\"]}},\"deny-start-resize-dragging\":{\"identifier\":\"deny-start-resize-dragging\",\"description\":\"Denies the start_resize_dragging command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"start_resize_dragging\"]}},\"deny-theme\":{\"identifier\":\"deny-theme\",\"description\":\"Denies the theme command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"theme\"]}},\"deny-title\":{\"identifier\":\"deny-title\",\"description\":\"Denies the title command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"title\"]}},\"deny-toggle-maximize\":{\"identifier\":\"deny-toggle-maximize\",\"description\":\"Denies the toggle_maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"toggle_maximize\"]}},\"deny-unmaximize\":{\"identifier\":\"deny-unmaximize\",\"description\":\"Denies the unmaximize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unmaximize\"]}},\"deny-unminimize\":{\"identifier\":\"deny-unminimize\",\"description\":\"Denies the unminimize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unminimize\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"dialog\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\",\"permissions\":[\"allow-ask\",\"allow-confirm\",\"allow-message\",\"allow-save\",\"allow-open\"]},\"permissions\":{\"allow-ask\":{\"identifier\":\"allow-ask\",\"description\":\"Enables the ask command without any pre-configured scope.\",\"commands\":{\"allow\":[\"ask\"],\"deny\":[]}},\"allow-confirm\":{\"identifier\":\"allow-confirm\",\"description\":\"Enables the confirm command without any pre-configured scope.\",\"commands\":{\"allow\":[\"confirm\"],\"deny\":[]}},\"allow-message\":{\"identifier\":\"allow-message\",\"description\":\"Enables the message command without any pre-configured scope.\",\"commands\":{\"allow\":[\"message\"],\"deny\":[]}},\"allow-open\":{\"identifier\":\"allow-open\",\"description\":\"Enables the open command without any pre-configured scope.\",\"commands\":{\"allow\":[\"open\"],\"deny\":[]}},\"allow-save\":{\"identifier\":\"allow-save\",\"description\":\"Enables the save command without any pre-configured scope.\",\"commands\":{\"allow\":[\"save\"],\"deny\":[]}},\"deny-ask\":{\"identifier\":\"deny-ask\",\"description\":\"Denies the ask command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"ask\"]}},\"deny-confirm\":{\"identifier\":\"deny-confirm\",\"description\":\"Denies the confirm command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"confirm\"]}},\"deny-message\":{\"identifier\":\"deny-message\",\"description\":\"Denies the message command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"message\"]}},\"deny-open\":{\"identifier\":\"deny-open\",\"description\":\"Denies the open command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"open\"]}},\"deny-save\":{\"identifier\":\"deny-save\",\"description\":\"Denies the save command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"save\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"fs\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\",\"permissions\":[\"create-app-specific-dirs\",\"read-app-specific-dirs-recursive\",\"deny-default\"]},\"permissions\":{\"allow-copy-file\":{\"identifier\":\"allow-copy-file\",\"description\":\"Enables the copy_file command without any pre-configured scope.\",\"commands\":{\"allow\":[\"copy_file\"],\"deny\":[]}},\"allow-create\":{\"identifier\":\"allow-create\",\"description\":\"Enables the create command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create\"],\"deny\":[]}},\"allow-exists\":{\"identifier\":\"allow-exists\",\"description\":\"Enables the exists command without any pre-configured scope.\",\"commands\":{\"allow\":[\"exists\"],\"deny\":[]}},\"allow-fstat\":{\"identifier\":\"allow-fstat\",\"description\":\"Enables the fstat command without any pre-configured scope.\",\"commands\":{\"allow\":[\"fstat\"],\"deny\":[]}},\"allow-ftruncate\":{\"identifier\":\"allow-ftruncate\",\"description\":\"Enables the ftruncate command without any pre-configured scope.\",\"commands\":{\"allow\":[\"ftruncate\"],\"deny\":[]}},\"allow-lstat\":{\"identifier\":\"allow-lstat\",\"description\":\"Enables the lstat command without any pre-configured scope.\",\"commands\":{\"allow\":[\"lstat\"],\"deny\":[]}},\"allow-mkdir\":{\"identifier\":\"allow-mkdir\",\"description\":\"Enables the mkdir command without any pre-configured scope.\",\"commands\":{\"allow\":[\"mkdir\"],\"deny\":[]}},\"allow-open\":{\"identifier\":\"allow-open\",\"description\":\"Enables the open command without any pre-configured scope.\",\"commands\":{\"allow\":[\"open\"],\"deny\":[]}},\"allow-read\":{\"identifier\":\"allow-read\",\"description\":\"Enables the read command without any pre-configured scope.\",\"commands\":{\"allow\":[\"read\"],\"deny\":[]}},\"allow-read-dir\":{\"identifier\":\"allow-read-dir\",\"description\":\"Enables the read_dir command without any pre-configured scope.\",\"commands\":{\"allow\":[\"read_dir\"],\"deny\":[]}},\"allow-read-file\":{\"identifier\":\"allow-read-file\",\"description\":\"Enables the read_file command without any pre-configured scope.\",\"commands\":{\"allow\":[\"read_file\"],\"deny\":[]}},\"allow-read-text-file\":{\"identifier\":\"allow-read-text-file\",\"description\":\"Enables the read_text_file command without any pre-configured scope.\",\"commands\":{\"allow\":[\"read_text_file\"],\"deny\":[]}},\"allow-read-text-file-lines\":{\"identifier\":\"allow-read-text-file-lines\",\"description\":\"Enables the read_text_file_lines command without any pre-configured scope.\",\"commands\":{\"allow\":[\"read_text_file_lines\",\"read_text_file_lines_next\"],\"deny\":[]}},\"allow-read-text-file-lines-next\":{\"identifier\":\"allow-read-text-file-lines-next\",\"description\":\"Enables the read_text_file_lines_next command without any pre-configured scope.\",\"commands\":{\"allow\":[\"read_text_file_lines_next\"],\"deny\":[]}},\"allow-remove\":{\"identifier\":\"allow-remove\",\"description\":\"Enables the remove command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove\"],\"deny\":[]}},\"allow-rename\":{\"identifier\":\"allow-rename\",\"description\":\"Enables the rename command without any pre-configured scope.\",\"commands\":{\"allow\":[\"rename\"],\"deny\":[]}},\"allow-seek\":{\"identifier\":\"allow-seek\",\"description\":\"Enables the seek command without any pre-configured scope.\",\"commands\":{\"allow\":[\"seek\"],\"deny\":[]}},\"allow-size\":{\"identifier\":\"allow-size\",\"description\":\"Enables the size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"size\"],\"deny\":[]}},\"allow-stat\":{\"identifier\":\"allow-stat\",\"description\":\"Enables the stat command without any pre-configured scope.\",\"commands\":{\"allow\":[\"stat\"],\"deny\":[]}},\"allow-truncate\":{\"identifier\":\"allow-truncate\",\"description\":\"Enables the truncate command without any pre-configured scope.\",\"commands\":{\"allow\":[\"truncate\"],\"deny\":[]}},\"allow-unwatch\":{\"identifier\":\"allow-unwatch\",\"description\":\"Enables the unwatch command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unwatch\"],\"deny\":[]}},\"allow-watch\":{\"identifier\":\"allow-watch\",\"description\":\"Enables the watch command without any pre-configured scope.\",\"commands\":{\"allow\":[\"watch\"],\"deny\":[]}},\"allow-write\":{\"identifier\":\"allow-write\",\"description\":\"Enables the write command without any pre-configured scope.\",\"commands\":{\"allow\":[\"write\"],\"deny\":[]}},\"allow-write-file\":{\"identifier\":\"allow-write-file\",\"description\":\"Enables the write_file command without any pre-configured scope.\",\"commands\":{\"allow\":[\"write_file\",\"open\",\"write\"],\"deny\":[]}},\"allow-write-text-file\":{\"identifier\":\"allow-write-text-file\",\"description\":\"Enables the write_text_file command without any pre-configured scope.\",\"commands\":{\"allow\":[\"write_text_file\"],\"deny\":[]}},\"create-app-specific-dirs\":{\"identifier\":\"create-app-specific-dirs\",\"description\":\"This permissions allows to create the application specific directories.\\n\",\"commands\":{\"allow\":[\"mkdir\",\"scope-app-index\"],\"deny\":[]}},\"deny-copy-file\":{\"identifier\":\"deny-copy-file\",\"description\":\"Denies the copy_file command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"copy_file\"]}},\"deny-create\":{\"identifier\":\"deny-create\",\"description\":\"Denies the create command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create\"]}},\"deny-exists\":{\"identifier\":\"deny-exists\",\"description\":\"Denies the exists command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"exists\"]}},\"deny-fstat\":{\"identifier\":\"deny-fstat\",\"description\":\"Denies the fstat command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"fstat\"]}},\"deny-ftruncate\":{\"identifier\":\"deny-ftruncate\",\"description\":\"Denies the ftruncate command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"ftruncate\"]}},\"deny-lstat\":{\"identifier\":\"deny-lstat\",\"description\":\"Denies the lstat command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"lstat\"]}},\"deny-mkdir\":{\"identifier\":\"deny-mkdir\",\"description\":\"Denies the mkdir command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"mkdir\"]}},\"deny-open\":{\"identifier\":\"deny-open\",\"description\":\"Denies the open command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"open\"]}},\"deny-read\":{\"identifier\":\"deny-read\",\"description\":\"Denies the read command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"read\"]}},\"deny-read-dir\":{\"identifier\":\"deny-read-dir\",\"description\":\"Denies the read_dir command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"read_dir\"]}},\"deny-read-file\":{\"identifier\":\"deny-read-file\",\"description\":\"Denies the read_file command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"read_file\"]}},\"deny-read-text-file\":{\"identifier\":\"deny-read-text-file\",\"description\":\"Denies the read_text_file command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"read_text_file\"]}},\"deny-read-text-file-lines\":{\"identifier\":\"deny-read-text-file-lines\",\"description\":\"Denies the read_text_file_lines command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"read_text_file_lines\"]}},\"deny-read-text-file-lines-next\":{\"identifier\":\"deny-read-text-file-lines-next\",\"description\":\"Denies the read_text_file_lines_next command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"read_text_file_lines_next\"]}},\"deny-remove\":{\"identifier\":\"deny-remove\",\"description\":\"Denies the remove command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove\"]}},\"deny-rename\":{\"identifier\":\"deny-rename\",\"description\":\"Denies the rename command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"rename\"]}},\"deny-seek\":{\"identifier\":\"deny-seek\",\"description\":\"Denies the seek command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"seek\"]}},\"deny-size\":{\"identifier\":\"deny-size\",\"description\":\"Denies the size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"size\"]}},\"deny-stat\":{\"identifier\":\"deny-stat\",\"description\":\"Denies the stat command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"stat\"]}},\"deny-truncate\":{\"identifier\":\"deny-truncate\",\"description\":\"Denies the truncate command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"truncate\"]}},\"deny-unwatch\":{\"identifier\":\"deny-unwatch\",\"description\":\"Denies the unwatch command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unwatch\"]}},\"deny-watch\":{\"identifier\":\"deny-watch\",\"description\":\"Denies the watch command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"watch\"]}},\"deny-webview-data-linux\":{\"identifier\":\"deny-webview-data-linux\",\"description\":\"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\"commands\":{\"allow\":[],\"deny\":[]}},\"deny-webview-data-windows\":{\"identifier\":\"deny-webview-data-windows\",\"description\":\"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\"commands\":{\"allow\":[],\"deny\":[]}},\"deny-write\":{\"identifier\":\"deny-write\",\"description\":\"Denies the write command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"write\"]}},\"deny-write-file\":{\"identifier\":\"deny-write-file\",\"description\":\"Denies the write_file command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"write_file\"]}},\"deny-write-text-file\":{\"identifier\":\"deny-write-text-file\",\"description\":\"Denies the write_text_file command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"write_text_file\"]}},\"read-all\":{\"identifier\":\"read-all\",\"description\":\"This enables all read related commands without any pre-configured accessible paths.\",\"commands\":{\"allow\":[\"read_dir\",\"read_file\",\"read\",\"open\",\"read_text_file\",\"read_text_file_lines\",\"read_text_file_lines_next\",\"seek\",\"stat\",\"lstat\",\"fstat\",\"exists\",\"watch\",\"unwatch\"],\"deny\":[]}},\"read-app-specific-dirs-recursive\":{\"identifier\":\"read-app-specific-dirs-recursive\",\"description\":\"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\",\"commands\":{\"allow\":[\"read_dir\",\"read_file\",\"read_text_file\",\"read_text_file_lines\",\"read_text_file_lines_next\",\"exists\",\"scope-app-recursive\"],\"deny\":[]}},\"read-dirs\":{\"identifier\":\"read-dirs\",\"description\":\"This enables directory read and file metadata related commands without any pre-configured accessible paths.\",\"commands\":{\"allow\":[\"read_dir\",\"stat\",\"lstat\",\"fstat\",\"exists\"],\"deny\":[]}},\"read-files\":{\"identifier\":\"read-files\",\"description\":\"This enables file read related commands without any pre-configured accessible paths.\",\"commands\":{\"allow\":[\"read_file\",\"read\",\"open\",\"read_text_file\",\"read_text_file_lines\",\"read_text_file_lines_next\",\"seek\",\"stat\",\"lstat\",\"fstat\",\"exists\"],\"deny\":[]}},\"read-meta\":{\"identifier\":\"read-meta\",\"description\":\"This enables all index or metadata related commands without any pre-configured accessible paths.\",\"commands\":{\"allow\":[\"read_dir\",\"stat\",\"lstat\",\"fstat\",\"exists\",\"size\"],\"deny\":[]}},\"scope\":{\"identifier\":\"scope\",\"description\":\"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\",\"commands\":{\"allow\":[],\"deny\":[]}},\"scope-app\":{\"identifier\":\"scope-app\",\"description\":\"This scope permits access to all files and list content of top level directories in the application folders.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCONFIG\"},{\"path\":\"$APPCONFIG/*\"},{\"path\":\"$APPDATA\"},{\"path\":\"$APPDATA/*\"},{\"path\":\"$APPLOCALDATA\"},{\"path\":\"$APPLOCALDATA/*\"},{\"path\":\"$APPCACHE\"},{\"path\":\"$APPCACHE/*\"},{\"path\":\"$APPLOG\"},{\"path\":\"$APPLOG/*\"}]}},\"scope-app-index\":{\"identifier\":\"scope-app-index\",\"description\":\"This scope permits to list all files and folders in the application directories.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCONFIG\"},{\"path\":\"$APPDATA\"},{\"path\":\"$APPLOCALDATA\"},{\"path\":\"$APPCACHE\"},{\"path\":\"$APPLOG\"}]}},\"scope-app-recursive\":{\"identifier\":\"scope-app-recursive\",\"description\":\"This scope permits recursive access to the complete application folders, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCONFIG\"},{\"path\":\"$APPCONFIG/**\"},{\"path\":\"$APPDATA\"},{\"path\":\"$APPDATA/**\"},{\"path\":\"$APPLOCALDATA\"},{\"path\":\"$APPLOCALDATA/**\"},{\"path\":\"$APPCACHE\"},{\"path\":\"$APPCACHE/**\"},{\"path\":\"$APPLOG\"},{\"path\":\"$APPLOG/**\"}]}},\"scope-appcache\":{\"identifier\":\"scope-appcache\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCACHE\"},{\"path\":\"$APPCACHE/*\"}]}},\"scope-appcache-index\":{\"identifier\":\"scope-appcache-index\",\"description\":\"This scope permits to list all files and folders in the `$APPCACHE`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCACHE\"}]}},\"scope-appcache-recursive\":{\"identifier\":\"scope-appcache-recursive\",\"description\":\"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCACHE\"},{\"path\":\"$APPCACHE/**\"}]}},\"scope-appconfig\":{\"identifier\":\"scope-appconfig\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCONFIG\"},{\"path\":\"$APPCONFIG/*\"}]}},\"scope-appconfig-index\":{\"identifier\":\"scope-appconfig-index\",\"description\":\"This scope permits to list all files and folders in the `$APPCONFIG`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCONFIG\"}]}},\"scope-appconfig-recursive\":{\"identifier\":\"scope-appconfig-recursive\",\"description\":\"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPCONFIG\"},{\"path\":\"$APPCONFIG/**\"}]}},\"scope-appdata\":{\"identifier\":\"scope-appdata\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPDATA\"},{\"path\":\"$APPDATA/*\"}]}},\"scope-appdata-index\":{\"identifier\":\"scope-appdata-index\",\"description\":\"This scope permits to list all files and folders in the `$APPDATA`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPDATA\"}]}},\"scope-appdata-recursive\":{\"identifier\":\"scope-appdata-recursive\",\"description\":\"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPDATA\"},{\"path\":\"$APPDATA/**\"}]}},\"scope-applocaldata\":{\"identifier\":\"scope-applocaldata\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPLOCALDATA\"},{\"path\":\"$APPLOCALDATA/*\"}]}},\"scope-applocaldata-index\":{\"identifier\":\"scope-applocaldata-index\",\"description\":\"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPLOCALDATA\"}]}},\"scope-applocaldata-recursive\":{\"identifier\":\"scope-applocaldata-recursive\",\"description\":\"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPLOCALDATA\"},{\"path\":\"$APPLOCALDATA/**\"}]}},\"scope-applog\":{\"identifier\":\"scope-applog\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPLOG\"},{\"path\":\"$APPLOG/*\"}]}},\"scope-applog-index\":{\"identifier\":\"scope-applog-index\",\"description\":\"This scope permits to list all files and folders in the `$APPLOG`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPLOG\"}]}},\"scope-applog-recursive\":{\"identifier\":\"scope-applog-recursive\",\"description\":\"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$APPLOG\"},{\"path\":\"$APPLOG/**\"}]}},\"scope-audio\":{\"identifier\":\"scope-audio\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$AUDIO\"},{\"path\":\"$AUDIO/*\"}]}},\"scope-audio-index\":{\"identifier\":\"scope-audio-index\",\"description\":\"This scope permits to list all files and folders in the `$AUDIO`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$AUDIO\"}]}},\"scope-audio-recursive\":{\"identifier\":\"scope-audio-recursive\",\"description\":\"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$AUDIO\"},{\"path\":\"$AUDIO/**\"}]}},\"scope-cache\":{\"identifier\":\"scope-cache\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$CACHE\"},{\"path\":\"$CACHE/*\"}]}},\"scope-cache-index\":{\"identifier\":\"scope-cache-index\",\"description\":\"This scope permits to list all files and folders in the `$CACHE`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$CACHE\"}]}},\"scope-cache-recursive\":{\"identifier\":\"scope-cache-recursive\",\"description\":\"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$CACHE\"},{\"path\":\"$CACHE/**\"}]}},\"scope-config\":{\"identifier\":\"scope-config\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$CONFIG\"},{\"path\":\"$CONFIG/*\"}]}},\"scope-config-index\":{\"identifier\":\"scope-config-index\",\"description\":\"This scope permits to list all files and folders in the `$CONFIG`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$CONFIG\"}]}},\"scope-config-recursive\":{\"identifier\":\"scope-config-recursive\",\"description\":\"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$CONFIG\"},{\"path\":\"$CONFIG/**\"}]}},\"scope-data\":{\"identifier\":\"scope-data\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DATA\"},{\"path\":\"$DATA/*\"}]}},\"scope-data-index\":{\"identifier\":\"scope-data-index\",\"description\":\"This scope permits to list all files and folders in the `$DATA`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DATA\"}]}},\"scope-data-recursive\":{\"identifier\":\"scope-data-recursive\",\"description\":\"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DATA\"},{\"path\":\"$DATA/**\"}]}},\"scope-desktop\":{\"identifier\":\"scope-desktop\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DESKTOP\"},{\"path\":\"$DESKTOP/*\"}]}},\"scope-desktop-index\":{\"identifier\":\"scope-desktop-index\",\"description\":\"This scope permits to list all files and folders in the `$DESKTOP`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DESKTOP\"}]}},\"scope-desktop-recursive\":{\"identifier\":\"scope-desktop-recursive\",\"description\":\"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DESKTOP\"},{\"path\":\"$DESKTOP/**\"}]}},\"scope-document\":{\"identifier\":\"scope-document\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DOCUMENT\"},{\"path\":\"$DOCUMENT/*\"}]}},\"scope-document-index\":{\"identifier\":\"scope-document-index\",\"description\":\"This scope permits to list all files and folders in the `$DOCUMENT`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DOCUMENT\"}]}},\"scope-document-recursive\":{\"identifier\":\"scope-document-recursive\",\"description\":\"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DOCUMENT\"},{\"path\":\"$DOCUMENT/**\"}]}},\"scope-download\":{\"identifier\":\"scope-download\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DOWNLOAD\"},{\"path\":\"$DOWNLOAD/*\"}]}},\"scope-download-index\":{\"identifier\":\"scope-download-index\",\"description\":\"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DOWNLOAD\"}]}},\"scope-download-recursive\":{\"identifier\":\"scope-download-recursive\",\"description\":\"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$DOWNLOAD\"},{\"path\":\"$DOWNLOAD/**\"}]}},\"scope-exe\":{\"identifier\":\"scope-exe\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$EXE\"},{\"path\":\"$EXE/*\"}]}},\"scope-exe-index\":{\"identifier\":\"scope-exe-index\",\"description\":\"This scope permits to list all files and folders in the `$EXE`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$EXE\"}]}},\"scope-exe-recursive\":{\"identifier\":\"scope-exe-recursive\",\"description\":\"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$EXE\"},{\"path\":\"$EXE/**\"}]}},\"scope-font\":{\"identifier\":\"scope-font\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$FONT\"},{\"path\":\"$FONT/*\"}]}},\"scope-font-index\":{\"identifier\":\"scope-font-index\",\"description\":\"This scope permits to list all files and folders in the `$FONT`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$FONT\"}]}},\"scope-font-recursive\":{\"identifier\":\"scope-font-recursive\",\"description\":\"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$FONT\"},{\"path\":\"$FONT/**\"}]}},\"scope-home\":{\"identifier\":\"scope-home\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$HOME\"},{\"path\":\"$HOME/*\"}]}},\"scope-home-index\":{\"identifier\":\"scope-home-index\",\"description\":\"This scope permits to list all files and folders in the `$HOME`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$HOME\"}]}},\"scope-home-recursive\":{\"identifier\":\"scope-home-recursive\",\"description\":\"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$HOME\"},{\"path\":\"$HOME/**\"}]}},\"scope-localdata\":{\"identifier\":\"scope-localdata\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$LOCALDATA\"},{\"path\":\"$LOCALDATA/*\"}]}},\"scope-localdata-index\":{\"identifier\":\"scope-localdata-index\",\"description\":\"This scope permits to list all files and folders in the `$LOCALDATA`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$LOCALDATA\"}]}},\"scope-localdata-recursive\":{\"identifier\":\"scope-localdata-recursive\",\"description\":\"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$LOCALDATA\"},{\"path\":\"$LOCALDATA/**\"}]}},\"scope-log\":{\"identifier\":\"scope-log\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$LOG\"},{\"path\":\"$LOG/*\"}]}},\"scope-log-index\":{\"identifier\":\"scope-log-index\",\"description\":\"This scope permits to list all files and folders in the `$LOG`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$LOG\"}]}},\"scope-log-recursive\":{\"identifier\":\"scope-log-recursive\",\"description\":\"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$LOG\"},{\"path\":\"$LOG/**\"}]}},\"scope-picture\":{\"identifier\":\"scope-picture\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$PICTURE\"},{\"path\":\"$PICTURE/*\"}]}},\"scope-picture-index\":{\"identifier\":\"scope-picture-index\",\"description\":\"This scope permits to list all files and folders in the `$PICTURE`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$PICTURE\"}]}},\"scope-picture-recursive\":{\"identifier\":\"scope-picture-recursive\",\"description\":\"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$PICTURE\"},{\"path\":\"$PICTURE/**\"}]}},\"scope-public\":{\"identifier\":\"scope-public\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$PUBLIC\"},{\"path\":\"$PUBLIC/*\"}]}},\"scope-public-index\":{\"identifier\":\"scope-public-index\",\"description\":\"This scope permits to list all files and folders in the `$PUBLIC`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$PUBLIC\"}]}},\"scope-public-recursive\":{\"identifier\":\"scope-public-recursive\",\"description\":\"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$PUBLIC\"},{\"path\":\"$PUBLIC/**\"}]}},\"scope-resource\":{\"identifier\":\"scope-resource\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$RESOURCE\"},{\"path\":\"$RESOURCE/*\"}]}},\"scope-resource-index\":{\"identifier\":\"scope-resource-index\",\"description\":\"This scope permits to list all files and folders in the `$RESOURCE`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$RESOURCE\"}]}},\"scope-resource-recursive\":{\"identifier\":\"scope-resource-recursive\",\"description\":\"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$RESOURCE\"},{\"path\":\"$RESOURCE/**\"}]}},\"scope-runtime\":{\"identifier\":\"scope-runtime\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$RUNTIME\"},{\"path\":\"$RUNTIME/*\"}]}},\"scope-runtime-index\":{\"identifier\":\"scope-runtime-index\",\"description\":\"This scope permits to list all files and folders in the `$RUNTIME`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$RUNTIME\"}]}},\"scope-runtime-recursive\":{\"identifier\":\"scope-runtime-recursive\",\"description\":\"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$RUNTIME\"},{\"path\":\"$RUNTIME/**\"}]}},\"scope-temp\":{\"identifier\":\"scope-temp\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$TEMP\"},{\"path\":\"$TEMP/*\"}]}},\"scope-temp-index\":{\"identifier\":\"scope-temp-index\",\"description\":\"This scope permits to list all files and folders in the `$TEMP`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$TEMP\"}]}},\"scope-temp-recursive\":{\"identifier\":\"scope-temp-recursive\",\"description\":\"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$TEMP\"},{\"path\":\"$TEMP/**\"}]}},\"scope-template\":{\"identifier\":\"scope-template\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$TEMPLATE\"},{\"path\":\"$TEMPLATE/*\"}]}},\"scope-template-index\":{\"identifier\":\"scope-template-index\",\"description\":\"This scope permits to list all files and folders in the `$TEMPLATE`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$TEMPLATE\"}]}},\"scope-template-recursive\":{\"identifier\":\"scope-template-recursive\",\"description\":\"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$TEMPLATE\"},{\"path\":\"$TEMPLATE/**\"}]}},\"scope-video\":{\"identifier\":\"scope-video\",\"description\":\"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$VIDEO\"},{\"path\":\"$VIDEO/*\"}]}},\"scope-video-index\":{\"identifier\":\"scope-video-index\",\"description\":\"This scope permits to list all files and folders in the `$VIDEO`folder.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$VIDEO\"}]}},\"scope-video-recursive\":{\"identifier\":\"scope-video-recursive\",\"description\":\"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\",\"commands\":{\"allow\":[],\"deny\":[]},\"scope\":{\"allow\":[{\"path\":\"$VIDEO\"},{\"path\":\"$VIDEO/**\"}]}},\"write-all\":{\"identifier\":\"write-all\",\"description\":\"This enables all write related commands without any pre-configured accessible paths.\",\"commands\":{\"allow\":[\"mkdir\",\"create\",\"copy_file\",\"remove\",\"rename\",\"truncate\",\"ftruncate\",\"write\",\"write_file\",\"write_text_file\"],\"deny\":[]}},\"write-files\":{\"identifier\":\"write-files\",\"description\":\"This enables all file write related commands without any pre-configured accessible paths.\",\"commands\":{\"allow\":[\"create\",\"copy_file\",\"remove\",\"rename\",\"truncate\",\"ftruncate\",\"write\",\"write_file\",\"write_text_file\"],\"deny\":[]}}},\"permission_sets\":{\"allow-app-meta\":{\"identifier\":\"allow-app-meta\",\"description\":\"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-app-index\"]},\"allow-app-meta-recursive\":{\"identifier\":\"allow-app-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-app-recursive\"]},\"allow-app-read\":{\"identifier\":\"allow-app-read\",\"description\":\"This allows non-recursive read access to the application folders.\",\"permissions\":[\"read-all\",\"scope-app\"]},\"allow-app-read-recursive\":{\"identifier\":\"allow-app-read-recursive\",\"description\":\"This allows full recursive read access to the complete application folders, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-app-recursive\"]},\"allow-app-write\":{\"identifier\":\"allow-app-write\",\"description\":\"This allows non-recursive write access to the application folders.\",\"permissions\":[\"write-all\",\"scope-app\"]},\"allow-app-write-recursive\":{\"identifier\":\"allow-app-write-recursive\",\"description\":\"This allows full recursive write access to the complete application folders, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-app-recursive\"]},\"allow-appcache-meta\":{\"identifier\":\"allow-appcache-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-appcache-index\"]},\"allow-appcache-meta-recursive\":{\"identifier\":\"allow-appcache-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-appcache-recursive\"]},\"allow-appcache-read\":{\"identifier\":\"allow-appcache-read\",\"description\":\"This allows non-recursive read access to the `$APPCACHE` folder.\",\"permissions\":[\"read-all\",\"scope-appcache\"]},\"allow-appcache-read-recursive\":{\"identifier\":\"allow-appcache-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-appcache-recursive\"]},\"allow-appcache-write\":{\"identifier\":\"allow-appcache-write\",\"description\":\"This allows non-recursive write access to the `$APPCACHE` folder.\",\"permissions\":[\"write-all\",\"scope-appcache\"]},\"allow-appcache-write-recursive\":{\"identifier\":\"allow-appcache-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-appcache-recursive\"]},\"allow-appconfig-meta\":{\"identifier\":\"allow-appconfig-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-appconfig-index\"]},\"allow-appconfig-meta-recursive\":{\"identifier\":\"allow-appconfig-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-appconfig-recursive\"]},\"allow-appconfig-read\":{\"identifier\":\"allow-appconfig-read\",\"description\":\"This allows non-recursive read access to the `$APPCONFIG` folder.\",\"permissions\":[\"read-all\",\"scope-appconfig\"]},\"allow-appconfig-read-recursive\":{\"identifier\":\"allow-appconfig-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-appconfig-recursive\"]},\"allow-appconfig-write\":{\"identifier\":\"allow-appconfig-write\",\"description\":\"This allows non-recursive write access to the `$APPCONFIG` folder.\",\"permissions\":[\"write-all\",\"scope-appconfig\"]},\"allow-appconfig-write-recursive\":{\"identifier\":\"allow-appconfig-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-appconfig-recursive\"]},\"allow-appdata-meta\":{\"identifier\":\"allow-appdata-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-appdata-index\"]},\"allow-appdata-meta-recursive\":{\"identifier\":\"allow-appdata-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-appdata-recursive\"]},\"allow-appdata-read\":{\"identifier\":\"allow-appdata-read\",\"description\":\"This allows non-recursive read access to the `$APPDATA` folder.\",\"permissions\":[\"read-all\",\"scope-appdata\"]},\"allow-appdata-read-recursive\":{\"identifier\":\"allow-appdata-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-appdata-recursive\"]},\"allow-appdata-write\":{\"identifier\":\"allow-appdata-write\",\"description\":\"This allows non-recursive write access to the `$APPDATA` folder.\",\"permissions\":[\"write-all\",\"scope-appdata\"]},\"allow-appdata-write-recursive\":{\"identifier\":\"allow-appdata-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-appdata-recursive\"]},\"allow-applocaldata-meta\":{\"identifier\":\"allow-applocaldata-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-applocaldata-index\"]},\"allow-applocaldata-meta-recursive\":{\"identifier\":\"allow-applocaldata-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-applocaldata-recursive\"]},\"allow-applocaldata-read\":{\"identifier\":\"allow-applocaldata-read\",\"description\":\"This allows non-recursive read access to the `$APPLOCALDATA` folder.\",\"permissions\":[\"read-all\",\"scope-applocaldata\"]},\"allow-applocaldata-read-recursive\":{\"identifier\":\"allow-applocaldata-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-applocaldata-recursive\"]},\"allow-applocaldata-write\":{\"identifier\":\"allow-applocaldata-write\",\"description\":\"This allows non-recursive write access to the `$APPLOCALDATA` folder.\",\"permissions\":[\"write-all\",\"scope-applocaldata\"]},\"allow-applocaldata-write-recursive\":{\"identifier\":\"allow-applocaldata-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-applocaldata-recursive\"]},\"allow-applog-meta\":{\"identifier\":\"allow-applog-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-applog-index\"]},\"allow-applog-meta-recursive\":{\"identifier\":\"allow-applog-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-applog-recursive\"]},\"allow-applog-read\":{\"identifier\":\"allow-applog-read\",\"description\":\"This allows non-recursive read access to the `$APPLOG` folder.\",\"permissions\":[\"read-all\",\"scope-applog\"]},\"allow-applog-read-recursive\":{\"identifier\":\"allow-applog-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-applog-recursive\"]},\"allow-applog-write\":{\"identifier\":\"allow-applog-write\",\"description\":\"This allows non-recursive write access to the `$APPLOG` folder.\",\"permissions\":[\"write-all\",\"scope-applog\"]},\"allow-applog-write-recursive\":{\"identifier\":\"allow-applog-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-applog-recursive\"]},\"allow-audio-meta\":{\"identifier\":\"allow-audio-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-audio-index\"]},\"allow-audio-meta-recursive\":{\"identifier\":\"allow-audio-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-audio-recursive\"]},\"allow-audio-read\":{\"identifier\":\"allow-audio-read\",\"description\":\"This allows non-recursive read access to the `$AUDIO` folder.\",\"permissions\":[\"read-all\",\"scope-audio\"]},\"allow-audio-read-recursive\":{\"identifier\":\"allow-audio-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-audio-recursive\"]},\"allow-audio-write\":{\"identifier\":\"allow-audio-write\",\"description\":\"This allows non-recursive write access to the `$AUDIO` folder.\",\"permissions\":[\"write-all\",\"scope-audio\"]},\"allow-audio-write-recursive\":{\"identifier\":\"allow-audio-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-audio-recursive\"]},\"allow-cache-meta\":{\"identifier\":\"allow-cache-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-cache-index\"]},\"allow-cache-meta-recursive\":{\"identifier\":\"allow-cache-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-cache-recursive\"]},\"allow-cache-read\":{\"identifier\":\"allow-cache-read\",\"description\":\"This allows non-recursive read access to the `$CACHE` folder.\",\"permissions\":[\"read-all\",\"scope-cache\"]},\"allow-cache-read-recursive\":{\"identifier\":\"allow-cache-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-cache-recursive\"]},\"allow-cache-write\":{\"identifier\":\"allow-cache-write\",\"description\":\"This allows non-recursive write access to the `$CACHE` folder.\",\"permissions\":[\"write-all\",\"scope-cache\"]},\"allow-cache-write-recursive\":{\"identifier\":\"allow-cache-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-cache-recursive\"]},\"allow-config-meta\":{\"identifier\":\"allow-config-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-config-index\"]},\"allow-config-meta-recursive\":{\"identifier\":\"allow-config-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-config-recursive\"]},\"allow-config-read\":{\"identifier\":\"allow-config-read\",\"description\":\"This allows non-recursive read access to the `$CONFIG` folder.\",\"permissions\":[\"read-all\",\"scope-config\"]},\"allow-config-read-recursive\":{\"identifier\":\"allow-config-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-config-recursive\"]},\"allow-config-write\":{\"identifier\":\"allow-config-write\",\"description\":\"This allows non-recursive write access to the `$CONFIG` folder.\",\"permissions\":[\"write-all\",\"scope-config\"]},\"allow-config-write-recursive\":{\"identifier\":\"allow-config-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-config-recursive\"]},\"allow-data-meta\":{\"identifier\":\"allow-data-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-data-index\"]},\"allow-data-meta-recursive\":{\"identifier\":\"allow-data-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-data-recursive\"]},\"allow-data-read\":{\"identifier\":\"allow-data-read\",\"description\":\"This allows non-recursive read access to the `$DATA` folder.\",\"permissions\":[\"read-all\",\"scope-data\"]},\"allow-data-read-recursive\":{\"identifier\":\"allow-data-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-data-recursive\"]},\"allow-data-write\":{\"identifier\":\"allow-data-write\",\"description\":\"This allows non-recursive write access to the `$DATA` folder.\",\"permissions\":[\"write-all\",\"scope-data\"]},\"allow-data-write-recursive\":{\"identifier\":\"allow-data-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-data-recursive\"]},\"allow-desktop-meta\":{\"identifier\":\"allow-desktop-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-desktop-index\"]},\"allow-desktop-meta-recursive\":{\"identifier\":\"allow-desktop-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-desktop-recursive\"]},\"allow-desktop-read\":{\"identifier\":\"allow-desktop-read\",\"description\":\"This allows non-recursive read access to the `$DESKTOP` folder.\",\"permissions\":[\"read-all\",\"scope-desktop\"]},\"allow-desktop-read-recursive\":{\"identifier\":\"allow-desktop-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-desktop-recursive\"]},\"allow-desktop-write\":{\"identifier\":\"allow-desktop-write\",\"description\":\"This allows non-recursive write access to the `$DESKTOP` folder.\",\"permissions\":[\"write-all\",\"scope-desktop\"]},\"allow-desktop-write-recursive\":{\"identifier\":\"allow-desktop-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-desktop-recursive\"]},\"allow-document-meta\":{\"identifier\":\"allow-document-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-document-index\"]},\"allow-document-meta-recursive\":{\"identifier\":\"allow-document-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-document-recursive\"]},\"allow-document-read\":{\"identifier\":\"allow-document-read\",\"description\":\"This allows non-recursive read access to the `$DOCUMENT` folder.\",\"permissions\":[\"read-all\",\"scope-document\"]},\"allow-document-read-recursive\":{\"identifier\":\"allow-document-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-document-recursive\"]},\"allow-document-write\":{\"identifier\":\"allow-document-write\",\"description\":\"This allows non-recursive write access to the `$DOCUMENT` folder.\",\"permissions\":[\"write-all\",\"scope-document\"]},\"allow-document-write-recursive\":{\"identifier\":\"allow-document-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-document-recursive\"]},\"allow-download-meta\":{\"identifier\":\"allow-download-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-download-index\"]},\"allow-download-meta-recursive\":{\"identifier\":\"allow-download-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-download-recursive\"]},\"allow-download-read\":{\"identifier\":\"allow-download-read\",\"description\":\"This allows non-recursive read access to the `$DOWNLOAD` folder.\",\"permissions\":[\"read-all\",\"scope-download\"]},\"allow-download-read-recursive\":{\"identifier\":\"allow-download-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-download-recursive\"]},\"allow-download-write\":{\"identifier\":\"allow-download-write\",\"description\":\"This allows non-recursive write access to the `$DOWNLOAD` folder.\",\"permissions\":[\"write-all\",\"scope-download\"]},\"allow-download-write-recursive\":{\"identifier\":\"allow-download-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-download-recursive\"]},\"allow-exe-meta\":{\"identifier\":\"allow-exe-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-exe-index\"]},\"allow-exe-meta-recursive\":{\"identifier\":\"allow-exe-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-exe-recursive\"]},\"allow-exe-read\":{\"identifier\":\"allow-exe-read\",\"description\":\"This allows non-recursive read access to the `$EXE` folder.\",\"permissions\":[\"read-all\",\"scope-exe\"]},\"allow-exe-read-recursive\":{\"identifier\":\"allow-exe-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-exe-recursive\"]},\"allow-exe-write\":{\"identifier\":\"allow-exe-write\",\"description\":\"This allows non-recursive write access to the `$EXE` folder.\",\"permissions\":[\"write-all\",\"scope-exe\"]},\"allow-exe-write-recursive\":{\"identifier\":\"allow-exe-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-exe-recursive\"]},\"allow-font-meta\":{\"identifier\":\"allow-font-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-font-index\"]},\"allow-font-meta-recursive\":{\"identifier\":\"allow-font-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-font-recursive\"]},\"allow-font-read\":{\"identifier\":\"allow-font-read\",\"description\":\"This allows non-recursive read access to the `$FONT` folder.\",\"permissions\":[\"read-all\",\"scope-font\"]},\"allow-font-read-recursive\":{\"identifier\":\"allow-font-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-font-recursive\"]},\"allow-font-write\":{\"identifier\":\"allow-font-write\",\"description\":\"This allows non-recursive write access to the `$FONT` folder.\",\"permissions\":[\"write-all\",\"scope-font\"]},\"allow-font-write-recursive\":{\"identifier\":\"allow-font-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-font-recursive\"]},\"allow-home-meta\":{\"identifier\":\"allow-home-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-home-index\"]},\"allow-home-meta-recursive\":{\"identifier\":\"allow-home-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-home-recursive\"]},\"allow-home-read\":{\"identifier\":\"allow-home-read\",\"description\":\"This allows non-recursive read access to the `$HOME` folder.\",\"permissions\":[\"read-all\",\"scope-home\"]},\"allow-home-read-recursive\":{\"identifier\":\"allow-home-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-home-recursive\"]},\"allow-home-write\":{\"identifier\":\"allow-home-write\",\"description\":\"This allows non-recursive write access to the `$HOME` folder.\",\"permissions\":[\"write-all\",\"scope-home\"]},\"allow-home-write-recursive\":{\"identifier\":\"allow-home-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-home-recursive\"]},\"allow-localdata-meta\":{\"identifier\":\"allow-localdata-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-localdata-index\"]},\"allow-localdata-meta-recursive\":{\"identifier\":\"allow-localdata-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-localdata-recursive\"]},\"allow-localdata-read\":{\"identifier\":\"allow-localdata-read\",\"description\":\"This allows non-recursive read access to the `$LOCALDATA` folder.\",\"permissions\":[\"read-all\",\"scope-localdata\"]},\"allow-localdata-read-recursive\":{\"identifier\":\"allow-localdata-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-localdata-recursive\"]},\"allow-localdata-write\":{\"identifier\":\"allow-localdata-write\",\"description\":\"This allows non-recursive write access to the `$LOCALDATA` folder.\",\"permissions\":[\"write-all\",\"scope-localdata\"]},\"allow-localdata-write-recursive\":{\"identifier\":\"allow-localdata-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-localdata-recursive\"]},\"allow-log-meta\":{\"identifier\":\"allow-log-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-log-index\"]},\"allow-log-meta-recursive\":{\"identifier\":\"allow-log-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-log-recursive\"]},\"allow-log-read\":{\"identifier\":\"allow-log-read\",\"description\":\"This allows non-recursive read access to the `$LOG` folder.\",\"permissions\":[\"read-all\",\"scope-log\"]},\"allow-log-read-recursive\":{\"identifier\":\"allow-log-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-log-recursive\"]},\"allow-log-write\":{\"identifier\":\"allow-log-write\",\"description\":\"This allows non-recursive write access to the `$LOG` folder.\",\"permissions\":[\"write-all\",\"scope-log\"]},\"allow-log-write-recursive\":{\"identifier\":\"allow-log-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-log-recursive\"]},\"allow-picture-meta\":{\"identifier\":\"allow-picture-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-picture-index\"]},\"allow-picture-meta-recursive\":{\"identifier\":\"allow-picture-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-picture-recursive\"]},\"allow-picture-read\":{\"identifier\":\"allow-picture-read\",\"description\":\"This allows non-recursive read access to the `$PICTURE` folder.\",\"permissions\":[\"read-all\",\"scope-picture\"]},\"allow-picture-read-recursive\":{\"identifier\":\"allow-picture-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-picture-recursive\"]},\"allow-picture-write\":{\"identifier\":\"allow-picture-write\",\"description\":\"This allows non-recursive write access to the `$PICTURE` folder.\",\"permissions\":[\"write-all\",\"scope-picture\"]},\"allow-picture-write-recursive\":{\"identifier\":\"allow-picture-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-picture-recursive\"]},\"allow-public-meta\":{\"identifier\":\"allow-public-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-public-index\"]},\"allow-public-meta-recursive\":{\"identifier\":\"allow-public-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-public-recursive\"]},\"allow-public-read\":{\"identifier\":\"allow-public-read\",\"description\":\"This allows non-recursive read access to the `$PUBLIC` folder.\",\"permissions\":[\"read-all\",\"scope-public\"]},\"allow-public-read-recursive\":{\"identifier\":\"allow-public-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-public-recursive\"]},\"allow-public-write\":{\"identifier\":\"allow-public-write\",\"description\":\"This allows non-recursive write access to the `$PUBLIC` folder.\",\"permissions\":[\"write-all\",\"scope-public\"]},\"allow-public-write-recursive\":{\"identifier\":\"allow-public-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-public-recursive\"]},\"allow-resource-meta\":{\"identifier\":\"allow-resource-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-resource-index\"]},\"allow-resource-meta-recursive\":{\"identifier\":\"allow-resource-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-resource-recursive\"]},\"allow-resource-read\":{\"identifier\":\"allow-resource-read\",\"description\":\"This allows non-recursive read access to the `$RESOURCE` folder.\",\"permissions\":[\"read-all\",\"scope-resource\"]},\"allow-resource-read-recursive\":{\"identifier\":\"allow-resource-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-resource-recursive\"]},\"allow-resource-write\":{\"identifier\":\"allow-resource-write\",\"description\":\"This allows non-recursive write access to the `$RESOURCE` folder.\",\"permissions\":[\"write-all\",\"scope-resource\"]},\"allow-resource-write-recursive\":{\"identifier\":\"allow-resource-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-resource-recursive\"]},\"allow-runtime-meta\":{\"identifier\":\"allow-runtime-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-runtime-index\"]},\"allow-runtime-meta-recursive\":{\"identifier\":\"allow-runtime-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-runtime-recursive\"]},\"allow-runtime-read\":{\"identifier\":\"allow-runtime-read\",\"description\":\"This allows non-recursive read access to the `$RUNTIME` folder.\",\"permissions\":[\"read-all\",\"scope-runtime\"]},\"allow-runtime-read-recursive\":{\"identifier\":\"allow-runtime-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-runtime-recursive\"]},\"allow-runtime-write\":{\"identifier\":\"allow-runtime-write\",\"description\":\"This allows non-recursive write access to the `$RUNTIME` folder.\",\"permissions\":[\"write-all\",\"scope-runtime\"]},\"allow-runtime-write-recursive\":{\"identifier\":\"allow-runtime-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-runtime-recursive\"]},\"allow-temp-meta\":{\"identifier\":\"allow-temp-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-temp-index\"]},\"allow-temp-meta-recursive\":{\"identifier\":\"allow-temp-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-temp-recursive\"]},\"allow-temp-read\":{\"identifier\":\"allow-temp-read\",\"description\":\"This allows non-recursive read access to the `$TEMP` folder.\",\"permissions\":[\"read-all\",\"scope-temp\"]},\"allow-temp-read-recursive\":{\"identifier\":\"allow-temp-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-temp-recursive\"]},\"allow-temp-write\":{\"identifier\":\"allow-temp-write\",\"description\":\"This allows non-recursive write access to the `$TEMP` folder.\",\"permissions\":[\"write-all\",\"scope-temp\"]},\"allow-temp-write-recursive\":{\"identifier\":\"allow-temp-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-temp-recursive\"]},\"allow-template-meta\":{\"identifier\":\"allow-template-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-template-index\"]},\"allow-template-meta-recursive\":{\"identifier\":\"allow-template-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-template-recursive\"]},\"allow-template-read\":{\"identifier\":\"allow-template-read\",\"description\":\"This allows non-recursive read access to the `$TEMPLATE` folder.\",\"permissions\":[\"read-all\",\"scope-template\"]},\"allow-template-read-recursive\":{\"identifier\":\"allow-template-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-template-recursive\"]},\"allow-template-write\":{\"identifier\":\"allow-template-write\",\"description\":\"This allows non-recursive write access to the `$TEMPLATE` folder.\",\"permissions\":[\"write-all\",\"scope-template\"]},\"allow-template-write-recursive\":{\"identifier\":\"allow-template-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-template-recursive\"]},\"allow-video-meta\":{\"identifier\":\"allow-video-meta\",\"description\":\"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-video-index\"]},\"allow-video-meta-recursive\":{\"identifier\":\"allow-video-meta-recursive\",\"description\":\"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\",\"permissions\":[\"read-meta\",\"scope-video-recursive\"]},\"allow-video-read\":{\"identifier\":\"allow-video-read\",\"description\":\"This allows non-recursive read access to the `$VIDEO` folder.\",\"permissions\":[\"read-all\",\"scope-video\"]},\"allow-video-read-recursive\":{\"identifier\":\"allow-video-read-recursive\",\"description\":\"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\",\"permissions\":[\"read-all\",\"scope-video-recursive\"]},\"allow-video-write\":{\"identifier\":\"allow-video-write\",\"description\":\"This allows non-recursive write access to the `$VIDEO` folder.\",\"permissions\":[\"write-all\",\"scope-video\"]},\"allow-video-write-recursive\":{\"identifier\":\"allow-video-write-recursive\",\"description\":\"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\",\"permissions\":[\"write-all\",\"scope-video-recursive\"]},\"deny-default\":{\"identifier\":\"deny-default\",\"description\":\"This denies access to dangerous Tauri relevant files and folders by default.\",\"permissions\":[\"deny-webview-data-linux\",\"deny-webview-data-windows\"]}},\"global_scope_schema\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"anyOf\":[{\"description\":\"A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\"type\":\"string\"},{\"properties\":{\"path\":{\"description\":\"A path that can be accessed by the webview when using the fs APIs.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}],\"description\":\"FS scope entry.\",\"title\":\"FsScopeEntry\"}},\"process\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\",\"permissions\":[\"allow-exit\",\"allow-restart\"]},\"permissions\":{\"allow-exit\":{\"identifier\":\"allow-exit\",\"description\":\"Enables the exit command without any pre-configured scope.\",\"commands\":{\"allow\":[\"exit\"],\"deny\":[]}},\"allow-restart\":{\"identifier\":\"allow-restart\",\"description\":\"Enables the restart command without any pre-configured scope.\",\"commands\":{\"allow\":[\"restart\"],\"deny\":[]}},\"deny-exit\":{\"identifier\":\"deny-exit\",\"description\":\"Denies the exit command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"exit\"]}},\"deny-restart\":{\"identifier\":\"deny-restart\",\"description\":\"Denies the restart command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"restart\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"shell\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\",\"permissions\":[\"allow-open\"]},\"permissions\":{\"allow-execute\":{\"identifier\":\"allow-execute\",\"description\":\"Enables the execute command without any pre-configured scope.\",\"commands\":{\"allow\":[\"execute\"],\"deny\":[]}},\"allow-kill\":{\"identifier\":\"allow-kill\",\"description\":\"Enables the kill command without any pre-configured scope.\",\"commands\":{\"allow\":[\"kill\"],\"deny\":[]}},\"allow-open\":{\"identifier\":\"allow-open\",\"description\":\"Enables the open command without any pre-configured scope.\",\"commands\":{\"allow\":[\"open\"],\"deny\":[]}},\"allow-spawn\":{\"identifier\":\"allow-spawn\",\"description\":\"Enables the spawn command without any pre-configured scope.\",\"commands\":{\"allow\":[\"spawn\"],\"deny\":[]}},\"allow-stdin-write\":{\"identifier\":\"allow-stdin-write\",\"description\":\"Enables the stdin_write command without any pre-configured scope.\",\"commands\":{\"allow\":[\"stdin_write\"],\"deny\":[]}},\"deny-execute\":{\"identifier\":\"deny-execute\",\"description\":\"Denies the execute command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"execute\"]}},\"deny-kill\":{\"identifier\":\"deny-kill\",\"description\":\"Denies the kill command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"kill\"]}},\"deny-open\":{\"identifier\":\"deny-open\",\"description\":\"Denies the open command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"open\"]}},\"deny-spawn\":{\"identifier\":\"deny-spawn\",\"description\":\"Denies the spawn command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"spawn\"]}},\"deny-stdin-write\":{\"identifier\":\"deny-stdin-write\",\"description\":\"Denies the stdin_write command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"stdin_write\"]}}},\"permission_sets\":{},\"global_scope_schema\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"args\":{\"allOf\":[{\"$ref\":\"#/definitions/ShellScopeEntryAllowedArgs\"}],\"description\":\"The allowed arguments for the command execution.\"},\"cmd\":{\"description\":\"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\"type\":\"string\"},\"name\":{\"description\":\"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\"type\":\"string\"}},\"required\":[\"cmd\",\"name\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"args\":{\"allOf\":[{\"$ref\":\"#/definitions/ShellScopeEntryAllowedArgs\"}],\"description\":\"The allowed arguments for the command execution.\"},\"name\":{\"description\":\"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\"type\":\"string\"},\"sidecar\":{\"description\":\"If this command is a sidecar command.\",\"type\":\"boolean\"}},\"required\":[\"name\",\"sidecar\"],\"type\":\"object\"}],\"definitions\":{\"ShellScopeEntryAllowedArg\":{\"anyOf\":[{\"description\":\"A non-configurable argument that is passed to the command in the order it was specified.\",\"type\":\"string\"},{\"additionalProperties\":false,\"description\":\"A variable that is set while calling the command from the webview API.\",\"properties\":{\"raw\":{\"default\":false,\"description\":\"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\\n\\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.\",\"type\":\"boolean\"},\"validator\":{\"description\":\"[regex] validator to require passed values to conform to an expected input.\\n\\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\\n\\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\\\w+` regex would be registered as `^https?://\\\\w+$`.\\n\\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>\",\"type\":\"string\"}},\"required\":[\"validator\"],\"type\":\"object\"}],\"description\":\"A command argument allowed to be executed by the webview API.\"},\"ShellScopeEntryAllowedArgs\":{\"anyOf\":[{\"description\":\"Use a simple boolean to allow all or disable all arguments to this command configuration.\",\"type\":\"boolean\"},{\"description\":\"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.\",\"items\":{\"$ref\":\"#/definitions/ShellScopeEntryAllowedArg\"},\"type\":\"array\"}],\"description\":\"A set of command arguments allowed to be executed by the webview API.\\n\\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.\"}},\"description\":\"Shell scope entry.\",\"title\":\"ShellScopeEntry\"}},\"updater\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\",\"permissions\":[\"allow-check\",\"allow-download\",\"allow-install\",\"allow-download-and-install\"]},\"permissions\":{\"allow-check\":{\"identifier\":\"allow-check\",\"description\":\"Enables the check command without any pre-configured scope.\",\"commands\":{\"allow\":[\"check\"],\"deny\":[]}},\"allow-download\":{\"identifier\":\"allow-download\",\"description\":\"Enables the download command without any pre-configured scope.\",\"commands\":{\"allow\":[\"download\"],\"deny\":[]}},\"allow-download-and-install\":{\"identifier\":\"allow-download-and-install\",\"description\":\"Enables the download_and_install command without any pre-configured scope.\",\"commands\":{\"allow\":[\"download_and_install\"],\"deny\":[]}},\"allow-install\":{\"identifier\":\"allow-install\",\"description\":\"Enables the install command without any pre-configured scope.\",\"commands\":{\"allow\":[\"install\"],\"deny\":[]}},\"deny-check\":{\"identifier\":\"deny-check\",\"description\":\"Denies the check command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"check\"]}},\"deny-download\":{\"identifier\":\"deny-download\",\"description\":\"Denies the download command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"download\"]}},\"deny-download-and-install\":{\"identifier\":\"deny-download-and-install\",\"description\":\"Denies the download_and_install command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"download_and_install\"]}},\"deny-install\":{\"identifier\":\"deny-install\",\"description\":\"Denies the install command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"install\"]}}},\"permission_sets\":{},\"global_scope_schema\":null}}"
  },
  {
    "path": "tauri/src-tauri/gen/schemas/capabilities.json",
    "content": "{\"default\":{\"identifier\":\"default\",\"description\":\"Default permissions for voicebox\",\"remote\":{\"urls\":[\"http://localhost:*\"]},\"local\":true,\"windows\":[\"main\"],\"permissions\":[\"core:default\",\"core:window:default\",\"core:window:allow-start-dragging\",\"core:webview:default\",\"core:webview:allow-internal-toggle-devtools\",\"shell:allow-open\",\"shell:allow-execute\",\"shell:allow-spawn\",\"updater:default\",\"process:default\",\"dialog:default\",\"dialog:allow-save\",\"dialog:allow-open\",\"fs:default\",\"fs:read-all\",\"fs:write-all\"],\"platforms\":[\"linux\",\"macOS\",\"windows\"]}}"
  },
  {
    "path": "tauri/src-tauri/gen/schemas/desktop-schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"CapabilityFile\",\n  \"description\": \"Capability formats accepted in a capability file.\",\n  \"anyOf\": [\n    {\n      \"description\": \"A single capability.\",\n      \"allOf\": [\n        {\n          \"$ref\": \"#/definitions/Capability\"\n        }\n      ]\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/Capability\"\n      }\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"capabilities\"\n      ],\n      \"properties\": {\n        \"capabilities\": {\n          \"description\": \"The list of capabilities.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Capability\"\n          }\n        }\n      }\n    }\n  ],\n  \"definitions\": {\n    \"Capability\": {\n      \"description\": \"A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\\n\\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\\n\\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\\n\\n## Example\\n\\n```json { \\\"identifier\\\": \\\"main-user-files-write\\\", \\\"description\\\": \\\"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\\\", \\\"windows\\\": [ \\\"main\\\" ], \\\"permissions\\\": [ \\\"core:default\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] }, ], \\\"platforms\\\": [\\\"macOS\\\",\\\"windows\\\"] } ```\",\n      \"type\": \"object\",\n      \"required\": [\n        \"identifier\",\n        \"permissions\"\n      ],\n      \"properties\": {\n        \"identifier\": {\n          \"description\": \"Identifier of the capability.\\n\\n## Example\\n\\n`main-user-files-write`\",\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"description\": \"Description of what the capability is intended to allow on associated windows.\\n\\nIt should contain a description of what the grouped permissions should allow.\\n\\n## Example\\n\\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\",\n          \"default\": \"\",\n          \"type\": \"string\"\n        },\n        \"remote\": {\n          \"description\": \"Configure remote URLs that can use the capability permissions.\\n\\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\\n\\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\\n\\n## Example\\n\\n```json { \\\"urls\\\": [\\\"https://*.mydomain.dev\\\"] } ```\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/CapabilityRemote\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"local\": {\n          \"description\": \"Whether this capability is enabled for local app URLs or not. Defaults to `true`.\",\n          \"default\": true,\n          \"type\": \"boolean\"\n        },\n        \"windows\": {\n          \"description\": \"List of windows that are affected by this capability. Can be a glob pattern.\\n\\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\\n\\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\\n\\n## Example\\n\\n`[\\\"main\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"webviews\": {\n          \"description\": \"List of webviews that are affected by this capability. Can be a glob pattern.\\n\\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\\n\\n## Example\\n\\n`[\\\"sub-webview-one\\\", \\\"sub-webview-two\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"permissions\": {\n          \"description\": \"List of permissions attached to this capability.\\n\\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\\n\\n## Example\\n\\n```json [ \\\"core:default\\\", \\\"shell:allow-open\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] } ] ```\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/PermissionEntry\"\n          },\n          \"uniqueItems\": true\n        },\n        \"platforms\": {\n          \"description\": \"Limit which target platforms this capability applies to.\\n\\nBy default all platforms are targeted.\\n\\n## Example\\n\\n`[\\\"macOS\\\",\\\"windows\\\"]`\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/definitions/Target\"\n          }\n        }\n      }\n    },\n    \"CapabilityRemote\": {\n      \"description\": \"Configuration for remote URLs that are associated with the capability.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"urls\"\n      ],\n      \"properties\": {\n        \"urls\": {\n          \"description\": \"Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\\n\\n## Examples\\n\\n- \\\"https://*.mydomain.dev\\\": allows subdomains of mydomain.dev - \\\"https://mydomain.dev/api/*\\\": allows any subpath of mydomain.dev/api\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"PermissionEntry\": {\n      \"description\": \"An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Reference a permission or permission set by identifier.\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Identifier\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Reference a permission or permission set by identifier and extends its scope.\",\n          \"type\": \"object\",\n          \"allOf\": [\n            {\n              \"if\": {\n                \"properties\": {\n                  \"identifier\": {\n                    \"anyOf\": [\n                      {\n                        \"description\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:default\",\n                        \"markdownDescription\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-default\",\n                        \"markdownDescription\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\"\n                      },\n                      {\n                        \"description\": \"Enables the copy_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-copy-file\",\n                        \"markdownDescription\": \"Enables the copy_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the create command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-create\",\n                        \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the exists command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exists\",\n                        \"markdownDescription\": \"Enables the exists command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the fstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-fstat\",\n                        \"markdownDescription\": \"Enables the fstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the ftruncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-ftruncate\",\n                        \"markdownDescription\": \"Enables the ftruncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the lstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-lstat\",\n                        \"markdownDescription\": \"Enables the lstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the mkdir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-mkdir\",\n                        \"markdownDescription\": \"Enables the mkdir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-open\",\n                        \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read\",\n                        \"markdownDescription\": \"Enables the read command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_dir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-dir\",\n                        \"markdownDescription\": \"Enables the read_dir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-file\",\n                        \"markdownDescription\": \"Enables the read_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file\",\n                        \"markdownDescription\": \"Enables the read_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file_lines command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file-lines\",\n                        \"markdownDescription\": \"Enables the read_text_file_lines command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file-lines-next\",\n                        \"markdownDescription\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the remove command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-remove\",\n                        \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the rename command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-rename\",\n                        \"markdownDescription\": \"Enables the rename command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the seek command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-seek\",\n                        \"markdownDescription\": \"Enables the seek command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the size command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-size\",\n                        \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the stat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-stat\",\n                        \"markdownDescription\": \"Enables the stat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the truncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-truncate\",\n                        \"markdownDescription\": \"Enables the truncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the unwatch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-unwatch\",\n                        \"markdownDescription\": \"Enables the unwatch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the watch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-watch\",\n                        \"markdownDescription\": \"Enables the watch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write\",\n                        \"markdownDescription\": \"Enables the write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write-file\",\n                        \"markdownDescription\": \"Enables the write_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write-text-file\",\n                        \"markdownDescription\": \"Enables the write_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This permissions allows to create the application specific directories.\\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:create-app-specific-dirs\",\n                        \"markdownDescription\": \"This permissions allows to create the application specific directories.\\n\"\n                      },\n                      {\n                        \"description\": \"Denies the copy_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-copy-file\",\n                        \"markdownDescription\": \"Denies the copy_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the create command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-create\",\n                        \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the exists command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-exists\",\n                        \"markdownDescription\": \"Denies the exists command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the fstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-fstat\",\n                        \"markdownDescription\": \"Denies the fstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the ftruncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-ftruncate\",\n                        \"markdownDescription\": \"Denies the ftruncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the lstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-lstat\",\n                        \"markdownDescription\": \"Denies the lstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the mkdir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-mkdir\",\n                        \"markdownDescription\": \"Denies the mkdir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-open\",\n                        \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read\",\n                        \"markdownDescription\": \"Denies the read command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_dir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-dir\",\n                        \"markdownDescription\": \"Denies the read_dir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-file\",\n                        \"markdownDescription\": \"Denies the read_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file\",\n                        \"markdownDescription\": \"Denies the read_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file_lines command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file-lines\",\n                        \"markdownDescription\": \"Denies the read_text_file_lines command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file-lines-next\",\n                        \"markdownDescription\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the remove command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-remove\",\n                        \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the rename command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-rename\",\n                        \"markdownDescription\": \"Denies the rename command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the seek command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-seek\",\n                        \"markdownDescription\": \"Denies the seek command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the size command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-size\",\n                        \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the stat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-stat\",\n                        \"markdownDescription\": \"Denies the stat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the truncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-truncate\",\n                        \"markdownDescription\": \"Denies the truncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the unwatch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-unwatch\",\n                        \"markdownDescription\": \"Denies the unwatch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the watch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-watch\",\n                        \"markdownDescription\": \"Denies the watch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-webview-data-linux\",\n                        \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n                      },\n                      {\n                        \"description\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-webview-data-windows\",\n                        \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n                      },\n                      {\n                        \"description\": \"Denies the write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write\",\n                        \"markdownDescription\": \"Denies the write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the write_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write-file\",\n                        \"markdownDescription\": \"Denies the write_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the write_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write-text-file\",\n                        \"markdownDescription\": \"Denies the write_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This enables all read related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-all\",\n                        \"markdownDescription\": \"This enables all read related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-app-specific-dirs-recursive\",\n                        \"markdownDescription\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\"\n                      },\n                      {\n                        \"description\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-dirs\",\n                        \"markdownDescription\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables file read related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-files\",\n                        \"markdownDescription\": \"This enables file read related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-meta\",\n                        \"markdownDescription\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope\",\n                        \"markdownDescription\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the application folders.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the application folders.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the application directories.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the application directories.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$CACHE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$CACHE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$EXE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$EXE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$FONT`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$FONT`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$HOME`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$HOME`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$LOG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$TEMP`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMP`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This enables all write related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:write-all\",\n                        \"markdownDescription\": \"This enables all write related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables all file write related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:write-files\",\n                        \"markdownDescription\": \"This enables all file write related commands without any pre-configured accessible paths.\"\n                      }\n                    ]\n                  }\n                }\n              },\n              \"then\": {\n                \"properties\": {\n                  \"allow\": {\n                    \"items\": {\n                      \"title\": \"FsScopeEntry\",\n                      \"description\": \"FS scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"description\": \"A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                          \"type\": \"string\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"path\"\n                          ],\n                          \"properties\": {\n                            \"path\": {\n                              \"description\": \"A path that can be accessed by the webview when using the fs APIs.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      ]\n                    }\n                  },\n                  \"deny\": {\n                    \"items\": {\n                      \"title\": \"FsScopeEntry\",\n                      \"description\": \"FS scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"description\": \"A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                          \"type\": \"string\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"path\"\n                          ],\n                          \"properties\": {\n                            \"path\": {\n                              \"description\": \"A path that can be accessed by the webview when using the fs APIs.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      ]\n                    }\n                  }\n                }\n              },\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                }\n              }\n            },\n            {\n              \"if\": {\n                \"properties\": {\n                  \"identifier\": {\n                    \"anyOf\": [\n                      {\n                        \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:default\",\n                        \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n                      },\n                      {\n                        \"description\": \"Enables the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-execute\",\n                        \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-kill\",\n                        \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-open\",\n                        \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-spawn\",\n                        \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-stdin-write\",\n                        \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-execute\",\n                        \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-kill\",\n                        \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-open\",\n                        \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-spawn\",\n                        \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-stdin-write\",\n                        \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n                      }\n                    ]\n                  }\n                }\n              },\n              \"then\": {\n                \"properties\": {\n                  \"allow\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  },\n                  \"deny\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  }\n                }\n              },\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                }\n              }\n            },\n            {\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                },\n                \"allow\": {\n                  \"description\": \"Data that defines what is allowed by the scope.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                },\n                \"deny\": {\n                  \"description\": \"Data that defines what is denied by the scope. This should be prioritized by validation logic.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                }\n              }\n            }\n          ],\n          \"required\": [\n            \"identifier\"\n          ]\n        }\n      ]\n    },\n    \"Identifier\": {\n      \"description\": \"Permission identifier\",\n      \"oneOf\": [\n        {\n          \"description\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\",\n          \"type\": \"string\",\n          \"const\": \"core:default\",\n          \"markdownDescription\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\",\n          \"type\": \"string\",\n          \"const\": \"core:app:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\"\n        },\n        {\n          \"description\": \"Enables the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-hide\",\n          \"markdownDescription\": \"Enables the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-show\",\n          \"markdownDescription\": \"Enables the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-bundle-type\",\n          \"markdownDescription\": \"Enables the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-default-window-icon\",\n          \"markdownDescription\": \"Enables the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-identifier\",\n          \"markdownDescription\": \"Enables the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-name\",\n          \"markdownDescription\": \"Enables the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-register-listener\",\n          \"markdownDescription\": \"Enables the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-data-store\",\n          \"markdownDescription\": \"Enables the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-listener\",\n          \"markdownDescription\": \"Enables the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-app-theme\",\n          \"markdownDescription\": \"Enables the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-dock-visibility\",\n          \"markdownDescription\": \"Enables the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-tauri-version\",\n          \"markdownDescription\": \"Enables the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-version\",\n          \"markdownDescription\": \"Enables the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-hide\",\n          \"markdownDescription\": \"Denies the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-show\",\n          \"markdownDescription\": \"Denies the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-bundle-type\",\n          \"markdownDescription\": \"Denies the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-default-window-icon\",\n          \"markdownDescription\": \"Denies the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-identifier\",\n          \"markdownDescription\": \"Denies the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-name\",\n          \"markdownDescription\": \"Denies the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-register-listener\",\n          \"markdownDescription\": \"Denies the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-data-store\",\n          \"markdownDescription\": \"Denies the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-listener\",\n          \"markdownDescription\": \"Denies the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-app-theme\",\n          \"markdownDescription\": \"Denies the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-dock-visibility\",\n          \"markdownDescription\": \"Denies the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-tauri-version\",\n          \"markdownDescription\": \"Denies the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-version\",\n          \"markdownDescription\": \"Denies the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\",\n          \"type\": \"string\",\n          \"const\": \"core:event:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\"\n        },\n        {\n          \"description\": \"Enables the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit\",\n          \"markdownDescription\": \"Enables the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit-to\",\n          \"markdownDescription\": \"Enables the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-listen\",\n          \"markdownDescription\": \"Enables the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-unlisten\",\n          \"markdownDescription\": \"Enables the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit\",\n          \"markdownDescription\": \"Denies the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit-to\",\n          \"markdownDescription\": \"Denies the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-listen\",\n          \"markdownDescription\": \"Denies the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-unlisten\",\n          \"markdownDescription\": \"Denies the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\",\n          \"type\": \"string\",\n          \"const\": \"core:image:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\"\n        },\n        {\n          \"description\": \"Enables the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-bytes\",\n          \"markdownDescription\": \"Enables the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-path\",\n          \"markdownDescription\": \"Enables the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-rgba\",\n          \"markdownDescription\": \"Enables the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-size\",\n          \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-bytes\",\n          \"markdownDescription\": \"Denies the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-path\",\n          \"markdownDescription\": \"Denies the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-rgba\",\n          \"markdownDescription\": \"Denies the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-size\",\n          \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\"\n        },\n        {\n          \"description\": \"Enables the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-append\",\n          \"markdownDescription\": \"Enables the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-create-default\",\n          \"markdownDescription\": \"Enables the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-get\",\n          \"markdownDescription\": \"Enables the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-insert\",\n          \"markdownDescription\": \"Enables the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-checked\",\n          \"markdownDescription\": \"Enables the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-items\",\n          \"markdownDescription\": \"Enables the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-popup\",\n          \"markdownDescription\": \"Enables the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-prepend\",\n          \"markdownDescription\": \"Enables the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove\",\n          \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove-at\",\n          \"markdownDescription\": \"Enables the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-accelerator\",\n          \"markdownDescription\": \"Enables the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-app-menu\",\n          \"markdownDescription\": \"Enables the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-window-menu\",\n          \"markdownDescription\": \"Enables the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-checked\",\n          \"markdownDescription\": \"Enables the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-text\",\n          \"markdownDescription\": \"Enables the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-text\",\n          \"markdownDescription\": \"Enables the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-append\",\n          \"markdownDescription\": \"Denies the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-create-default\",\n          \"markdownDescription\": \"Denies the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-get\",\n          \"markdownDescription\": \"Denies the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-insert\",\n          \"markdownDescription\": \"Denies the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-checked\",\n          \"markdownDescription\": \"Denies the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-items\",\n          \"markdownDescription\": \"Denies the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-popup\",\n          \"markdownDescription\": \"Denies the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-prepend\",\n          \"markdownDescription\": \"Denies the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove\",\n          \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove-at\",\n          \"markdownDescription\": \"Denies the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-accelerator\",\n          \"markdownDescription\": \"Denies the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-app-menu\",\n          \"markdownDescription\": \"Denies the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-window-menu\",\n          \"markdownDescription\": \"Denies the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-checked\",\n          \"markdownDescription\": \"Denies the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-text\",\n          \"markdownDescription\": \"Denies the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-text\",\n          \"markdownDescription\": \"Denies the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\",\n          \"type\": \"string\",\n          \"const\": \"core:path:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\"\n        },\n        {\n          \"description\": \"Enables the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-basename\",\n          \"markdownDescription\": \"Enables the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-dirname\",\n          \"markdownDescription\": \"Enables the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-extname\",\n          \"markdownDescription\": \"Enables the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-is-absolute\",\n          \"markdownDescription\": \"Enables the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-join\",\n          \"markdownDescription\": \"Enables the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-normalize\",\n          \"markdownDescription\": \"Enables the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve\",\n          \"markdownDescription\": \"Enables the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve-directory\",\n          \"markdownDescription\": \"Enables the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-basename\",\n          \"markdownDescription\": \"Denies the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-dirname\",\n          \"markdownDescription\": \"Denies the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-extname\",\n          \"markdownDescription\": \"Denies the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-is-absolute\",\n          \"markdownDescription\": \"Denies the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-join\",\n          \"markdownDescription\": \"Denies the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-normalize\",\n          \"markdownDescription\": \"Denies the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve\",\n          \"markdownDescription\": \"Denies the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve-directory\",\n          \"markdownDescription\": \"Denies the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\"\n        },\n        {\n          \"description\": \"Enables the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-get-by-id\",\n          \"markdownDescription\": \"Enables the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-remove-by-id\",\n          \"markdownDescription\": \"Enables the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon-as-template\",\n          \"markdownDescription\": \"Enables the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-menu\",\n          \"markdownDescription\": \"Enables the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-temp-dir-path\",\n          \"markdownDescription\": \"Enables the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-tooltip\",\n          \"markdownDescription\": \"Enables the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-visible\",\n          \"markdownDescription\": \"Enables the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-get-by-id\",\n          \"markdownDescription\": \"Denies the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-remove-by-id\",\n          \"markdownDescription\": \"Denies the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon-as-template\",\n          \"markdownDescription\": \"Denies the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-menu\",\n          \"markdownDescription\": \"Denies the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-temp-dir-path\",\n          \"markdownDescription\": \"Denies the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-tooltip\",\n          \"markdownDescription\": \"Denies the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-visible\",\n          \"markdownDescription\": \"Denies the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\"\n        },\n        {\n          \"description\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-clear-all-browsing-data\",\n          \"markdownDescription\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview\",\n          \"markdownDescription\": \"Enables the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview-window\",\n          \"markdownDescription\": \"Enables the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-get-all-webviews\",\n          \"markdownDescription\": \"Enables the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-internal-toggle-devtools\",\n          \"markdownDescription\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-print\",\n          \"markdownDescription\": \"Enables the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-reparent\",\n          \"markdownDescription\": \"Enables the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-auto-resize\",\n          \"markdownDescription\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-background-color\",\n          \"markdownDescription\": \"Enables the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-focus\",\n          \"markdownDescription\": \"Enables the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-position\",\n          \"markdownDescription\": \"Enables the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-size\",\n          \"markdownDescription\": \"Enables the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-zoom\",\n          \"markdownDescription\": \"Enables the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-close\",\n          \"markdownDescription\": \"Enables the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-hide\",\n          \"markdownDescription\": \"Enables the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-position\",\n          \"markdownDescription\": \"Enables the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-show\",\n          \"markdownDescription\": \"Enables the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-size\",\n          \"markdownDescription\": \"Enables the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-clear-all-browsing-data\",\n          \"markdownDescription\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview\",\n          \"markdownDescription\": \"Denies the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview-window\",\n          \"markdownDescription\": \"Denies the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-get-all-webviews\",\n          \"markdownDescription\": \"Denies the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-internal-toggle-devtools\",\n          \"markdownDescription\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-print\",\n          \"markdownDescription\": \"Denies the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-reparent\",\n          \"markdownDescription\": \"Denies the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-auto-resize\",\n          \"markdownDescription\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-background-color\",\n          \"markdownDescription\": \"Denies the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-focus\",\n          \"markdownDescription\": \"Denies the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-position\",\n          \"markdownDescription\": \"Denies the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-size\",\n          \"markdownDescription\": \"Denies the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-zoom\",\n          \"markdownDescription\": \"Denies the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-close\",\n          \"markdownDescription\": \"Denies the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-hide\",\n          \"markdownDescription\": \"Denies the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-position\",\n          \"markdownDescription\": \"Denies the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-show\",\n          \"markdownDescription\": \"Denies the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-size\",\n          \"markdownDescription\": \"Denies the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\",\n          \"type\": \"string\",\n          \"const\": \"core:window:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\"\n        },\n        {\n          \"description\": \"Enables the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-available-monitors\",\n          \"markdownDescription\": \"Enables the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-center\",\n          \"markdownDescription\": \"Enables the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-create\",\n          \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-current-monitor\",\n          \"markdownDescription\": \"Enables the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-cursor-position\",\n          \"markdownDescription\": \"Enables the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-destroy\",\n          \"markdownDescription\": \"Enables the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-get-all-windows\",\n          \"markdownDescription\": \"Enables the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-hide\",\n          \"markdownDescription\": \"Enables the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-position\",\n          \"markdownDescription\": \"Enables the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-size\",\n          \"markdownDescription\": \"Enables the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-internal-toggle-maximize\",\n          \"markdownDescription\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-always-on-top\",\n          \"markdownDescription\": \"Enables the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-closable\",\n          \"markdownDescription\": \"Enables the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-decorated\",\n          \"markdownDescription\": \"Enables the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-focused\",\n          \"markdownDescription\": \"Enables the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-fullscreen\",\n          \"markdownDescription\": \"Enables the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximizable\",\n          \"markdownDescription\": \"Enables the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximized\",\n          \"markdownDescription\": \"Enables the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimizable\",\n          \"markdownDescription\": \"Enables the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimized\",\n          \"markdownDescription\": \"Enables the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-resizable\",\n          \"markdownDescription\": \"Enables the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-visible\",\n          \"markdownDescription\": \"Enables the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-maximize\",\n          \"markdownDescription\": \"Enables the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-minimize\",\n          \"markdownDescription\": \"Enables the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-monitor-from-point\",\n          \"markdownDescription\": \"Enables the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-position\",\n          \"markdownDescription\": \"Enables the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-size\",\n          \"markdownDescription\": \"Enables the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-primary-monitor\",\n          \"markdownDescription\": \"Enables the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-request-user-attention\",\n          \"markdownDescription\": \"Enables the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-scale-factor\",\n          \"markdownDescription\": \"Enables the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-bottom\",\n          \"markdownDescription\": \"Enables the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-top\",\n          \"markdownDescription\": \"Enables the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-background-color\",\n          \"markdownDescription\": \"Enables the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-count\",\n          \"markdownDescription\": \"Enables the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-label\",\n          \"markdownDescription\": \"Enables the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-closable\",\n          \"markdownDescription\": \"Enables the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-content-protected\",\n          \"markdownDescription\": \"Enables the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-grab\",\n          \"markdownDescription\": \"Enables the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-icon\",\n          \"markdownDescription\": \"Enables the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-position\",\n          \"markdownDescription\": \"Enables the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-visible\",\n          \"markdownDescription\": \"Enables the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-decorations\",\n          \"markdownDescription\": \"Enables the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-effects\",\n          \"markdownDescription\": \"Enables the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focus\",\n          \"markdownDescription\": \"Enables the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focusable\",\n          \"markdownDescription\": \"Enables the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-fullscreen\",\n          \"markdownDescription\": \"Enables the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-max-size\",\n          \"markdownDescription\": \"Enables the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-maximizable\",\n          \"markdownDescription\": \"Enables the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-min-size\",\n          \"markdownDescription\": \"Enables the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-minimizable\",\n          \"markdownDescription\": \"Enables the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-overlay-icon\",\n          \"markdownDescription\": \"Enables the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-position\",\n          \"markdownDescription\": \"Enables the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-progress-bar\",\n          \"markdownDescription\": \"Enables the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-resizable\",\n          \"markdownDescription\": \"Enables the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-shadow\",\n          \"markdownDescription\": \"Enables the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-simple-fullscreen\",\n          \"markdownDescription\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size\",\n          \"markdownDescription\": \"Enables the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size-constraints\",\n          \"markdownDescription\": \"Enables the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-skip-taskbar\",\n          \"markdownDescription\": \"Enables the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-theme\",\n          \"markdownDescription\": \"Enables the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title-bar-style\",\n          \"markdownDescription\": \"Enables the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-show\",\n          \"markdownDescription\": \"Enables the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-dragging\",\n          \"markdownDescription\": \"Enables the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-resize-dragging\",\n          \"markdownDescription\": \"Enables the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-theme\",\n          \"markdownDescription\": \"Enables the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-title\",\n          \"markdownDescription\": \"Enables the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-toggle-maximize\",\n          \"markdownDescription\": \"Enables the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unmaximize\",\n          \"markdownDescription\": \"Enables the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unminimize\",\n          \"markdownDescription\": \"Enables the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-available-monitors\",\n          \"markdownDescription\": \"Denies the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-center\",\n          \"markdownDescription\": \"Denies the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-create\",\n          \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-current-monitor\",\n          \"markdownDescription\": \"Denies the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-cursor-position\",\n          \"markdownDescription\": \"Denies the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-destroy\",\n          \"markdownDescription\": \"Denies the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-get-all-windows\",\n          \"markdownDescription\": \"Denies the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-hide\",\n          \"markdownDescription\": \"Denies the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-position\",\n          \"markdownDescription\": \"Denies the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-size\",\n          \"markdownDescription\": \"Denies the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-internal-toggle-maximize\",\n          \"markdownDescription\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-always-on-top\",\n          \"markdownDescription\": \"Denies the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-closable\",\n          \"markdownDescription\": \"Denies the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-decorated\",\n          \"markdownDescription\": \"Denies the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-focused\",\n          \"markdownDescription\": \"Denies the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-fullscreen\",\n          \"markdownDescription\": \"Denies the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximizable\",\n          \"markdownDescription\": \"Denies the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximized\",\n          \"markdownDescription\": \"Denies the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimizable\",\n          \"markdownDescription\": \"Denies the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimized\",\n          \"markdownDescription\": \"Denies the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-resizable\",\n          \"markdownDescription\": \"Denies the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-visible\",\n          \"markdownDescription\": \"Denies the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-maximize\",\n          \"markdownDescription\": \"Denies the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-minimize\",\n          \"markdownDescription\": \"Denies the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-monitor-from-point\",\n          \"markdownDescription\": \"Denies the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-position\",\n          \"markdownDescription\": \"Denies the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-size\",\n          \"markdownDescription\": \"Denies the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-primary-monitor\",\n          \"markdownDescription\": \"Denies the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-request-user-attention\",\n          \"markdownDescription\": \"Denies the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-scale-factor\",\n          \"markdownDescription\": \"Denies the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-bottom\",\n          \"markdownDescription\": \"Denies the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-top\",\n          \"markdownDescription\": \"Denies the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-background-color\",\n          \"markdownDescription\": \"Denies the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-count\",\n          \"markdownDescription\": \"Denies the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-label\",\n          \"markdownDescription\": \"Denies the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-closable\",\n          \"markdownDescription\": \"Denies the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-content-protected\",\n          \"markdownDescription\": \"Denies the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-grab\",\n          \"markdownDescription\": \"Denies the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-icon\",\n          \"markdownDescription\": \"Denies the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-position\",\n          \"markdownDescription\": \"Denies the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-visible\",\n          \"markdownDescription\": \"Denies the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-decorations\",\n          \"markdownDescription\": \"Denies the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-effects\",\n          \"markdownDescription\": \"Denies the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focus\",\n          \"markdownDescription\": \"Denies the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focusable\",\n          \"markdownDescription\": \"Denies the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-fullscreen\",\n          \"markdownDescription\": \"Denies the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-max-size\",\n          \"markdownDescription\": \"Denies the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-maximizable\",\n          \"markdownDescription\": \"Denies the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-min-size\",\n          \"markdownDescription\": \"Denies the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-minimizable\",\n          \"markdownDescription\": \"Denies the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-overlay-icon\",\n          \"markdownDescription\": \"Denies the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-position\",\n          \"markdownDescription\": \"Denies the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-progress-bar\",\n          \"markdownDescription\": \"Denies the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-resizable\",\n          \"markdownDescription\": \"Denies the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-shadow\",\n          \"markdownDescription\": \"Denies the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-simple-fullscreen\",\n          \"markdownDescription\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size\",\n          \"markdownDescription\": \"Denies the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size-constraints\",\n          \"markdownDescription\": \"Denies the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-skip-taskbar\",\n          \"markdownDescription\": \"Denies the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-theme\",\n          \"markdownDescription\": \"Denies the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title-bar-style\",\n          \"markdownDescription\": \"Denies the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-show\",\n          \"markdownDescription\": \"Denies the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-dragging\",\n          \"markdownDescription\": \"Denies the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-resize-dragging\",\n          \"markdownDescription\": \"Denies the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-theme\",\n          \"markdownDescription\": \"Denies the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-title\",\n          \"markdownDescription\": \"Denies the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-toggle-maximize\",\n          \"markdownDescription\": \"Denies the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unmaximize\",\n          \"markdownDescription\": \"Denies the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unminimize\",\n          \"markdownDescription\": \"Denies the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"dialog:default\",\n          \"markdownDescription\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-ask\",\n          \"markdownDescription\": \"Enables the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-confirm\",\n          \"markdownDescription\": \"Enables the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-message\",\n          \"markdownDescription\": \"Enables the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-save\",\n          \"markdownDescription\": \"Enables the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-ask\",\n          \"markdownDescription\": \"Denies the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-confirm\",\n          \"markdownDescription\": \"Denies the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-message\",\n          \"markdownDescription\": \"Denies the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-save\",\n          \"markdownDescription\": \"Denies the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\",\n          \"type\": \"string\",\n          \"const\": \"fs:default\",\n          \"markdownDescription\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-default\",\n          \"markdownDescription\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\"\n        },\n        {\n          \"description\": \"Enables the copy_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-copy-file\",\n          \"markdownDescription\": \"Enables the copy_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-create\",\n          \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the exists command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exists\",\n          \"markdownDescription\": \"Enables the exists command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the fstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-fstat\",\n          \"markdownDescription\": \"Enables the fstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the ftruncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-ftruncate\",\n          \"markdownDescription\": \"Enables the ftruncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the lstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-lstat\",\n          \"markdownDescription\": \"Enables the lstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the mkdir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-mkdir\",\n          \"markdownDescription\": \"Enables the mkdir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read\",\n          \"markdownDescription\": \"Enables the read command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_dir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-dir\",\n          \"markdownDescription\": \"Enables the read_dir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-file\",\n          \"markdownDescription\": \"Enables the read_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file\",\n          \"markdownDescription\": \"Enables the read_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file_lines command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file-lines\",\n          \"markdownDescription\": \"Enables the read_text_file_lines command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file-lines-next\",\n          \"markdownDescription\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-remove\",\n          \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the rename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-rename\",\n          \"markdownDescription\": \"Enables the rename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the seek command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-seek\",\n          \"markdownDescription\": \"Enables the seek command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-size\",\n          \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the stat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-stat\",\n          \"markdownDescription\": \"Enables the stat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the truncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-truncate\",\n          \"markdownDescription\": \"Enables the truncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unwatch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-unwatch\",\n          \"markdownDescription\": \"Enables the unwatch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the watch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-watch\",\n          \"markdownDescription\": \"Enables the watch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write\",\n          \"markdownDescription\": \"Enables the write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write-file\",\n          \"markdownDescription\": \"Enables the write_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write-text-file\",\n          \"markdownDescription\": \"Enables the write_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permissions allows to create the application specific directories.\\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:create-app-specific-dirs\",\n          \"markdownDescription\": \"This permissions allows to create the application specific directories.\\n\"\n        },\n        {\n          \"description\": \"Denies the copy_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-copy-file\",\n          \"markdownDescription\": \"Denies the copy_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-create\",\n          \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the exists command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-exists\",\n          \"markdownDescription\": \"Denies the exists command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the fstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-fstat\",\n          \"markdownDescription\": \"Denies the fstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the ftruncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-ftruncate\",\n          \"markdownDescription\": \"Denies the ftruncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the lstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-lstat\",\n          \"markdownDescription\": \"Denies the lstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the mkdir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-mkdir\",\n          \"markdownDescription\": \"Denies the mkdir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read\",\n          \"markdownDescription\": \"Denies the read command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_dir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-dir\",\n          \"markdownDescription\": \"Denies the read_dir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-file\",\n          \"markdownDescription\": \"Denies the read_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file\",\n          \"markdownDescription\": \"Denies the read_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file_lines command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file-lines\",\n          \"markdownDescription\": \"Denies the read_text_file_lines command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file-lines-next\",\n          \"markdownDescription\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-remove\",\n          \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the rename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-rename\",\n          \"markdownDescription\": \"Denies the rename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the seek command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-seek\",\n          \"markdownDescription\": \"Denies the seek command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-size\",\n          \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the stat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-stat\",\n          \"markdownDescription\": \"Denies the stat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the truncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-truncate\",\n          \"markdownDescription\": \"Denies the truncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unwatch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-unwatch\",\n          \"markdownDescription\": \"Denies the unwatch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the watch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-watch\",\n          \"markdownDescription\": \"Denies the watch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-webview-data-linux\",\n          \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n        },\n        {\n          \"description\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-webview-data-windows\",\n          \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n        },\n        {\n          \"description\": \"Denies the write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write\",\n          \"markdownDescription\": \"Denies the write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write-file\",\n          \"markdownDescription\": \"Denies the write_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write-text-file\",\n          \"markdownDescription\": \"Denies the write_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This enables all read related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-all\",\n          \"markdownDescription\": \"This enables all read related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-app-specific-dirs-recursive\",\n          \"markdownDescription\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\"\n        },\n        {\n          \"description\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-dirs\",\n          \"markdownDescription\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables file read related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-files\",\n          \"markdownDescription\": \"This enables file read related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-meta\",\n          \"markdownDescription\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope\",\n          \"markdownDescription\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the application folders.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the application folders.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the application directories.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the application directories.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$CACHE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$CACHE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$EXE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$EXE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$FONT`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$FONT`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$HOME`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$HOME`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$LOG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$TEMP`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMP`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This enables all write related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:write-all\",\n          \"markdownDescription\": \"This enables all write related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables all file write related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:write-files\",\n          \"markdownDescription\": \"This enables all file write related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\",\n          \"type\": \"string\",\n          \"const\": \"process:default\",\n          \"markdownDescription\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\"\n        },\n        {\n          \"description\": \"Enables the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-exit\",\n          \"markdownDescription\": \"Enables the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-restart\",\n          \"markdownDescription\": \"Enables the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-exit\",\n          \"markdownDescription\": \"Denies the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-restart\",\n          \"markdownDescription\": \"Denies the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"shell:default\",\n          \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-execute\",\n          \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-kill\",\n          \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-spawn\",\n          \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-stdin-write\",\n          \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-execute\",\n          \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-kill\",\n          \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-spawn\",\n          \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-stdin-write\",\n          \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\",\n          \"type\": \"string\",\n          \"const\": \"updater:default\",\n          \"markdownDescription\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\"\n        },\n        {\n          \"description\": \"Enables the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-check\",\n          \"markdownDescription\": \"Enables the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download\",\n          \"markdownDescription\": \"Enables the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download-and-install\",\n          \"markdownDescription\": \"Enables the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-install\",\n          \"markdownDescription\": \"Enables the install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-check\",\n          \"markdownDescription\": \"Denies the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download\",\n          \"markdownDescription\": \"Denies the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download-and-install\",\n          \"markdownDescription\": \"Denies the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-install\",\n          \"markdownDescription\": \"Denies the install command without any pre-configured scope.\"\n        }\n      ]\n    },\n    \"Value\": {\n      \"description\": \"All supported ACL values.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents a null JSON value.\",\n          \"type\": \"null\"\n        },\n        {\n          \"description\": \"Represents a [`bool`].\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"Represents a valid ACL [`Number`].\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Number\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Represents a [`String`].\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"Represents a list of other [`Value`]s.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        },\n        {\n          \"description\": \"Represents a map of [`String`] keys to [`Value`]s.\",\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        }\n      ]\n    },\n    \"Number\": {\n      \"description\": \"A valid ACL number.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents an [`i64`].\",\n          \"type\": \"integer\",\n          \"format\": \"int64\"\n        },\n        {\n          \"description\": \"Represents a [`f64`].\",\n          \"type\": \"number\",\n          \"format\": \"double\"\n        }\n      ]\n    },\n    \"Target\": {\n      \"description\": \"Platform target.\",\n      \"oneOf\": [\n        {\n          \"description\": \"MacOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"macOS\"\n          ]\n        },\n        {\n          \"description\": \"Windows.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"windows\"\n          ]\n        },\n        {\n          \"description\": \"Linux.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"linux\"\n          ]\n        },\n        {\n          \"description\": \"Android.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"android\"\n          ]\n        },\n        {\n          \"description\": \"iOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"iOS\"\n          ]\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArg\": {\n      \"description\": \"A command argument allowed to be executed by the webview API.\",\n      \"anyOf\": [\n        {\n          \"description\": \"A non-configurable argument that is passed to the command in the order it was specified.\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"A variable that is set while calling the command from the webview API.\",\n          \"type\": \"object\",\n          \"required\": [\n            \"validator\"\n          ],\n          \"properties\": {\n            \"raw\": {\n              \"description\": \"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\\n\\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.\",\n              \"default\": false,\n              \"type\": \"boolean\"\n            },\n            \"validator\": {\n              \"description\": \"[regex] validator to require passed values to conform to an expected input.\\n\\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\\n\\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\\\w+` regex would be registered as `^https?://\\\\w+$`.\\n\\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>\",\n              \"type\": \"string\"\n            }\n          },\n          \"additionalProperties\": false\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArgs\": {\n      \"description\": \"A set of command arguments allowed to be executed by the webview API.\\n\\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Use a simple boolean to allow all or disable all arguments to this command configuration.\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/ShellScopeEntryAllowedArg\"\n          }\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tauri/src-tauri/gen/schemas/macOS-schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"CapabilityFile\",\n  \"description\": \"Capability formats accepted in a capability file.\",\n  \"anyOf\": [\n    {\n      \"description\": \"A single capability.\",\n      \"allOf\": [\n        {\n          \"$ref\": \"#/definitions/Capability\"\n        }\n      ]\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/Capability\"\n      }\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"capabilities\"\n      ],\n      \"properties\": {\n        \"capabilities\": {\n          \"description\": \"The list of capabilities.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Capability\"\n          }\n        }\n      }\n    }\n  ],\n  \"definitions\": {\n    \"Capability\": {\n      \"description\": \"A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\\n\\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\\n\\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\\n\\n## Example\\n\\n```json { \\\"identifier\\\": \\\"main-user-files-write\\\", \\\"description\\\": \\\"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\\\", \\\"windows\\\": [ \\\"main\\\" ], \\\"permissions\\\": [ \\\"core:default\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] }, ], \\\"platforms\\\": [\\\"macOS\\\",\\\"windows\\\"] } ```\",\n      \"type\": \"object\",\n      \"required\": [\n        \"identifier\",\n        \"permissions\"\n      ],\n      \"properties\": {\n        \"identifier\": {\n          \"description\": \"Identifier of the capability.\\n\\n## Example\\n\\n`main-user-files-write`\",\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"description\": \"Description of what the capability is intended to allow on associated windows.\\n\\nIt should contain a description of what the grouped permissions should allow.\\n\\n## Example\\n\\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\",\n          \"default\": \"\",\n          \"type\": \"string\"\n        },\n        \"remote\": {\n          \"description\": \"Configure remote URLs that can use the capability permissions.\\n\\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\\n\\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\\n\\n## Example\\n\\n```json { \\\"urls\\\": [\\\"https://*.mydomain.dev\\\"] } ```\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/CapabilityRemote\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"local\": {\n          \"description\": \"Whether this capability is enabled for local app URLs or not. Defaults to `true`.\",\n          \"default\": true,\n          \"type\": \"boolean\"\n        },\n        \"windows\": {\n          \"description\": \"List of windows that are affected by this capability. Can be a glob pattern.\\n\\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\\n\\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\\n\\n## Example\\n\\n`[\\\"main\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"webviews\": {\n          \"description\": \"List of webviews that are affected by this capability. Can be a glob pattern.\\n\\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\\n\\n## Example\\n\\n`[\\\"sub-webview-one\\\", \\\"sub-webview-two\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"permissions\": {\n          \"description\": \"List of permissions attached to this capability.\\n\\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\\n\\n## Example\\n\\n```json [ \\\"core:default\\\", \\\"shell:allow-open\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] } ] ```\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/PermissionEntry\"\n          },\n          \"uniqueItems\": true\n        },\n        \"platforms\": {\n          \"description\": \"Limit which target platforms this capability applies to.\\n\\nBy default all platforms are targeted.\\n\\n## Example\\n\\n`[\\\"macOS\\\",\\\"windows\\\"]`\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/definitions/Target\"\n          }\n        }\n      }\n    },\n    \"CapabilityRemote\": {\n      \"description\": \"Configuration for remote URLs that are associated with the capability.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"urls\"\n      ],\n      \"properties\": {\n        \"urls\": {\n          \"description\": \"Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\\n\\n## Examples\\n\\n- \\\"https://*.mydomain.dev\\\": allows subdomains of mydomain.dev - \\\"https://mydomain.dev/api/*\\\": allows any subpath of mydomain.dev/api\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"PermissionEntry\": {\n      \"description\": \"An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Reference a permission or permission set by identifier.\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Identifier\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Reference a permission or permission set by identifier and extends its scope.\",\n          \"type\": \"object\",\n          \"allOf\": [\n            {\n              \"if\": {\n                \"properties\": {\n                  \"identifier\": {\n                    \"anyOf\": [\n                      {\n                        \"description\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:default\",\n                        \"markdownDescription\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-default\",\n                        \"markdownDescription\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\"\n                      },\n                      {\n                        \"description\": \"Enables the copy_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-copy-file\",\n                        \"markdownDescription\": \"Enables the copy_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the create command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-create\",\n                        \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the exists command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exists\",\n                        \"markdownDescription\": \"Enables the exists command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the fstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-fstat\",\n                        \"markdownDescription\": \"Enables the fstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the ftruncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-ftruncate\",\n                        \"markdownDescription\": \"Enables the ftruncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the lstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-lstat\",\n                        \"markdownDescription\": \"Enables the lstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the mkdir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-mkdir\",\n                        \"markdownDescription\": \"Enables the mkdir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-open\",\n                        \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read\",\n                        \"markdownDescription\": \"Enables the read command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_dir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-dir\",\n                        \"markdownDescription\": \"Enables the read_dir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-file\",\n                        \"markdownDescription\": \"Enables the read_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file\",\n                        \"markdownDescription\": \"Enables the read_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file_lines command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file-lines\",\n                        \"markdownDescription\": \"Enables the read_text_file_lines command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file-lines-next\",\n                        \"markdownDescription\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the remove command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-remove\",\n                        \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the rename command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-rename\",\n                        \"markdownDescription\": \"Enables the rename command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the seek command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-seek\",\n                        \"markdownDescription\": \"Enables the seek command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the size command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-size\",\n                        \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the stat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-stat\",\n                        \"markdownDescription\": \"Enables the stat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the truncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-truncate\",\n                        \"markdownDescription\": \"Enables the truncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the unwatch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-unwatch\",\n                        \"markdownDescription\": \"Enables the unwatch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the watch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-watch\",\n                        \"markdownDescription\": \"Enables the watch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write\",\n                        \"markdownDescription\": \"Enables the write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write-file\",\n                        \"markdownDescription\": \"Enables the write_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write-text-file\",\n                        \"markdownDescription\": \"Enables the write_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This permissions allows to create the application specific directories.\\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:create-app-specific-dirs\",\n                        \"markdownDescription\": \"This permissions allows to create the application specific directories.\\n\"\n                      },\n                      {\n                        \"description\": \"Denies the copy_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-copy-file\",\n                        \"markdownDescription\": \"Denies the copy_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the create command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-create\",\n                        \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the exists command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-exists\",\n                        \"markdownDescription\": \"Denies the exists command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the fstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-fstat\",\n                        \"markdownDescription\": \"Denies the fstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the ftruncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-ftruncate\",\n                        \"markdownDescription\": \"Denies the ftruncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the lstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-lstat\",\n                        \"markdownDescription\": \"Denies the lstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the mkdir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-mkdir\",\n                        \"markdownDescription\": \"Denies the mkdir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-open\",\n                        \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read\",\n                        \"markdownDescription\": \"Denies the read command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_dir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-dir\",\n                        \"markdownDescription\": \"Denies the read_dir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-file\",\n                        \"markdownDescription\": \"Denies the read_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file\",\n                        \"markdownDescription\": \"Denies the read_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file_lines command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file-lines\",\n                        \"markdownDescription\": \"Denies the read_text_file_lines command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file-lines-next\",\n                        \"markdownDescription\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the remove command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-remove\",\n                        \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the rename command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-rename\",\n                        \"markdownDescription\": \"Denies the rename command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the seek command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-seek\",\n                        \"markdownDescription\": \"Denies the seek command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the size command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-size\",\n                        \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the stat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-stat\",\n                        \"markdownDescription\": \"Denies the stat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the truncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-truncate\",\n                        \"markdownDescription\": \"Denies the truncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the unwatch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-unwatch\",\n                        \"markdownDescription\": \"Denies the unwatch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the watch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-watch\",\n                        \"markdownDescription\": \"Denies the watch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-webview-data-linux\",\n                        \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n                      },\n                      {\n                        \"description\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-webview-data-windows\",\n                        \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n                      },\n                      {\n                        \"description\": \"Denies the write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write\",\n                        \"markdownDescription\": \"Denies the write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the write_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write-file\",\n                        \"markdownDescription\": \"Denies the write_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the write_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write-text-file\",\n                        \"markdownDescription\": \"Denies the write_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This enables all read related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-all\",\n                        \"markdownDescription\": \"This enables all read related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-app-specific-dirs-recursive\",\n                        \"markdownDescription\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\"\n                      },\n                      {\n                        \"description\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-dirs\",\n                        \"markdownDescription\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables file read related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-files\",\n                        \"markdownDescription\": \"This enables file read related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-meta\",\n                        \"markdownDescription\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope\",\n                        \"markdownDescription\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the application folders.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the application folders.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the application directories.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the application directories.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$CACHE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$CACHE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$EXE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$EXE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$FONT`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$FONT`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$HOME`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$HOME`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$LOG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$TEMP`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMP`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This enables all write related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:write-all\",\n                        \"markdownDescription\": \"This enables all write related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables all file write related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:write-files\",\n                        \"markdownDescription\": \"This enables all file write related commands without any pre-configured accessible paths.\"\n                      }\n                    ]\n                  }\n                }\n              },\n              \"then\": {\n                \"properties\": {\n                  \"allow\": {\n                    \"items\": {\n                      \"title\": \"FsScopeEntry\",\n                      \"description\": \"FS scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"description\": \"A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                          \"type\": \"string\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"path\"\n                          ],\n                          \"properties\": {\n                            \"path\": {\n                              \"description\": \"A path that can be accessed by the webview when using the fs APIs.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      ]\n                    }\n                  },\n                  \"deny\": {\n                    \"items\": {\n                      \"title\": \"FsScopeEntry\",\n                      \"description\": \"FS scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"description\": \"A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                          \"type\": \"string\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"path\"\n                          ],\n                          \"properties\": {\n                            \"path\": {\n                              \"description\": \"A path that can be accessed by the webview when using the fs APIs.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      ]\n                    }\n                  }\n                }\n              },\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                }\n              }\n            },\n            {\n              \"if\": {\n                \"properties\": {\n                  \"identifier\": {\n                    \"anyOf\": [\n                      {\n                        \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:default\",\n                        \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n                      },\n                      {\n                        \"description\": \"Enables the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-execute\",\n                        \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-kill\",\n                        \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-open\",\n                        \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-spawn\",\n                        \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-stdin-write\",\n                        \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-execute\",\n                        \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-kill\",\n                        \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-open\",\n                        \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-spawn\",\n                        \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-stdin-write\",\n                        \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n                      }\n                    ]\n                  }\n                }\n              },\n              \"then\": {\n                \"properties\": {\n                  \"allow\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  },\n                  \"deny\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  }\n                }\n              },\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                }\n              }\n            },\n            {\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                },\n                \"allow\": {\n                  \"description\": \"Data that defines what is allowed by the scope.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                },\n                \"deny\": {\n                  \"description\": \"Data that defines what is denied by the scope. This should be prioritized by validation logic.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                }\n              }\n            }\n          ],\n          \"required\": [\n            \"identifier\"\n          ]\n        }\n      ]\n    },\n    \"Identifier\": {\n      \"description\": \"Permission identifier\",\n      \"oneOf\": [\n        {\n          \"description\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\",\n          \"type\": \"string\",\n          \"const\": \"core:default\",\n          \"markdownDescription\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\",\n          \"type\": \"string\",\n          \"const\": \"core:app:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\"\n        },\n        {\n          \"description\": \"Enables the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-hide\",\n          \"markdownDescription\": \"Enables the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-show\",\n          \"markdownDescription\": \"Enables the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-bundle-type\",\n          \"markdownDescription\": \"Enables the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-default-window-icon\",\n          \"markdownDescription\": \"Enables the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-identifier\",\n          \"markdownDescription\": \"Enables the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-name\",\n          \"markdownDescription\": \"Enables the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-register-listener\",\n          \"markdownDescription\": \"Enables the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-data-store\",\n          \"markdownDescription\": \"Enables the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-listener\",\n          \"markdownDescription\": \"Enables the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-app-theme\",\n          \"markdownDescription\": \"Enables the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-dock-visibility\",\n          \"markdownDescription\": \"Enables the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-tauri-version\",\n          \"markdownDescription\": \"Enables the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-version\",\n          \"markdownDescription\": \"Enables the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-hide\",\n          \"markdownDescription\": \"Denies the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-show\",\n          \"markdownDescription\": \"Denies the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-bundle-type\",\n          \"markdownDescription\": \"Denies the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-default-window-icon\",\n          \"markdownDescription\": \"Denies the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-identifier\",\n          \"markdownDescription\": \"Denies the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-name\",\n          \"markdownDescription\": \"Denies the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-register-listener\",\n          \"markdownDescription\": \"Denies the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-data-store\",\n          \"markdownDescription\": \"Denies the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-listener\",\n          \"markdownDescription\": \"Denies the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-app-theme\",\n          \"markdownDescription\": \"Denies the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-dock-visibility\",\n          \"markdownDescription\": \"Denies the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-tauri-version\",\n          \"markdownDescription\": \"Denies the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-version\",\n          \"markdownDescription\": \"Denies the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\",\n          \"type\": \"string\",\n          \"const\": \"core:event:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\"\n        },\n        {\n          \"description\": \"Enables the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit\",\n          \"markdownDescription\": \"Enables the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit-to\",\n          \"markdownDescription\": \"Enables the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-listen\",\n          \"markdownDescription\": \"Enables the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-unlisten\",\n          \"markdownDescription\": \"Enables the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit\",\n          \"markdownDescription\": \"Denies the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit-to\",\n          \"markdownDescription\": \"Denies the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-listen\",\n          \"markdownDescription\": \"Denies the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-unlisten\",\n          \"markdownDescription\": \"Denies the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\",\n          \"type\": \"string\",\n          \"const\": \"core:image:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\"\n        },\n        {\n          \"description\": \"Enables the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-bytes\",\n          \"markdownDescription\": \"Enables the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-path\",\n          \"markdownDescription\": \"Enables the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-rgba\",\n          \"markdownDescription\": \"Enables the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-size\",\n          \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-bytes\",\n          \"markdownDescription\": \"Denies the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-path\",\n          \"markdownDescription\": \"Denies the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-rgba\",\n          \"markdownDescription\": \"Denies the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-size\",\n          \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\"\n        },\n        {\n          \"description\": \"Enables the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-append\",\n          \"markdownDescription\": \"Enables the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-create-default\",\n          \"markdownDescription\": \"Enables the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-get\",\n          \"markdownDescription\": \"Enables the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-insert\",\n          \"markdownDescription\": \"Enables the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-checked\",\n          \"markdownDescription\": \"Enables the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-items\",\n          \"markdownDescription\": \"Enables the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-popup\",\n          \"markdownDescription\": \"Enables the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-prepend\",\n          \"markdownDescription\": \"Enables the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove\",\n          \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove-at\",\n          \"markdownDescription\": \"Enables the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-accelerator\",\n          \"markdownDescription\": \"Enables the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-app-menu\",\n          \"markdownDescription\": \"Enables the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-window-menu\",\n          \"markdownDescription\": \"Enables the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-checked\",\n          \"markdownDescription\": \"Enables the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-text\",\n          \"markdownDescription\": \"Enables the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-text\",\n          \"markdownDescription\": \"Enables the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-append\",\n          \"markdownDescription\": \"Denies the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-create-default\",\n          \"markdownDescription\": \"Denies the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-get\",\n          \"markdownDescription\": \"Denies the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-insert\",\n          \"markdownDescription\": \"Denies the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-checked\",\n          \"markdownDescription\": \"Denies the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-items\",\n          \"markdownDescription\": \"Denies the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-popup\",\n          \"markdownDescription\": \"Denies the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-prepend\",\n          \"markdownDescription\": \"Denies the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove\",\n          \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove-at\",\n          \"markdownDescription\": \"Denies the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-accelerator\",\n          \"markdownDescription\": \"Denies the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-app-menu\",\n          \"markdownDescription\": \"Denies the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-window-menu\",\n          \"markdownDescription\": \"Denies the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-checked\",\n          \"markdownDescription\": \"Denies the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-text\",\n          \"markdownDescription\": \"Denies the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-text\",\n          \"markdownDescription\": \"Denies the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\",\n          \"type\": \"string\",\n          \"const\": \"core:path:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\"\n        },\n        {\n          \"description\": \"Enables the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-basename\",\n          \"markdownDescription\": \"Enables the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-dirname\",\n          \"markdownDescription\": \"Enables the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-extname\",\n          \"markdownDescription\": \"Enables the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-is-absolute\",\n          \"markdownDescription\": \"Enables the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-join\",\n          \"markdownDescription\": \"Enables the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-normalize\",\n          \"markdownDescription\": \"Enables the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve\",\n          \"markdownDescription\": \"Enables the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve-directory\",\n          \"markdownDescription\": \"Enables the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-basename\",\n          \"markdownDescription\": \"Denies the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-dirname\",\n          \"markdownDescription\": \"Denies the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-extname\",\n          \"markdownDescription\": \"Denies the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-is-absolute\",\n          \"markdownDescription\": \"Denies the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-join\",\n          \"markdownDescription\": \"Denies the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-normalize\",\n          \"markdownDescription\": \"Denies the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve\",\n          \"markdownDescription\": \"Denies the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve-directory\",\n          \"markdownDescription\": \"Denies the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\"\n        },\n        {\n          \"description\": \"Enables the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-get-by-id\",\n          \"markdownDescription\": \"Enables the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-remove-by-id\",\n          \"markdownDescription\": \"Enables the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon-as-template\",\n          \"markdownDescription\": \"Enables the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-menu\",\n          \"markdownDescription\": \"Enables the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-temp-dir-path\",\n          \"markdownDescription\": \"Enables the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-tooltip\",\n          \"markdownDescription\": \"Enables the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-visible\",\n          \"markdownDescription\": \"Enables the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-get-by-id\",\n          \"markdownDescription\": \"Denies the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-remove-by-id\",\n          \"markdownDescription\": \"Denies the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon-as-template\",\n          \"markdownDescription\": \"Denies the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-menu\",\n          \"markdownDescription\": \"Denies the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-temp-dir-path\",\n          \"markdownDescription\": \"Denies the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-tooltip\",\n          \"markdownDescription\": \"Denies the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-visible\",\n          \"markdownDescription\": \"Denies the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\"\n        },\n        {\n          \"description\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-clear-all-browsing-data\",\n          \"markdownDescription\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview\",\n          \"markdownDescription\": \"Enables the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview-window\",\n          \"markdownDescription\": \"Enables the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-get-all-webviews\",\n          \"markdownDescription\": \"Enables the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-internal-toggle-devtools\",\n          \"markdownDescription\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-print\",\n          \"markdownDescription\": \"Enables the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-reparent\",\n          \"markdownDescription\": \"Enables the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-auto-resize\",\n          \"markdownDescription\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-background-color\",\n          \"markdownDescription\": \"Enables the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-focus\",\n          \"markdownDescription\": \"Enables the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-position\",\n          \"markdownDescription\": \"Enables the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-size\",\n          \"markdownDescription\": \"Enables the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-zoom\",\n          \"markdownDescription\": \"Enables the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-close\",\n          \"markdownDescription\": \"Enables the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-hide\",\n          \"markdownDescription\": \"Enables the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-position\",\n          \"markdownDescription\": \"Enables the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-show\",\n          \"markdownDescription\": \"Enables the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-size\",\n          \"markdownDescription\": \"Enables the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-clear-all-browsing-data\",\n          \"markdownDescription\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview\",\n          \"markdownDescription\": \"Denies the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview-window\",\n          \"markdownDescription\": \"Denies the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-get-all-webviews\",\n          \"markdownDescription\": \"Denies the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-internal-toggle-devtools\",\n          \"markdownDescription\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-print\",\n          \"markdownDescription\": \"Denies the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-reparent\",\n          \"markdownDescription\": \"Denies the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-auto-resize\",\n          \"markdownDescription\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-background-color\",\n          \"markdownDescription\": \"Denies the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-focus\",\n          \"markdownDescription\": \"Denies the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-position\",\n          \"markdownDescription\": \"Denies the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-size\",\n          \"markdownDescription\": \"Denies the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-zoom\",\n          \"markdownDescription\": \"Denies the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-close\",\n          \"markdownDescription\": \"Denies the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-hide\",\n          \"markdownDescription\": \"Denies the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-position\",\n          \"markdownDescription\": \"Denies the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-show\",\n          \"markdownDescription\": \"Denies the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-size\",\n          \"markdownDescription\": \"Denies the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\",\n          \"type\": \"string\",\n          \"const\": \"core:window:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\"\n        },\n        {\n          \"description\": \"Enables the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-available-monitors\",\n          \"markdownDescription\": \"Enables the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-center\",\n          \"markdownDescription\": \"Enables the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-create\",\n          \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-current-monitor\",\n          \"markdownDescription\": \"Enables the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-cursor-position\",\n          \"markdownDescription\": \"Enables the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-destroy\",\n          \"markdownDescription\": \"Enables the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-get-all-windows\",\n          \"markdownDescription\": \"Enables the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-hide\",\n          \"markdownDescription\": \"Enables the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-position\",\n          \"markdownDescription\": \"Enables the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-size\",\n          \"markdownDescription\": \"Enables the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-internal-toggle-maximize\",\n          \"markdownDescription\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-always-on-top\",\n          \"markdownDescription\": \"Enables the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-closable\",\n          \"markdownDescription\": \"Enables the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-decorated\",\n          \"markdownDescription\": \"Enables the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-focused\",\n          \"markdownDescription\": \"Enables the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-fullscreen\",\n          \"markdownDescription\": \"Enables the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximizable\",\n          \"markdownDescription\": \"Enables the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximized\",\n          \"markdownDescription\": \"Enables the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimizable\",\n          \"markdownDescription\": \"Enables the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimized\",\n          \"markdownDescription\": \"Enables the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-resizable\",\n          \"markdownDescription\": \"Enables the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-visible\",\n          \"markdownDescription\": \"Enables the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-maximize\",\n          \"markdownDescription\": \"Enables the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-minimize\",\n          \"markdownDescription\": \"Enables the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-monitor-from-point\",\n          \"markdownDescription\": \"Enables the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-position\",\n          \"markdownDescription\": \"Enables the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-size\",\n          \"markdownDescription\": \"Enables the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-primary-monitor\",\n          \"markdownDescription\": \"Enables the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-request-user-attention\",\n          \"markdownDescription\": \"Enables the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-scale-factor\",\n          \"markdownDescription\": \"Enables the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-bottom\",\n          \"markdownDescription\": \"Enables the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-top\",\n          \"markdownDescription\": \"Enables the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-background-color\",\n          \"markdownDescription\": \"Enables the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-count\",\n          \"markdownDescription\": \"Enables the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-label\",\n          \"markdownDescription\": \"Enables the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-closable\",\n          \"markdownDescription\": \"Enables the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-content-protected\",\n          \"markdownDescription\": \"Enables the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-grab\",\n          \"markdownDescription\": \"Enables the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-icon\",\n          \"markdownDescription\": \"Enables the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-position\",\n          \"markdownDescription\": \"Enables the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-visible\",\n          \"markdownDescription\": \"Enables the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-decorations\",\n          \"markdownDescription\": \"Enables the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-effects\",\n          \"markdownDescription\": \"Enables the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focus\",\n          \"markdownDescription\": \"Enables the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focusable\",\n          \"markdownDescription\": \"Enables the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-fullscreen\",\n          \"markdownDescription\": \"Enables the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-max-size\",\n          \"markdownDescription\": \"Enables the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-maximizable\",\n          \"markdownDescription\": \"Enables the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-min-size\",\n          \"markdownDescription\": \"Enables the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-minimizable\",\n          \"markdownDescription\": \"Enables the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-overlay-icon\",\n          \"markdownDescription\": \"Enables the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-position\",\n          \"markdownDescription\": \"Enables the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-progress-bar\",\n          \"markdownDescription\": \"Enables the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-resizable\",\n          \"markdownDescription\": \"Enables the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-shadow\",\n          \"markdownDescription\": \"Enables the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-simple-fullscreen\",\n          \"markdownDescription\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size\",\n          \"markdownDescription\": \"Enables the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size-constraints\",\n          \"markdownDescription\": \"Enables the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-skip-taskbar\",\n          \"markdownDescription\": \"Enables the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-theme\",\n          \"markdownDescription\": \"Enables the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title-bar-style\",\n          \"markdownDescription\": \"Enables the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-show\",\n          \"markdownDescription\": \"Enables the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-dragging\",\n          \"markdownDescription\": \"Enables the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-resize-dragging\",\n          \"markdownDescription\": \"Enables the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-theme\",\n          \"markdownDescription\": \"Enables the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-title\",\n          \"markdownDescription\": \"Enables the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-toggle-maximize\",\n          \"markdownDescription\": \"Enables the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unmaximize\",\n          \"markdownDescription\": \"Enables the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unminimize\",\n          \"markdownDescription\": \"Enables the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-available-monitors\",\n          \"markdownDescription\": \"Denies the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-center\",\n          \"markdownDescription\": \"Denies the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-create\",\n          \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-current-monitor\",\n          \"markdownDescription\": \"Denies the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-cursor-position\",\n          \"markdownDescription\": \"Denies the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-destroy\",\n          \"markdownDescription\": \"Denies the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-get-all-windows\",\n          \"markdownDescription\": \"Denies the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-hide\",\n          \"markdownDescription\": \"Denies the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-position\",\n          \"markdownDescription\": \"Denies the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-size\",\n          \"markdownDescription\": \"Denies the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-internal-toggle-maximize\",\n          \"markdownDescription\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-always-on-top\",\n          \"markdownDescription\": \"Denies the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-closable\",\n          \"markdownDescription\": \"Denies the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-decorated\",\n          \"markdownDescription\": \"Denies the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-focused\",\n          \"markdownDescription\": \"Denies the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-fullscreen\",\n          \"markdownDescription\": \"Denies the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximizable\",\n          \"markdownDescription\": \"Denies the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximized\",\n          \"markdownDescription\": \"Denies the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimizable\",\n          \"markdownDescription\": \"Denies the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimized\",\n          \"markdownDescription\": \"Denies the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-resizable\",\n          \"markdownDescription\": \"Denies the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-visible\",\n          \"markdownDescription\": \"Denies the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-maximize\",\n          \"markdownDescription\": \"Denies the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-minimize\",\n          \"markdownDescription\": \"Denies the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-monitor-from-point\",\n          \"markdownDescription\": \"Denies the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-position\",\n          \"markdownDescription\": \"Denies the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-size\",\n          \"markdownDescription\": \"Denies the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-primary-monitor\",\n          \"markdownDescription\": \"Denies the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-request-user-attention\",\n          \"markdownDescription\": \"Denies the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-scale-factor\",\n          \"markdownDescription\": \"Denies the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-bottom\",\n          \"markdownDescription\": \"Denies the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-top\",\n          \"markdownDescription\": \"Denies the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-background-color\",\n          \"markdownDescription\": \"Denies the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-count\",\n          \"markdownDescription\": \"Denies the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-label\",\n          \"markdownDescription\": \"Denies the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-closable\",\n          \"markdownDescription\": \"Denies the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-content-protected\",\n          \"markdownDescription\": \"Denies the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-grab\",\n          \"markdownDescription\": \"Denies the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-icon\",\n          \"markdownDescription\": \"Denies the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-position\",\n          \"markdownDescription\": \"Denies the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-visible\",\n          \"markdownDescription\": \"Denies the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-decorations\",\n          \"markdownDescription\": \"Denies the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-effects\",\n          \"markdownDescription\": \"Denies the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focus\",\n          \"markdownDescription\": \"Denies the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focusable\",\n          \"markdownDescription\": \"Denies the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-fullscreen\",\n          \"markdownDescription\": \"Denies the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-max-size\",\n          \"markdownDescription\": \"Denies the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-maximizable\",\n          \"markdownDescription\": \"Denies the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-min-size\",\n          \"markdownDescription\": \"Denies the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-minimizable\",\n          \"markdownDescription\": \"Denies the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-overlay-icon\",\n          \"markdownDescription\": \"Denies the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-position\",\n          \"markdownDescription\": \"Denies the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-progress-bar\",\n          \"markdownDescription\": \"Denies the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-resizable\",\n          \"markdownDescription\": \"Denies the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-shadow\",\n          \"markdownDescription\": \"Denies the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-simple-fullscreen\",\n          \"markdownDescription\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size\",\n          \"markdownDescription\": \"Denies the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size-constraints\",\n          \"markdownDescription\": \"Denies the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-skip-taskbar\",\n          \"markdownDescription\": \"Denies the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-theme\",\n          \"markdownDescription\": \"Denies the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title-bar-style\",\n          \"markdownDescription\": \"Denies the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-show\",\n          \"markdownDescription\": \"Denies the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-dragging\",\n          \"markdownDescription\": \"Denies the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-resize-dragging\",\n          \"markdownDescription\": \"Denies the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-theme\",\n          \"markdownDescription\": \"Denies the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-title\",\n          \"markdownDescription\": \"Denies the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-toggle-maximize\",\n          \"markdownDescription\": \"Denies the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unmaximize\",\n          \"markdownDescription\": \"Denies the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unminimize\",\n          \"markdownDescription\": \"Denies the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"dialog:default\",\n          \"markdownDescription\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-ask\",\n          \"markdownDescription\": \"Enables the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-confirm\",\n          \"markdownDescription\": \"Enables the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-message\",\n          \"markdownDescription\": \"Enables the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-save\",\n          \"markdownDescription\": \"Enables the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-ask\",\n          \"markdownDescription\": \"Denies the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-confirm\",\n          \"markdownDescription\": \"Denies the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-message\",\n          \"markdownDescription\": \"Denies the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-save\",\n          \"markdownDescription\": \"Denies the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\",\n          \"type\": \"string\",\n          \"const\": \"fs:default\",\n          \"markdownDescription\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-default\",\n          \"markdownDescription\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\"\n        },\n        {\n          \"description\": \"Enables the copy_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-copy-file\",\n          \"markdownDescription\": \"Enables the copy_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-create\",\n          \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the exists command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exists\",\n          \"markdownDescription\": \"Enables the exists command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the fstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-fstat\",\n          \"markdownDescription\": \"Enables the fstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the ftruncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-ftruncate\",\n          \"markdownDescription\": \"Enables the ftruncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the lstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-lstat\",\n          \"markdownDescription\": \"Enables the lstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the mkdir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-mkdir\",\n          \"markdownDescription\": \"Enables the mkdir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read\",\n          \"markdownDescription\": \"Enables the read command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_dir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-dir\",\n          \"markdownDescription\": \"Enables the read_dir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-file\",\n          \"markdownDescription\": \"Enables the read_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file\",\n          \"markdownDescription\": \"Enables the read_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file_lines command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file-lines\",\n          \"markdownDescription\": \"Enables the read_text_file_lines command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file-lines-next\",\n          \"markdownDescription\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-remove\",\n          \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the rename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-rename\",\n          \"markdownDescription\": \"Enables the rename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the seek command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-seek\",\n          \"markdownDescription\": \"Enables the seek command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-size\",\n          \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the stat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-stat\",\n          \"markdownDescription\": \"Enables the stat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the truncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-truncate\",\n          \"markdownDescription\": \"Enables the truncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unwatch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-unwatch\",\n          \"markdownDescription\": \"Enables the unwatch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the watch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-watch\",\n          \"markdownDescription\": \"Enables the watch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write\",\n          \"markdownDescription\": \"Enables the write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write-file\",\n          \"markdownDescription\": \"Enables the write_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write-text-file\",\n          \"markdownDescription\": \"Enables the write_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permissions allows to create the application specific directories.\\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:create-app-specific-dirs\",\n          \"markdownDescription\": \"This permissions allows to create the application specific directories.\\n\"\n        },\n        {\n          \"description\": \"Denies the copy_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-copy-file\",\n          \"markdownDescription\": \"Denies the copy_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-create\",\n          \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the exists command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-exists\",\n          \"markdownDescription\": \"Denies the exists command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the fstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-fstat\",\n          \"markdownDescription\": \"Denies the fstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the ftruncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-ftruncate\",\n          \"markdownDescription\": \"Denies the ftruncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the lstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-lstat\",\n          \"markdownDescription\": \"Denies the lstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the mkdir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-mkdir\",\n          \"markdownDescription\": \"Denies the mkdir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read\",\n          \"markdownDescription\": \"Denies the read command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_dir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-dir\",\n          \"markdownDescription\": \"Denies the read_dir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-file\",\n          \"markdownDescription\": \"Denies the read_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file\",\n          \"markdownDescription\": \"Denies the read_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file_lines command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file-lines\",\n          \"markdownDescription\": \"Denies the read_text_file_lines command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file-lines-next\",\n          \"markdownDescription\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-remove\",\n          \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the rename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-rename\",\n          \"markdownDescription\": \"Denies the rename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the seek command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-seek\",\n          \"markdownDescription\": \"Denies the seek command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-size\",\n          \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the stat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-stat\",\n          \"markdownDescription\": \"Denies the stat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the truncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-truncate\",\n          \"markdownDescription\": \"Denies the truncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unwatch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-unwatch\",\n          \"markdownDescription\": \"Denies the unwatch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the watch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-watch\",\n          \"markdownDescription\": \"Denies the watch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-webview-data-linux\",\n          \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n        },\n        {\n          \"description\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-webview-data-windows\",\n          \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n        },\n        {\n          \"description\": \"Denies the write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write\",\n          \"markdownDescription\": \"Denies the write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write-file\",\n          \"markdownDescription\": \"Denies the write_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write-text-file\",\n          \"markdownDescription\": \"Denies the write_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This enables all read related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-all\",\n          \"markdownDescription\": \"This enables all read related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-app-specific-dirs-recursive\",\n          \"markdownDescription\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\"\n        },\n        {\n          \"description\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-dirs\",\n          \"markdownDescription\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables file read related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-files\",\n          \"markdownDescription\": \"This enables file read related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-meta\",\n          \"markdownDescription\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope\",\n          \"markdownDescription\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the application folders.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the application folders.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the application directories.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the application directories.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$CACHE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$CACHE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$EXE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$EXE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$FONT`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$FONT`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$HOME`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$HOME`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$LOG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$TEMP`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMP`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This enables all write related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:write-all\",\n          \"markdownDescription\": \"This enables all write related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables all file write related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:write-files\",\n          \"markdownDescription\": \"This enables all file write related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\",\n          \"type\": \"string\",\n          \"const\": \"process:default\",\n          \"markdownDescription\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\"\n        },\n        {\n          \"description\": \"Enables the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-exit\",\n          \"markdownDescription\": \"Enables the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-restart\",\n          \"markdownDescription\": \"Enables the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-exit\",\n          \"markdownDescription\": \"Denies the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-restart\",\n          \"markdownDescription\": \"Denies the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"shell:default\",\n          \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-execute\",\n          \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-kill\",\n          \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-spawn\",\n          \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-stdin-write\",\n          \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-execute\",\n          \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-kill\",\n          \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-spawn\",\n          \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-stdin-write\",\n          \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\",\n          \"type\": \"string\",\n          \"const\": \"updater:default\",\n          \"markdownDescription\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\"\n        },\n        {\n          \"description\": \"Enables the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-check\",\n          \"markdownDescription\": \"Enables the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download\",\n          \"markdownDescription\": \"Enables the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download-and-install\",\n          \"markdownDescription\": \"Enables the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-install\",\n          \"markdownDescription\": \"Enables the install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-check\",\n          \"markdownDescription\": \"Denies the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download\",\n          \"markdownDescription\": \"Denies the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download-and-install\",\n          \"markdownDescription\": \"Denies the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-install\",\n          \"markdownDescription\": \"Denies the install command without any pre-configured scope.\"\n        }\n      ]\n    },\n    \"Value\": {\n      \"description\": \"All supported ACL values.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents a null JSON value.\",\n          \"type\": \"null\"\n        },\n        {\n          \"description\": \"Represents a [`bool`].\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"Represents a valid ACL [`Number`].\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Number\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Represents a [`String`].\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"Represents a list of other [`Value`]s.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        },\n        {\n          \"description\": \"Represents a map of [`String`] keys to [`Value`]s.\",\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        }\n      ]\n    },\n    \"Number\": {\n      \"description\": \"A valid ACL number.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents an [`i64`].\",\n          \"type\": \"integer\",\n          \"format\": \"int64\"\n        },\n        {\n          \"description\": \"Represents a [`f64`].\",\n          \"type\": \"number\",\n          \"format\": \"double\"\n        }\n      ]\n    },\n    \"Target\": {\n      \"description\": \"Platform target.\",\n      \"oneOf\": [\n        {\n          \"description\": \"MacOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"macOS\"\n          ]\n        },\n        {\n          \"description\": \"Windows.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"windows\"\n          ]\n        },\n        {\n          \"description\": \"Linux.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"linux\"\n          ]\n        },\n        {\n          \"description\": \"Android.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"android\"\n          ]\n        },\n        {\n          \"description\": \"iOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"iOS\"\n          ]\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArg\": {\n      \"description\": \"A command argument allowed to be executed by the webview API.\",\n      \"anyOf\": [\n        {\n          \"description\": \"A non-configurable argument that is passed to the command in the order it was specified.\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"A variable that is set while calling the command from the webview API.\",\n          \"type\": \"object\",\n          \"required\": [\n            \"validator\"\n          ],\n          \"properties\": {\n            \"raw\": {\n              \"description\": \"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\\n\\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.\",\n              \"default\": false,\n              \"type\": \"boolean\"\n            },\n            \"validator\": {\n              \"description\": \"[regex] validator to require passed values to conform to an expected input.\\n\\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\\n\\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\\\w+` regex would be registered as `^https?://\\\\w+$`.\\n\\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>\",\n              \"type\": \"string\"\n            }\n          },\n          \"additionalProperties\": false\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArgs\": {\n      \"description\": \"A set of command arguments allowed to be executed by the webview API.\\n\\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Use a simple boolean to allow all or disable all arguments to this command configuration.\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/ShellScopeEntryAllowedArg\"\n          }\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tauri/src-tauri/gen/schemas/windows-schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"CapabilityFile\",\n  \"description\": \"Capability formats accepted in a capability file.\",\n  \"anyOf\": [\n    {\n      \"description\": \"A single capability.\",\n      \"allOf\": [\n        {\n          \"$ref\": \"#/definitions/Capability\"\n        }\n      ]\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/Capability\"\n      }\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"capabilities\"\n      ],\n      \"properties\": {\n        \"capabilities\": {\n          \"description\": \"The list of capabilities.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Capability\"\n          }\n        }\n      }\n    }\n  ],\n  \"definitions\": {\n    \"Capability\": {\n      \"description\": \"A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\\n\\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\\n\\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\\n\\n## Example\\n\\n```json { \\\"identifier\\\": \\\"main-user-files-write\\\", \\\"description\\\": \\\"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\\\", \\\"windows\\\": [ \\\"main\\\" ], \\\"permissions\\\": [ \\\"core:default\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] }, ], \\\"platforms\\\": [\\\"macOS\\\",\\\"windows\\\"] } ```\",\n      \"type\": \"object\",\n      \"required\": [\n        \"identifier\",\n        \"permissions\"\n      ],\n      \"properties\": {\n        \"identifier\": {\n          \"description\": \"Identifier of the capability.\\n\\n## Example\\n\\n`main-user-files-write`\",\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"description\": \"Description of what the capability is intended to allow on associated windows.\\n\\nIt should contain a description of what the grouped permissions should allow.\\n\\n## Example\\n\\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\",\n          \"default\": \"\",\n          \"type\": \"string\"\n        },\n        \"remote\": {\n          \"description\": \"Configure remote URLs that can use the capability permissions.\\n\\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\\n\\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\\n\\n## Example\\n\\n```json { \\\"urls\\\": [\\\"https://*.mydomain.dev\\\"] } ```\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/CapabilityRemote\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"local\": {\n          \"description\": \"Whether this capability is enabled for local app URLs or not. Defaults to `true`.\",\n          \"default\": true,\n          \"type\": \"boolean\"\n        },\n        \"windows\": {\n          \"description\": \"List of windows that are affected by this capability. Can be a glob pattern.\\n\\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\\n\\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\\n\\n## Example\\n\\n`[\\\"main\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"webviews\": {\n          \"description\": \"List of webviews that are affected by this capability. Can be a glob pattern.\\n\\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\\n\\n## Example\\n\\n`[\\\"sub-webview-one\\\", \\\"sub-webview-two\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"permissions\": {\n          \"description\": \"List of permissions attached to this capability.\\n\\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\\n\\n## Example\\n\\n```json [ \\\"core:default\\\", \\\"shell:allow-open\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] } ] ```\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/PermissionEntry\"\n          },\n          \"uniqueItems\": true\n        },\n        \"platforms\": {\n          \"description\": \"Limit which target platforms this capability applies to.\\n\\nBy default all platforms are targeted.\\n\\n## Example\\n\\n`[\\\"macOS\\\",\\\"windows\\\"]`\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/definitions/Target\"\n          }\n        }\n      }\n    },\n    \"CapabilityRemote\": {\n      \"description\": \"Configuration for remote URLs that are associated with the capability.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"urls\"\n      ],\n      \"properties\": {\n        \"urls\": {\n          \"description\": \"Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\\n\\n## Examples\\n\\n- \\\"https://*.mydomain.dev\\\": allows subdomains of mydomain.dev - \\\"https://mydomain.dev/api/*\\\": allows any subpath of mydomain.dev/api\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"PermissionEntry\": {\n      \"description\": \"An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Reference a permission or permission set by identifier.\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Identifier\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Reference a permission or permission set by identifier and extends its scope.\",\n          \"type\": \"object\",\n          \"allOf\": [\n            {\n              \"if\": {\n                \"properties\": {\n                  \"identifier\": {\n                    \"anyOf\": [\n                      {\n                        \"description\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:default\",\n                        \"markdownDescription\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-app-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appcache-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appconfig-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-appdata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applocaldata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-applog-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-audio-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-cache-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-config-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-data-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-desktop-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-document-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-download-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exe-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-font-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-home-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-localdata-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-log-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-picture-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-public-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-resource-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-runtime-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-temp-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-template-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-meta\",\n                        \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-meta-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-read\",\n                        \"markdownDescription\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-read-recursive\",\n                        \"markdownDescription\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-write\",\n                        \"markdownDescription\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\"\n                      },\n                      {\n                        \"description\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-video-write-recursive\",\n                        \"markdownDescription\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\"\n                      },\n                      {\n                        \"description\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-default\",\n                        \"markdownDescription\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\"\n                      },\n                      {\n                        \"description\": \"Enables the copy_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-copy-file\",\n                        \"markdownDescription\": \"Enables the copy_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the create command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-create\",\n                        \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the exists command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-exists\",\n                        \"markdownDescription\": \"Enables the exists command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the fstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-fstat\",\n                        \"markdownDescription\": \"Enables the fstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the ftruncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-ftruncate\",\n                        \"markdownDescription\": \"Enables the ftruncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the lstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-lstat\",\n                        \"markdownDescription\": \"Enables the lstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the mkdir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-mkdir\",\n                        \"markdownDescription\": \"Enables the mkdir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-open\",\n                        \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read\",\n                        \"markdownDescription\": \"Enables the read command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_dir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-dir\",\n                        \"markdownDescription\": \"Enables the read_dir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-file\",\n                        \"markdownDescription\": \"Enables the read_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file\",\n                        \"markdownDescription\": \"Enables the read_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file_lines command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file-lines\",\n                        \"markdownDescription\": \"Enables the read_text_file_lines command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-read-text-file-lines-next\",\n                        \"markdownDescription\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the remove command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-remove\",\n                        \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the rename command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-rename\",\n                        \"markdownDescription\": \"Enables the rename command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the seek command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-seek\",\n                        \"markdownDescription\": \"Enables the seek command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the size command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-size\",\n                        \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the stat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-stat\",\n                        \"markdownDescription\": \"Enables the stat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the truncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-truncate\",\n                        \"markdownDescription\": \"Enables the truncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the unwatch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-unwatch\",\n                        \"markdownDescription\": \"Enables the unwatch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the watch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-watch\",\n                        \"markdownDescription\": \"Enables the watch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write\",\n                        \"markdownDescription\": \"Enables the write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write-file\",\n                        \"markdownDescription\": \"Enables the write_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the write_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:allow-write-text-file\",\n                        \"markdownDescription\": \"Enables the write_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This permissions allows to create the application specific directories.\\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:create-app-specific-dirs\",\n                        \"markdownDescription\": \"This permissions allows to create the application specific directories.\\n\"\n                      },\n                      {\n                        \"description\": \"Denies the copy_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-copy-file\",\n                        \"markdownDescription\": \"Denies the copy_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the create command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-create\",\n                        \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the exists command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-exists\",\n                        \"markdownDescription\": \"Denies the exists command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the fstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-fstat\",\n                        \"markdownDescription\": \"Denies the fstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the ftruncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-ftruncate\",\n                        \"markdownDescription\": \"Denies the ftruncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the lstat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-lstat\",\n                        \"markdownDescription\": \"Denies the lstat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the mkdir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-mkdir\",\n                        \"markdownDescription\": \"Denies the mkdir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-open\",\n                        \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read\",\n                        \"markdownDescription\": \"Denies the read command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_dir command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-dir\",\n                        \"markdownDescription\": \"Denies the read_dir command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-file\",\n                        \"markdownDescription\": \"Denies the read_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file\",\n                        \"markdownDescription\": \"Denies the read_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file_lines command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file-lines\",\n                        \"markdownDescription\": \"Denies the read_text_file_lines command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-read-text-file-lines-next\",\n                        \"markdownDescription\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the remove command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-remove\",\n                        \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the rename command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-rename\",\n                        \"markdownDescription\": \"Denies the rename command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the seek command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-seek\",\n                        \"markdownDescription\": \"Denies the seek command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the size command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-size\",\n                        \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the stat command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-stat\",\n                        \"markdownDescription\": \"Denies the stat command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the truncate command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-truncate\",\n                        \"markdownDescription\": \"Denies the truncate command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the unwatch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-unwatch\",\n                        \"markdownDescription\": \"Denies the unwatch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the watch command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-watch\",\n                        \"markdownDescription\": \"Denies the watch command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-webview-data-linux\",\n                        \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n                      },\n                      {\n                        \"description\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-webview-data-windows\",\n                        \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n                      },\n                      {\n                        \"description\": \"Denies the write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write\",\n                        \"markdownDescription\": \"Denies the write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the write_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write-file\",\n                        \"markdownDescription\": \"Denies the write_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the write_text_file command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:deny-write-text-file\",\n                        \"markdownDescription\": \"Denies the write_text_file command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"This enables all read related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-all\",\n                        \"markdownDescription\": \"This enables all read related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-app-specific-dirs-recursive\",\n                        \"markdownDescription\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\"\n                      },\n                      {\n                        \"description\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-dirs\",\n                        \"markdownDescription\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables file read related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-files\",\n                        \"markdownDescription\": \"This enables file read related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:read-meta\",\n                        \"markdownDescription\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope\",\n                        \"markdownDescription\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the application folders.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the application folders.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the application directories.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the application directories.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-app-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appcache-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appconfig-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-appdata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applocaldata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-applog-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-audio-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$CACHE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$CACHE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-cache-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-config-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-data-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-desktop-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-document-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-download-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$EXE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$EXE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-exe-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$FONT`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$FONT`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-font-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$HOME`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$HOME`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-home-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-localdata-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$LOG`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOG`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-log-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-picture-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-public-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-resource-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-runtime-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$TEMP`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMP`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-temp-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-template-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video\",\n                        \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video-index\",\n                        \"markdownDescription\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\"\n                      },\n                      {\n                        \"description\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:scope-video-recursive\",\n                        \"markdownDescription\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\"\n                      },\n                      {\n                        \"description\": \"This enables all write related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:write-all\",\n                        \"markdownDescription\": \"This enables all write related commands without any pre-configured accessible paths.\"\n                      },\n                      {\n                        \"description\": \"This enables all file write related commands without any pre-configured accessible paths.\",\n                        \"type\": \"string\",\n                        \"const\": \"fs:write-files\",\n                        \"markdownDescription\": \"This enables all file write related commands without any pre-configured accessible paths.\"\n                      }\n                    ]\n                  }\n                }\n              },\n              \"then\": {\n                \"properties\": {\n                  \"allow\": {\n                    \"items\": {\n                      \"title\": \"FsScopeEntry\",\n                      \"description\": \"FS scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"description\": \"A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                          \"type\": \"string\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"path\"\n                          ],\n                          \"properties\": {\n                            \"path\": {\n                              \"description\": \"A path that can be accessed by the webview when using the fs APIs.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      ]\n                    }\n                  },\n                  \"deny\": {\n                    \"items\": {\n                      \"title\": \"FsScopeEntry\",\n                      \"description\": \"FS scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"description\": \"A path that can be accessed by the webview when using the fs APIs. FS scope path pattern.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                          \"type\": \"string\"\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"path\"\n                          ],\n                          \"properties\": {\n                            \"path\": {\n                              \"description\": \"A path that can be accessed by the webview when using the fs APIs.\\n\\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            }\n                          }\n                        }\n                      ]\n                    }\n                  }\n                }\n              },\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                }\n              }\n            },\n            {\n              \"if\": {\n                \"properties\": {\n                  \"identifier\": {\n                    \"anyOf\": [\n                      {\n                        \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:default\",\n                        \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n                      },\n                      {\n                        \"description\": \"Enables the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-execute\",\n                        \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-kill\",\n                        \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-open\",\n                        \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-spawn\",\n                        \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-stdin-write\",\n                        \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-execute\",\n                        \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-kill\",\n                        \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-open\",\n                        \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-spawn\",\n                        \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-stdin-write\",\n                        \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n                      }\n                    ]\n                  }\n                }\n              },\n              \"then\": {\n                \"properties\": {\n                  \"allow\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  },\n                  \"deny\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  }\n                }\n              },\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                }\n              }\n            },\n            {\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                },\n                \"allow\": {\n                  \"description\": \"Data that defines what is allowed by the scope.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                },\n                \"deny\": {\n                  \"description\": \"Data that defines what is denied by the scope. This should be prioritized by validation logic.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                }\n              }\n            }\n          ],\n          \"required\": [\n            \"identifier\"\n          ]\n        }\n      ]\n    },\n    \"Identifier\": {\n      \"description\": \"Permission identifier\",\n      \"oneOf\": [\n        {\n          \"description\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\",\n          \"type\": \"string\",\n          \"const\": \"core:default\",\n          \"markdownDescription\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\",\n          \"type\": \"string\",\n          \"const\": \"core:app:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\"\n        },\n        {\n          \"description\": \"Enables the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-hide\",\n          \"markdownDescription\": \"Enables the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-show\",\n          \"markdownDescription\": \"Enables the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-bundle-type\",\n          \"markdownDescription\": \"Enables the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-default-window-icon\",\n          \"markdownDescription\": \"Enables the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-identifier\",\n          \"markdownDescription\": \"Enables the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-name\",\n          \"markdownDescription\": \"Enables the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-register-listener\",\n          \"markdownDescription\": \"Enables the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-data-store\",\n          \"markdownDescription\": \"Enables the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-listener\",\n          \"markdownDescription\": \"Enables the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-app-theme\",\n          \"markdownDescription\": \"Enables the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-dock-visibility\",\n          \"markdownDescription\": \"Enables the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-tauri-version\",\n          \"markdownDescription\": \"Enables the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-version\",\n          \"markdownDescription\": \"Enables the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-hide\",\n          \"markdownDescription\": \"Denies the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-show\",\n          \"markdownDescription\": \"Denies the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-bundle-type\",\n          \"markdownDescription\": \"Denies the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-default-window-icon\",\n          \"markdownDescription\": \"Denies the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-identifier\",\n          \"markdownDescription\": \"Denies the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-name\",\n          \"markdownDescription\": \"Denies the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-register-listener\",\n          \"markdownDescription\": \"Denies the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-data-store\",\n          \"markdownDescription\": \"Denies the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-listener\",\n          \"markdownDescription\": \"Denies the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-app-theme\",\n          \"markdownDescription\": \"Denies the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-dock-visibility\",\n          \"markdownDescription\": \"Denies the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-tauri-version\",\n          \"markdownDescription\": \"Denies the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-version\",\n          \"markdownDescription\": \"Denies the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\",\n          \"type\": \"string\",\n          \"const\": \"core:event:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\"\n        },\n        {\n          \"description\": \"Enables the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit\",\n          \"markdownDescription\": \"Enables the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit-to\",\n          \"markdownDescription\": \"Enables the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-listen\",\n          \"markdownDescription\": \"Enables the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-unlisten\",\n          \"markdownDescription\": \"Enables the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit\",\n          \"markdownDescription\": \"Denies the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit-to\",\n          \"markdownDescription\": \"Denies the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-listen\",\n          \"markdownDescription\": \"Denies the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-unlisten\",\n          \"markdownDescription\": \"Denies the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\",\n          \"type\": \"string\",\n          \"const\": \"core:image:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\"\n        },\n        {\n          \"description\": \"Enables the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-bytes\",\n          \"markdownDescription\": \"Enables the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-path\",\n          \"markdownDescription\": \"Enables the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-rgba\",\n          \"markdownDescription\": \"Enables the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-size\",\n          \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-bytes\",\n          \"markdownDescription\": \"Denies the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-path\",\n          \"markdownDescription\": \"Denies the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-rgba\",\n          \"markdownDescription\": \"Denies the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-size\",\n          \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\"\n        },\n        {\n          \"description\": \"Enables the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-append\",\n          \"markdownDescription\": \"Enables the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-create-default\",\n          \"markdownDescription\": \"Enables the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-get\",\n          \"markdownDescription\": \"Enables the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-insert\",\n          \"markdownDescription\": \"Enables the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-checked\",\n          \"markdownDescription\": \"Enables the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-items\",\n          \"markdownDescription\": \"Enables the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-popup\",\n          \"markdownDescription\": \"Enables the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-prepend\",\n          \"markdownDescription\": \"Enables the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove\",\n          \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove-at\",\n          \"markdownDescription\": \"Enables the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-accelerator\",\n          \"markdownDescription\": \"Enables the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-app-menu\",\n          \"markdownDescription\": \"Enables the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-window-menu\",\n          \"markdownDescription\": \"Enables the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-checked\",\n          \"markdownDescription\": \"Enables the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-text\",\n          \"markdownDescription\": \"Enables the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-text\",\n          \"markdownDescription\": \"Enables the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-append\",\n          \"markdownDescription\": \"Denies the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-create-default\",\n          \"markdownDescription\": \"Denies the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-get\",\n          \"markdownDescription\": \"Denies the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-insert\",\n          \"markdownDescription\": \"Denies the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-checked\",\n          \"markdownDescription\": \"Denies the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-items\",\n          \"markdownDescription\": \"Denies the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-popup\",\n          \"markdownDescription\": \"Denies the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-prepend\",\n          \"markdownDescription\": \"Denies the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove\",\n          \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove-at\",\n          \"markdownDescription\": \"Denies the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-accelerator\",\n          \"markdownDescription\": \"Denies the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-app-menu\",\n          \"markdownDescription\": \"Denies the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-window-menu\",\n          \"markdownDescription\": \"Denies the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-checked\",\n          \"markdownDescription\": \"Denies the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-text\",\n          \"markdownDescription\": \"Denies the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-text\",\n          \"markdownDescription\": \"Denies the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\",\n          \"type\": \"string\",\n          \"const\": \"core:path:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\"\n        },\n        {\n          \"description\": \"Enables the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-basename\",\n          \"markdownDescription\": \"Enables the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-dirname\",\n          \"markdownDescription\": \"Enables the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-extname\",\n          \"markdownDescription\": \"Enables the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-is-absolute\",\n          \"markdownDescription\": \"Enables the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-join\",\n          \"markdownDescription\": \"Enables the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-normalize\",\n          \"markdownDescription\": \"Enables the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve\",\n          \"markdownDescription\": \"Enables the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve-directory\",\n          \"markdownDescription\": \"Enables the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-basename\",\n          \"markdownDescription\": \"Denies the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-dirname\",\n          \"markdownDescription\": \"Denies the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-extname\",\n          \"markdownDescription\": \"Denies the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-is-absolute\",\n          \"markdownDescription\": \"Denies the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-join\",\n          \"markdownDescription\": \"Denies the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-normalize\",\n          \"markdownDescription\": \"Denies the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve\",\n          \"markdownDescription\": \"Denies the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve-directory\",\n          \"markdownDescription\": \"Denies the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\"\n        },\n        {\n          \"description\": \"Enables the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-get-by-id\",\n          \"markdownDescription\": \"Enables the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-remove-by-id\",\n          \"markdownDescription\": \"Enables the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon-as-template\",\n          \"markdownDescription\": \"Enables the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-menu\",\n          \"markdownDescription\": \"Enables the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-temp-dir-path\",\n          \"markdownDescription\": \"Enables the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-tooltip\",\n          \"markdownDescription\": \"Enables the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-visible\",\n          \"markdownDescription\": \"Enables the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-get-by-id\",\n          \"markdownDescription\": \"Denies the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-remove-by-id\",\n          \"markdownDescription\": \"Denies the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon-as-template\",\n          \"markdownDescription\": \"Denies the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-menu\",\n          \"markdownDescription\": \"Denies the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-temp-dir-path\",\n          \"markdownDescription\": \"Denies the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-tooltip\",\n          \"markdownDescription\": \"Denies the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-visible\",\n          \"markdownDescription\": \"Denies the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\"\n        },\n        {\n          \"description\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-clear-all-browsing-data\",\n          \"markdownDescription\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview\",\n          \"markdownDescription\": \"Enables the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview-window\",\n          \"markdownDescription\": \"Enables the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-get-all-webviews\",\n          \"markdownDescription\": \"Enables the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-internal-toggle-devtools\",\n          \"markdownDescription\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-print\",\n          \"markdownDescription\": \"Enables the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-reparent\",\n          \"markdownDescription\": \"Enables the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-auto-resize\",\n          \"markdownDescription\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-background-color\",\n          \"markdownDescription\": \"Enables the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-focus\",\n          \"markdownDescription\": \"Enables the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-position\",\n          \"markdownDescription\": \"Enables the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-size\",\n          \"markdownDescription\": \"Enables the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-zoom\",\n          \"markdownDescription\": \"Enables the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-close\",\n          \"markdownDescription\": \"Enables the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-hide\",\n          \"markdownDescription\": \"Enables the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-position\",\n          \"markdownDescription\": \"Enables the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-show\",\n          \"markdownDescription\": \"Enables the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-size\",\n          \"markdownDescription\": \"Enables the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-clear-all-browsing-data\",\n          \"markdownDescription\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview\",\n          \"markdownDescription\": \"Denies the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview-window\",\n          \"markdownDescription\": \"Denies the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-get-all-webviews\",\n          \"markdownDescription\": \"Denies the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-internal-toggle-devtools\",\n          \"markdownDescription\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-print\",\n          \"markdownDescription\": \"Denies the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-reparent\",\n          \"markdownDescription\": \"Denies the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-auto-resize\",\n          \"markdownDescription\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-background-color\",\n          \"markdownDescription\": \"Denies the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-focus\",\n          \"markdownDescription\": \"Denies the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-position\",\n          \"markdownDescription\": \"Denies the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-size\",\n          \"markdownDescription\": \"Denies the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-zoom\",\n          \"markdownDescription\": \"Denies the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-close\",\n          \"markdownDescription\": \"Denies the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-hide\",\n          \"markdownDescription\": \"Denies the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-position\",\n          \"markdownDescription\": \"Denies the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-show\",\n          \"markdownDescription\": \"Denies the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-size\",\n          \"markdownDescription\": \"Denies the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\",\n          \"type\": \"string\",\n          \"const\": \"core:window:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\"\n        },\n        {\n          \"description\": \"Enables the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-available-monitors\",\n          \"markdownDescription\": \"Enables the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-center\",\n          \"markdownDescription\": \"Enables the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-create\",\n          \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-current-monitor\",\n          \"markdownDescription\": \"Enables the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-cursor-position\",\n          \"markdownDescription\": \"Enables the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-destroy\",\n          \"markdownDescription\": \"Enables the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-get-all-windows\",\n          \"markdownDescription\": \"Enables the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-hide\",\n          \"markdownDescription\": \"Enables the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-position\",\n          \"markdownDescription\": \"Enables the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-size\",\n          \"markdownDescription\": \"Enables the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-internal-toggle-maximize\",\n          \"markdownDescription\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-always-on-top\",\n          \"markdownDescription\": \"Enables the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-closable\",\n          \"markdownDescription\": \"Enables the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-decorated\",\n          \"markdownDescription\": \"Enables the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-focused\",\n          \"markdownDescription\": \"Enables the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-fullscreen\",\n          \"markdownDescription\": \"Enables the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximizable\",\n          \"markdownDescription\": \"Enables the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximized\",\n          \"markdownDescription\": \"Enables the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimizable\",\n          \"markdownDescription\": \"Enables the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimized\",\n          \"markdownDescription\": \"Enables the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-resizable\",\n          \"markdownDescription\": \"Enables the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-visible\",\n          \"markdownDescription\": \"Enables the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-maximize\",\n          \"markdownDescription\": \"Enables the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-minimize\",\n          \"markdownDescription\": \"Enables the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-monitor-from-point\",\n          \"markdownDescription\": \"Enables the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-position\",\n          \"markdownDescription\": \"Enables the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-size\",\n          \"markdownDescription\": \"Enables the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-primary-monitor\",\n          \"markdownDescription\": \"Enables the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-request-user-attention\",\n          \"markdownDescription\": \"Enables the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-scale-factor\",\n          \"markdownDescription\": \"Enables the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-bottom\",\n          \"markdownDescription\": \"Enables the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-top\",\n          \"markdownDescription\": \"Enables the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-background-color\",\n          \"markdownDescription\": \"Enables the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-count\",\n          \"markdownDescription\": \"Enables the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-label\",\n          \"markdownDescription\": \"Enables the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-closable\",\n          \"markdownDescription\": \"Enables the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-content-protected\",\n          \"markdownDescription\": \"Enables the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-grab\",\n          \"markdownDescription\": \"Enables the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-icon\",\n          \"markdownDescription\": \"Enables the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-position\",\n          \"markdownDescription\": \"Enables the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-visible\",\n          \"markdownDescription\": \"Enables the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-decorations\",\n          \"markdownDescription\": \"Enables the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-effects\",\n          \"markdownDescription\": \"Enables the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focus\",\n          \"markdownDescription\": \"Enables the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focusable\",\n          \"markdownDescription\": \"Enables the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-fullscreen\",\n          \"markdownDescription\": \"Enables the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-max-size\",\n          \"markdownDescription\": \"Enables the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-maximizable\",\n          \"markdownDescription\": \"Enables the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-min-size\",\n          \"markdownDescription\": \"Enables the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-minimizable\",\n          \"markdownDescription\": \"Enables the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-overlay-icon\",\n          \"markdownDescription\": \"Enables the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-position\",\n          \"markdownDescription\": \"Enables the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-progress-bar\",\n          \"markdownDescription\": \"Enables the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-resizable\",\n          \"markdownDescription\": \"Enables the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-shadow\",\n          \"markdownDescription\": \"Enables the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-simple-fullscreen\",\n          \"markdownDescription\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size\",\n          \"markdownDescription\": \"Enables the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size-constraints\",\n          \"markdownDescription\": \"Enables the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-skip-taskbar\",\n          \"markdownDescription\": \"Enables the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-theme\",\n          \"markdownDescription\": \"Enables the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title-bar-style\",\n          \"markdownDescription\": \"Enables the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-show\",\n          \"markdownDescription\": \"Enables the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-dragging\",\n          \"markdownDescription\": \"Enables the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-resize-dragging\",\n          \"markdownDescription\": \"Enables the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-theme\",\n          \"markdownDescription\": \"Enables the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-title\",\n          \"markdownDescription\": \"Enables the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-toggle-maximize\",\n          \"markdownDescription\": \"Enables the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unmaximize\",\n          \"markdownDescription\": \"Enables the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unminimize\",\n          \"markdownDescription\": \"Enables the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-available-monitors\",\n          \"markdownDescription\": \"Denies the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-center\",\n          \"markdownDescription\": \"Denies the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-create\",\n          \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-current-monitor\",\n          \"markdownDescription\": \"Denies the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-cursor-position\",\n          \"markdownDescription\": \"Denies the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-destroy\",\n          \"markdownDescription\": \"Denies the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-get-all-windows\",\n          \"markdownDescription\": \"Denies the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-hide\",\n          \"markdownDescription\": \"Denies the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-position\",\n          \"markdownDescription\": \"Denies the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-size\",\n          \"markdownDescription\": \"Denies the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-internal-toggle-maximize\",\n          \"markdownDescription\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-always-on-top\",\n          \"markdownDescription\": \"Denies the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-closable\",\n          \"markdownDescription\": \"Denies the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-decorated\",\n          \"markdownDescription\": \"Denies the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-focused\",\n          \"markdownDescription\": \"Denies the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-fullscreen\",\n          \"markdownDescription\": \"Denies the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximizable\",\n          \"markdownDescription\": \"Denies the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximized\",\n          \"markdownDescription\": \"Denies the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimizable\",\n          \"markdownDescription\": \"Denies the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimized\",\n          \"markdownDescription\": \"Denies the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-resizable\",\n          \"markdownDescription\": \"Denies the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-visible\",\n          \"markdownDescription\": \"Denies the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-maximize\",\n          \"markdownDescription\": \"Denies the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-minimize\",\n          \"markdownDescription\": \"Denies the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-monitor-from-point\",\n          \"markdownDescription\": \"Denies the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-position\",\n          \"markdownDescription\": \"Denies the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-size\",\n          \"markdownDescription\": \"Denies the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-primary-monitor\",\n          \"markdownDescription\": \"Denies the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-request-user-attention\",\n          \"markdownDescription\": \"Denies the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-scale-factor\",\n          \"markdownDescription\": \"Denies the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-bottom\",\n          \"markdownDescription\": \"Denies the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-top\",\n          \"markdownDescription\": \"Denies the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-background-color\",\n          \"markdownDescription\": \"Denies the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-count\",\n          \"markdownDescription\": \"Denies the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-label\",\n          \"markdownDescription\": \"Denies the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-closable\",\n          \"markdownDescription\": \"Denies the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-content-protected\",\n          \"markdownDescription\": \"Denies the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-grab\",\n          \"markdownDescription\": \"Denies the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-icon\",\n          \"markdownDescription\": \"Denies the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-position\",\n          \"markdownDescription\": \"Denies the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-visible\",\n          \"markdownDescription\": \"Denies the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-decorations\",\n          \"markdownDescription\": \"Denies the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-effects\",\n          \"markdownDescription\": \"Denies the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focus\",\n          \"markdownDescription\": \"Denies the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focusable\",\n          \"markdownDescription\": \"Denies the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-fullscreen\",\n          \"markdownDescription\": \"Denies the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-max-size\",\n          \"markdownDescription\": \"Denies the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-maximizable\",\n          \"markdownDescription\": \"Denies the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-min-size\",\n          \"markdownDescription\": \"Denies the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-minimizable\",\n          \"markdownDescription\": \"Denies the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-overlay-icon\",\n          \"markdownDescription\": \"Denies the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-position\",\n          \"markdownDescription\": \"Denies the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-progress-bar\",\n          \"markdownDescription\": \"Denies the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-resizable\",\n          \"markdownDescription\": \"Denies the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-shadow\",\n          \"markdownDescription\": \"Denies the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-simple-fullscreen\",\n          \"markdownDescription\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size\",\n          \"markdownDescription\": \"Denies the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size-constraints\",\n          \"markdownDescription\": \"Denies the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-skip-taskbar\",\n          \"markdownDescription\": \"Denies the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-theme\",\n          \"markdownDescription\": \"Denies the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title-bar-style\",\n          \"markdownDescription\": \"Denies the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-show\",\n          \"markdownDescription\": \"Denies the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-dragging\",\n          \"markdownDescription\": \"Denies the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-resize-dragging\",\n          \"markdownDescription\": \"Denies the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-theme\",\n          \"markdownDescription\": \"Denies the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-title\",\n          \"markdownDescription\": \"Denies the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-toggle-maximize\",\n          \"markdownDescription\": \"Denies the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unmaximize\",\n          \"markdownDescription\": \"Denies the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unminimize\",\n          \"markdownDescription\": \"Denies the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"dialog:default\",\n          \"markdownDescription\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-ask\",\n          \"markdownDescription\": \"Enables the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-confirm\",\n          \"markdownDescription\": \"Enables the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-message\",\n          \"markdownDescription\": \"Enables the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-save\",\n          \"markdownDescription\": \"Enables the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-ask\",\n          \"markdownDescription\": \"Denies the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-confirm\",\n          \"markdownDescription\": \"Denies the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-message\",\n          \"markdownDescription\": \"Denies the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-save\",\n          \"markdownDescription\": \"Denies the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\",\n          \"type\": \"string\",\n          \"const\": \"fs:default\",\n          \"markdownDescription\": \"This set of permissions describes the what kind of\\nfile system access the `fs` plugin has enabled or denied by default.\\n\\n#### Granted Permissions\\n\\nThis default permission set enables read access to the\\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\\nAppLog) and all files and sub directories created in it.\\nThe location of these directories depends on the operating system,\\nwhere the application is run.\\n\\nIn general these directories need to be manually created\\nby the application at runtime, before accessing files or folders\\nin it is possible.\\n\\nTherefore, it is also allowed to create all of these folders via\\nthe `mkdir` command.\\n\\n#### Denied Permissions\\n\\nThis default permission set prevents access to critical components\\nof the Tauri application by default.\\nOn Windows the webview data folder access is denied.\\n\\n#### This default permission set includes:\\n\\n- `create-app-specific-dirs`\\n- `read-app-specific-dirs-recursive`\\n- `deny-default`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the application folders, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the application folders.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the application folders.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-app-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete application folders, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-app-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPCACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appcache-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appcache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPCONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPCONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appconfig-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPCONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appconfig-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-appdata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-appdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applocaldata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applocaldata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$APPLOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$APPLOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-applog-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$APPLOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-applog-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$AUDIO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$AUDIO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-audio-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$AUDIO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-audio-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CACHE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$CACHE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-cache-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$CACHE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-cache-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$CONFIG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$CONFIG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-config-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$CONFIG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-config-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-data-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-data-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DESKTOP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DESKTOP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-desktop-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DESKTOP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-desktop-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOCUMENT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DOCUMENT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-document-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DOCUMENT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-document-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$DOWNLOAD` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$DOWNLOAD` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-download-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$DOWNLOAD` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-download-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$EXE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$EXE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exe-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$EXE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-exe-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$FONT` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$FONT` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-font-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$FONT` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-font-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$HOME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$HOME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-home-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$HOME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-home-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOCALDATA` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$LOCALDATA` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-localdata-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$LOCALDATA` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-localdata-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$LOG` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$LOG` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-log-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$LOG` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-log-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PICTURE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$PICTURE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-picture-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$PICTURE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-picture-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$PUBLIC` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$PUBLIC` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-public-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$PUBLIC` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-public-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RESOURCE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$RESOURCE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-resource-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$RESOURCE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-resource-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$RUNTIME` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$RUNTIME` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-runtime-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$RUNTIME` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-runtime-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMP` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$TEMP` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-temp-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMP` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-temp-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$TEMPLATE` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$TEMPLATE` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-template-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$TEMPLATE` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-template-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-meta\",\n          \"markdownDescription\": \"This allows non-recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-index`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-meta-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to metadata of the `$VIDEO` folder, including file listing and statistics.\\n#### This permission set includes:\\n\\n- `read-meta`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-read\",\n          \"markdownDescription\": \"This allows non-recursive read access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video`\"\n        },\n        {\n          \"description\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-read-recursive\",\n          \"markdownDescription\": \"This allows full recursive read access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `read-all`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-write\",\n          \"markdownDescription\": \"This allows non-recursive write access to the `$VIDEO` folder.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video`\"\n        },\n        {\n          \"description\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-video-write-recursive\",\n          \"markdownDescription\": \"This allows full recursive write access to the complete `$VIDEO` folder, files and subdirectories.\\n#### This permission set includes:\\n\\n- `write-all`\\n- `scope-video-recursive`\"\n        },\n        {\n          \"description\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-default\",\n          \"markdownDescription\": \"This denies access to dangerous Tauri relevant files and folders by default.\\n#### This permission set includes:\\n\\n- `deny-webview-data-linux`\\n- `deny-webview-data-windows`\"\n        },\n        {\n          \"description\": \"Enables the copy_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-copy-file\",\n          \"markdownDescription\": \"Enables the copy_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-create\",\n          \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the exists command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-exists\",\n          \"markdownDescription\": \"Enables the exists command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the fstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-fstat\",\n          \"markdownDescription\": \"Enables the fstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the ftruncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-ftruncate\",\n          \"markdownDescription\": \"Enables the ftruncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the lstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-lstat\",\n          \"markdownDescription\": \"Enables the lstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the mkdir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-mkdir\",\n          \"markdownDescription\": \"Enables the mkdir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read\",\n          \"markdownDescription\": \"Enables the read command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_dir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-dir\",\n          \"markdownDescription\": \"Enables the read_dir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-file\",\n          \"markdownDescription\": \"Enables the read_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file\",\n          \"markdownDescription\": \"Enables the read_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file_lines command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file-lines\",\n          \"markdownDescription\": \"Enables the read_text_file_lines command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-read-text-file-lines-next\",\n          \"markdownDescription\": \"Enables the read_text_file_lines_next command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-remove\",\n          \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the rename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-rename\",\n          \"markdownDescription\": \"Enables the rename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the seek command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-seek\",\n          \"markdownDescription\": \"Enables the seek command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-size\",\n          \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the stat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-stat\",\n          \"markdownDescription\": \"Enables the stat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the truncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-truncate\",\n          \"markdownDescription\": \"Enables the truncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unwatch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-unwatch\",\n          \"markdownDescription\": \"Enables the unwatch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the watch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-watch\",\n          \"markdownDescription\": \"Enables the watch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write\",\n          \"markdownDescription\": \"Enables the write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write-file\",\n          \"markdownDescription\": \"Enables the write_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:allow-write-text-file\",\n          \"markdownDescription\": \"Enables the write_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permissions allows to create the application specific directories.\\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:create-app-specific-dirs\",\n          \"markdownDescription\": \"This permissions allows to create the application specific directories.\\n\"\n        },\n        {\n          \"description\": \"Denies the copy_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-copy-file\",\n          \"markdownDescription\": \"Denies the copy_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-create\",\n          \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the exists command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-exists\",\n          \"markdownDescription\": \"Denies the exists command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the fstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-fstat\",\n          \"markdownDescription\": \"Denies the fstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the ftruncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-ftruncate\",\n          \"markdownDescription\": \"Denies the ftruncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the lstat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-lstat\",\n          \"markdownDescription\": \"Denies the lstat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the mkdir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-mkdir\",\n          \"markdownDescription\": \"Denies the mkdir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read\",\n          \"markdownDescription\": \"Denies the read command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_dir command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-dir\",\n          \"markdownDescription\": \"Denies the read_dir command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-file\",\n          \"markdownDescription\": \"Denies the read_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file\",\n          \"markdownDescription\": \"Denies the read_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file_lines command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file-lines\",\n          \"markdownDescription\": \"Denies the read_text_file_lines command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-read-text-file-lines-next\",\n          \"markdownDescription\": \"Denies the read_text_file_lines_next command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-remove\",\n          \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the rename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-rename\",\n          \"markdownDescription\": \"Denies the rename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the seek command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-seek\",\n          \"markdownDescription\": \"Denies the seek command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-size\",\n          \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the stat command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-stat\",\n          \"markdownDescription\": \"Denies the stat command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the truncate command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-truncate\",\n          \"markdownDescription\": \"Denies the truncate command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unwatch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-unwatch\",\n          \"markdownDescription\": \"Denies the unwatch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the watch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-watch\",\n          \"markdownDescription\": \"Denies the watch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-webview-data-linux\",\n          \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n        },\n        {\n          \"description\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-webview-data-windows\",\n          \"markdownDescription\": \"This denies read access to the\\n`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.\\nAllowing access can lead to sensitive information disclosure and should be well considered.\"\n        },\n        {\n          \"description\": \"Denies the write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write\",\n          \"markdownDescription\": \"Denies the write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write-file\",\n          \"markdownDescription\": \"Denies the write_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_text_file command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"fs:deny-write-text-file\",\n          \"markdownDescription\": \"Denies the write_text_file command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This enables all read related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-all\",\n          \"markdownDescription\": \"This enables all read related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-app-specific-dirs-recursive\",\n          \"markdownDescription\": \"This permission allows recursive read functionality on the application\\nspecific base directories. \\n\"\n        },\n        {\n          \"description\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-dirs\",\n          \"markdownDescription\": \"This enables directory read and file metadata related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables file read related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-files\",\n          \"markdownDescription\": \"This enables file read related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:read-meta\",\n          \"markdownDescription\": \"This enables all index or metadata related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope\",\n          \"markdownDescription\": \"An empty permission you can use to modify the global scope.\\n\\n## Example\\n\\n```json\\n{\\n  \\\"identifier\\\": \\\"read-documents\\\",\\n  \\\"windows\\\": [\\\"main\\\"],\\n  \\\"permissions\\\": [\\n    \\\"fs:allow-read\\\",\\n    {\\n      \\\"identifier\\\": \\\"fs:scope\\\",\\n      \\\"allow\\\": [\\n        \\\"$APPDATA/documents/**/*\\\"\\n      ],\\n      \\\"deny\\\": [\\n        \\\"$APPDATA/documents/secret.txt\\\"\\n      ]\\n    }\\n  ]\\n}\\n```\\n\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the application folders.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the application folders.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the application directories.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the application directories.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-app-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete application folders, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCACHE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCACHE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appcache-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCACHE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPCONFIG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPCONFIG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appconfig-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPCONFIG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-appdata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOCALDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOCALDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applocaldata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOCALDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$APPLOG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$APPLOG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-applog-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$APPLOG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$AUDIO` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$AUDIO`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-audio-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$AUDIO` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CACHE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$CACHE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$CACHE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-cache-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$CACHE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$CONFIG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$CONFIG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-config-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$CONFIG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-data-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DESKTOP` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DESKTOP`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-desktop-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DESKTOP` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOCUMENT` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOCUMENT`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-document-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DOCUMENT` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$DOWNLOAD` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$DOWNLOAD`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-download-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$DOWNLOAD` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$EXE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$EXE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$EXE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-exe-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$EXE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$FONT` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$FONT`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$FONT`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-font-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$FONT` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$HOME` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$HOME`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$HOME`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-home-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$HOME` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOCALDATA` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOCALDATA`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-localdata-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$LOCALDATA` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$LOG` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$LOG`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$LOG`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-log-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$LOG` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PICTURE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$PICTURE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-picture-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$PICTURE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$PUBLIC` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$PUBLIC`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-public-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$PUBLIC` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RESOURCE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$RESOURCE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-resource-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$RESOURCE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$RUNTIME` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$RUNTIME`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-runtime-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$RUNTIME` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMP` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$TEMP`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMP`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-temp-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMP` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$TEMPLATE` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$TEMPLATE`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-template-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$TEMPLATE` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video\",\n          \"markdownDescription\": \"This scope permits access to all files and list content of top level directories in the `$VIDEO` folder.\"\n        },\n        {\n          \"description\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video-index\",\n          \"markdownDescription\": \"This scope permits to list all files and folders in the `$VIDEO`folder.\"\n        },\n        {\n          \"description\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\",\n          \"type\": \"string\",\n          \"const\": \"fs:scope-video-recursive\",\n          \"markdownDescription\": \"This scope permits recursive access to the complete `$VIDEO` folder, including sub directories and files.\"\n        },\n        {\n          \"description\": \"This enables all write related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:write-all\",\n          \"markdownDescription\": \"This enables all write related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This enables all file write related commands without any pre-configured accessible paths.\",\n          \"type\": \"string\",\n          \"const\": \"fs:write-files\",\n          \"markdownDescription\": \"This enables all file write related commands without any pre-configured accessible paths.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\",\n          \"type\": \"string\",\n          \"const\": \"process:default\",\n          \"markdownDescription\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\"\n        },\n        {\n          \"description\": \"Enables the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-exit\",\n          \"markdownDescription\": \"Enables the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-restart\",\n          \"markdownDescription\": \"Enables the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-exit\",\n          \"markdownDescription\": \"Denies the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-restart\",\n          \"markdownDescription\": \"Denies the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"shell:default\",\n          \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-execute\",\n          \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-kill\",\n          \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-spawn\",\n          \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-stdin-write\",\n          \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-execute\",\n          \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-kill\",\n          \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-spawn\",\n          \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-stdin-write\",\n          \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\",\n          \"type\": \"string\",\n          \"const\": \"updater:default\",\n          \"markdownDescription\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\"\n        },\n        {\n          \"description\": \"Enables the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-check\",\n          \"markdownDescription\": \"Enables the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download\",\n          \"markdownDescription\": \"Enables the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download-and-install\",\n          \"markdownDescription\": \"Enables the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-install\",\n          \"markdownDescription\": \"Enables the install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-check\",\n          \"markdownDescription\": \"Denies the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download\",\n          \"markdownDescription\": \"Denies the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download-and-install\",\n          \"markdownDescription\": \"Denies the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-install\",\n          \"markdownDescription\": \"Denies the install command without any pre-configured scope.\"\n        }\n      ]\n    },\n    \"Value\": {\n      \"description\": \"All supported ACL values.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents a null JSON value.\",\n          \"type\": \"null\"\n        },\n        {\n          \"description\": \"Represents a [`bool`].\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"Represents a valid ACL [`Number`].\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Number\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Represents a [`String`].\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"Represents a list of other [`Value`]s.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        },\n        {\n          \"description\": \"Represents a map of [`String`] keys to [`Value`]s.\",\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        }\n      ]\n    },\n    \"Number\": {\n      \"description\": \"A valid ACL number.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents an [`i64`].\",\n          \"type\": \"integer\",\n          \"format\": \"int64\"\n        },\n        {\n          \"description\": \"Represents a [`f64`].\",\n          \"type\": \"number\",\n          \"format\": \"double\"\n        }\n      ]\n    },\n    \"Target\": {\n      \"description\": \"Platform target.\",\n      \"oneOf\": [\n        {\n          \"description\": \"MacOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"macOS\"\n          ]\n        },\n        {\n          \"description\": \"Windows.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"windows\"\n          ]\n        },\n        {\n          \"description\": \"Linux.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"linux\"\n          ]\n        },\n        {\n          \"description\": \"Android.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"android\"\n          ]\n        },\n        {\n          \"description\": \"iOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"iOS\"\n          ]\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArg\": {\n      \"description\": \"A command argument allowed to be executed by the webview API.\",\n      \"anyOf\": [\n        {\n          \"description\": \"A non-configurable argument that is passed to the command in the order it was specified.\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"A variable that is set while calling the command from the webview API.\",\n          \"type\": \"object\",\n          \"required\": [\n            \"validator\"\n          ],\n          \"properties\": {\n            \"raw\": {\n              \"description\": \"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\\n\\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.\",\n              \"default\": false,\n              \"type\": \"boolean\"\n            },\n            \"validator\": {\n              \"description\": \"[regex] validator to require passed values to conform to an expected input.\\n\\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\\n\\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\\\w+` regex would be registered as `^https?://\\\\w+$`.\\n\\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>\",\n              \"type\": \"string\"\n            }\n          },\n          \"additionalProperties\": false\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArgs\": {\n      \"description\": \"A set of command arguments allowed to be executed by the webview API.\\n\\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Use a simple boolean to allow all or disable all arguments to this command configuration.\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/ShellScopeEntryAllowedArg\"\n          }\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "tauri/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n  <background android:drawable=\"@color/ic_launcher_background\"/>\n</adaptive-icon>"
  },
  {
    "path": "tauri/src-tauri/icons/android/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n  <color name=\"ic_launcher_background\">#fff</color>\n</resources>"
  },
  {
    "path": "tauri/src-tauri/src/audio_capture/linux.rs",
    "content": "use crate::audio_capture::AudioCaptureState;\nuse base64::{engine::general_purpose, Engine as _};\nuse cpal::traits::{DeviceTrait, HostTrait, StreamTrait};\nuse cpal::{SampleFormat, StreamConfig};\nuse hound::{WavSpec, WavWriter};\nuse std::io::Cursor;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse std::thread;\n\n/// Start capturing system audio on Linux using PulseAudio monitor sources.\n///\n/// PulseAudio exposes \"monitor\" devices that mirror the output of each sink,\n/// allowing us to capture whatever audio is currently playing on the system.\n/// We use `cpal` with the default host (which will be PulseAudio or PipeWire\n/// on modern Linux) and look for monitor input devices.\npub async fn start_capture(\n    state: &AudioCaptureState,\n    max_duration_secs: u32,\n) -> Result<(), String> {\n    // Reset previous samples\n    state.reset();\n\n    let samples = state.samples.clone();\n    let sample_rate_arc = state.sample_rate.clone();\n    let channels_arc = state.channels.clone();\n    let stop_tx = state.stop_tx.clone();\n    let error_arc = state.error.clone();\n\n    // Use AtomicBool for stop signal (works across threads)\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    let stop_flag_clone = stop_flag.clone();\n\n    // Create tokio channel and spawn a task to bridge it to the AtomicBool\n    let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);\n    *stop_tx.lock().unwrap() = Some(tx);\n\n    tokio::spawn(async move {\n        rx.recv().await;\n        stop_flag_clone.store(true, Ordering::Relaxed);\n    });\n\n    // Spawn capture on a dedicated thread\n    thread::spawn(move || {\n        let host = cpal::default_host();\n\n        // Try to find a monitor device for system audio capture.\n        // On PulseAudio/PipeWire, monitor sources have \"monitor\" in their name.\n        let device = {\n            let mut monitor_device = None;\n\n            if let Ok(devices) = host.input_devices() {\n                for d in devices {\n                    if let Ok(name) = d.name() {\n                        let name_lower = name.to_lowercase();\n                        if name_lower.contains(\"monitor\") {\n                            eprintln!(\"Linux audio capture: Found monitor device: {}\", name);\n                            monitor_device = Some(d);\n                            break;\n                        }\n                    }\n                }\n            }\n\n            match monitor_device {\n                Some(d) => d,\n                None => {\n                    // Fallback to default input device (microphone)\n                    eprintln!(\"Linux audio capture: No monitor device found, falling back to default input\");\n                    match host.default_input_device() {\n                        Some(d) => d,\n                        None => {\n                            let error_msg = \"No audio input device available\".to_string();\n                            eprintln!(\"{}\", error_msg);\n                            *error_arc.lock().unwrap() = Some(error_msg);\n                            return;\n                        }\n                    }\n                }\n            }\n        };\n\n        let device_name = device.name().unwrap_or_else(|_| \"unknown\".to_string());\n        eprintln!(\"Linux audio capture: Using device: {}\", device_name);\n\n        // Get supported config\n        let config = match device.default_input_config() {\n            Ok(c) => c,\n            Err(e) => {\n                let error_msg = format!(\"Failed to get default input config: {}\", e);\n                eprintln!(\"{}\", error_msg);\n                *error_arc.lock().unwrap() = Some(error_msg);\n                return;\n            }\n        };\n\n        let sample_rate = config.sample_rate().0;\n        let channels = config.channels();\n        let sample_format = config.sample_format();\n\n        eprintln!(\n            \"Linux audio capture: Config - {}Hz, {} channels, format: {:?}\",\n            sample_rate, channels, sample_format\n        );\n\n        *sample_rate_arc.lock().unwrap() = sample_rate;\n        *channels_arc.lock().unwrap() = channels;\n\n        let stream_config = StreamConfig {\n            channels,\n            sample_rate: cpal::SampleRate(sample_rate),\n            buffer_size: cpal::BufferSize::Default,\n        };\n\n        let samples_clone = samples.clone();\n        let error_arc_clone = error_arc.clone();\n        let stop_flag_for_stream = stop_flag.clone();\n\n        let err_fn = {\n            let error_arc = error_arc.clone();\n            move |err: cpal::StreamError| {\n                let error_msg = format!(\"Stream error: {}\", err);\n                eprintln!(\"{}\", error_msg);\n                *error_arc.lock().unwrap() = Some(error_msg);\n            }\n        };\n\n        let stream = match sample_format {\n            SampleFormat::F32 => {\n                let samples = samples_clone.clone();\n                let stop = stop_flag_for_stream.clone();\n                device.build_input_stream(\n                    &stream_config,\n                    move |data: &[f32], _: &cpal::InputCallbackInfo| {\n                        if stop.load(Ordering::Relaxed) {\n                            return;\n                        }\n                        let mut guard = samples.lock().unwrap();\n                        guard.extend_from_slice(data);\n                    },\n                    err_fn,\n                    None,\n                )\n            }\n            SampleFormat::I16 => {\n                let samples = samples_clone.clone();\n                let stop = stop_flag_for_stream.clone();\n                device.build_input_stream(\n                    &stream_config,\n                    move |data: &[i16], _: &cpal::InputCallbackInfo| {\n                        if stop.load(Ordering::Relaxed) {\n                            return;\n                        }\n                        let mut guard = samples.lock().unwrap();\n                        for &s in data {\n                            guard.push(s as f32 / 32768.0);\n                        }\n                    },\n                    err_fn,\n                    None,\n                )\n            }\n            SampleFormat::U16 => {\n                let samples = samples_clone.clone();\n                let stop = stop_flag_for_stream.clone();\n                device.build_input_stream(\n                    &stream_config,\n                    move |data: &[u16], _: &cpal::InputCallbackInfo| {\n                        if stop.load(Ordering::Relaxed) {\n                            return;\n                        }\n                        let mut guard = samples.lock().unwrap();\n                        for &s in data {\n                            guard.push((s as f32 / 32768.0) - 1.0);\n                        }\n                    },\n                    err_fn,\n                    None,\n                )\n            }\n            _ => {\n                let error_msg = format!(\"Unsupported sample format: {:?}\", sample_format);\n                eprintln!(\"{}\", error_msg);\n                *error_arc_clone.lock().unwrap() = Some(error_msg);\n                return;\n            }\n        };\n\n        let stream = match stream {\n            Ok(s) => s,\n            Err(e) => {\n                let error_msg = format!(\"Failed to build input stream: {}\", e);\n                eprintln!(\"{}\", error_msg);\n                *error_arc_clone.lock().unwrap() = Some(error_msg);\n                return;\n            }\n        };\n\n        if let Err(e) = stream.play() {\n            let error_msg = format!(\"Failed to start stream: {}\", e);\n            eprintln!(\"{}\", error_msg);\n            *error_arc_clone.lock().unwrap() = Some(error_msg);\n            return;\n        }\n\n        eprintln!(\"Linux audio capture: Stream started successfully\");\n\n        // Keep thread alive until stop signal\n        loop {\n            if stop_flag.load(Ordering::Relaxed) {\n                break;\n            }\n            std::thread::sleep(std::time::Duration::from_millis(100));\n        }\n\n        // Stream will be dropped here, stopping capture\n        eprintln!(\"Linux audio capture: Stream stopped\");\n    });\n\n    // Spawn timeout task\n    let stop_tx_clone = state.stop_tx.clone();\n    tokio::spawn(async move {\n        tokio::time::sleep(tokio::time::Duration::from_secs(max_duration_secs as u64)).await;\n        let tx = stop_tx_clone.lock().unwrap().take();\n        if let Some(tx) = tx {\n            let _ = tx.send(()).await;\n        }\n    });\n\n    Ok(())\n}\n\npub async fn stop_capture(state: &AudioCaptureState) -> Result<String, String> {\n    // Signal stop\n    if let Some(tx) = state.stop_tx.lock().unwrap().take() {\n        let _ = tx.send(());\n    }\n\n    // Wait a bit for capture to stop\n    tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n\n    // Check if there was an error during capture\n    if let Some(error) = state.error.lock().unwrap().as_ref() {\n        return Err(error.clone());\n    }\n\n    // Get samples\n    let samples = state.samples.lock().unwrap().clone();\n    let sample_rate = *state.sample_rate.lock().unwrap();\n    let channels = *state.channels.lock().unwrap();\n\n    if samples.is_empty() {\n        return Err(\n            \"No audio samples captured. Make sure audio is playing on your system during recording.\"\n                .to_string(),\n        );\n    }\n\n    // Convert to WAV\n    let wav_data = samples_to_wav(&samples, sample_rate, channels)?;\n\n    // Encode to base64\n    let base64_data = general_purpose::STANDARD.encode(&wav_data);\n\n    Ok(base64_data)\n}\n\npub fn is_supported() -> bool {\n    // Check if we can find a monitor device for system audio capture\n    let host = cpal::default_host();\n    if let Ok(devices) = host.input_devices() {\n        for d in devices {\n            if let Ok(name) = d.name() {\n                if name.to_lowercase().contains(\"monitor\") {\n                    return true;\n                }\n            }\n        }\n    }\n    // Even without a monitor, basic input capture is available\n    host.default_input_device().is_some()\n}\n\nfn samples_to_wav(samples: &[f32], sample_rate: u32, channels: u16) -> Result<Vec<u8>, String> {\n    let mut buffer = Vec::new();\n    let cursor = Cursor::new(&mut buffer);\n\n    let spec = WavSpec {\n        channels,\n        sample_rate,\n        bits_per_sample: 16,\n        sample_format: hound::SampleFormat::Int,\n    };\n\n    let mut writer =\n        WavWriter::new(cursor, spec).map_err(|e| format!(\"Failed to create WAV writer: {}\", e))?;\n\n    // Convert f32 samples to i16\n    for sample in samples {\n        let clamped = sample.clamp(-1.0, 1.0);\n        let i16_sample = (clamped * 32767.0) as i16;\n        writer\n            .write_sample(i16_sample)\n            .map_err(|e| format!(\"Failed to write sample: {}\", e))?;\n    }\n\n    writer\n        .finalize()\n        .map_err(|e| format!(\"Failed to finalize WAV: {}\", e))?;\n\n    Ok(buffer)\n}\n"
  },
  {
    "path": "tauri/src-tauri/src/audio_capture/macos.rs",
    "content": "use crate::audio_capture::AudioCaptureState;\nuse base64::{engine::general_purpose, Engine as _};\nuse hound::{WavSpec, WavWriter};\nuse screencapturekit::{\n    cm::CMSampleBuffer,\n    shareable_content::SCShareableContent,\n    stream::{\n        configuration::SCStreamConfiguration,\n        content_filter::SCContentFilter,\n        output_trait::SCStreamOutputTrait,\n        output_type::SCStreamOutputType,\n        sc_stream::SCStream,\n    },\n};\nuse std::io::Cursor;\nuse std::sync::{Arc, Mutex};\nuse tokio::sync::mpsc;\n\npub async fn start_capture(\n    state: &AudioCaptureState,\n    max_duration_secs: u32,\n) -> Result<(), String> {\n    // Reset previous samples\n    state.reset();\n\n    // Get shareable content\n    let content = SCShareableContent::get()\n        .map_err(|e| format!(\"Failed to get shareable content: {}\", e))?;\n\n    // Get first display\n    let displays = content.displays();\n    if displays.is_empty() {\n        return Err(\"No displays available\".to_string());\n    }\n    let display = &displays[0];\n\n    // Create content filter for desktop audio\n    let filter = SCContentFilter::create()\n        .with_display(display)\n        .with_excluding_windows(&[])\n        .build();\n\n    // Create stream configuration - audio only\n    let mut config = SCStreamConfiguration::default();\n    config.set_captures_audio(true);\n    config.set_excludes_current_process_audio(false);\n    config.set_sample_rate(48000); // Use i32 directly\n    config.set_channel_count(2); // Use i32 directly\n\n    // Create stream using builder\n    let (tx, mut rx) = mpsc::channel::<()>(1);\n    *state.stop_tx.lock().unwrap() = Some(tx);\n\n    let samples = state.samples.clone();\n    let sample_rate = state.sample_rate.clone();\n    let channels = state.channels.clone();\n\n    // Set sample rate and channels\n    *sample_rate.lock().unwrap() = 48000;\n    *channels.lock().unwrap() = 2;\n\n    // Create output handler struct\n    struct AudioHandler {\n        samples: Arc<Mutex<Vec<f32>>>,\n    }\n\n    impl SCStreamOutputTrait for AudioHandler {\n        fn did_output_sample_buffer(\n            &self,\n            sample: CMSampleBuffer,\n            _type: SCStreamOutputType,\n        ) {\n            if _type == SCStreamOutputType::Audio {\n                if let Ok(audio_samples) = extract_audio_samples(sample) {\n                    let mut samples_guard = self.samples.lock().unwrap();\n                    samples_guard.extend_from_slice(&audio_samples);\n                }\n            }\n        }\n    }\n\n    let handler = AudioHandler {\n        samples: samples.clone(),\n    };\n\n    // Create stream\n    let mut stream = SCStream::new(&filter, &config);\n    \n    // Add output handler for audio (order: handler, then output_type)\n    stream.add_output_handler(handler, SCStreamOutputType::Audio);\n\n    // Store stream reference\n    *state.stream.lock().unwrap() = Some(stream.clone());\n\n    stream.start_capture().map_err(|e| format!(\"Failed to start capture: {}\", e))?;\n\n    // Spawn task to stop after max duration\n    let stream_clone = stream.clone();\n    tokio::spawn(async move {\n        tokio::select! {\n            _ = tokio::time::sleep(tokio::time::Duration::from_secs(max_duration_secs as u64)) => {\n                // Timeout reached\n            }\n            _ = rx.recv() => {\n                // Manual stop\n            }\n        }\n        let _ = stream_clone.stop_capture();\n    });\n\n    Ok(())\n}\n\npub async fn stop_capture(state: &AudioCaptureState) -> Result<String, String> {\n    // Signal stop\n    if let Some(tx) = state.stop_tx.lock().unwrap().take() {\n        let _ = tx.send(());\n    }\n\n    // Stop stream if still active\n    if let Some(stream) = state.stream.lock().unwrap().take() {\n        let _ = stream.stop_capture();\n    }\n\n    // Wait a bit for capture to stop\n    tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n\n    // Get samples\n    let samples = state.samples.lock().unwrap().clone();\n    let sample_rate = *state.sample_rate.lock().unwrap();\n    let channels = *state.channels.lock().unwrap();\n\n    if samples.is_empty() {\n        return Err(\"No audio samples captured\".to_string());\n    }\n\n    // Convert to WAV\n    let wav_data = samples_to_wav(&samples, sample_rate, channels)?;\n    \n    // Encode to base64\n    let base64_data = general_purpose::STANDARD.encode(&wav_data);\n    \n    Ok(base64_data)\n}\n\npub fn is_supported() -> bool {\n    // ScreenCaptureKit requires macOS 12.3+\n    // Check if we're on a supported version\n    #[cfg(target_os = \"macos\")]\n    {\n        // Basic check - ScreenCaptureKit should be available on macOS 12.3+\n        true\n    }\n    #[cfg(not(target_os = \"macos\"))]\n    {\n        false\n    }\n}\n\nfn extract_audio_samples(sample_buffer: CMSampleBuffer) -> Result<Vec<f32>, String> {\n    // Use the crate's built-in method to get audio buffer list\n    let audio_buffer_list = sample_buffer\n        .audio_buffer_list()\n        .ok_or_else(|| \"Failed to get audio buffer list\".to_string())?;\n\n    let buffers: Vec<_> = audio_buffer_list.iter().collect();\n    let num_buffers = buffers.len();\n    \n    if num_buffers == 0 {\n        return Ok(Vec::new());\n    }\n\n    // ScreenCaptureKit on macOS provides audio in Float32 format\n    // The audio can be either:\n    // - Interleaved (1 buffer with L,R,L,R,... samples)\n    // - Planar (2 buffers, one for L channel, one for R channel)\n    \n    if num_buffers == 1 {\n        // Interleaved stereo or mono in a single buffer\n        let buffer = &buffers[0];\n        let data_bytes = buffer.data();\n        let num_samples = data_bytes.len() / std::mem::size_of::<f32>();\n        \n        if num_samples > 0 {\n            unsafe {\n                let data_ptr = data_bytes.as_ptr() as *const f32;\n                let data = std::slice::from_raw_parts(data_ptr, num_samples);\n                return Ok(data.to_vec());\n            }\n        }\n    } else {\n        // Planar format - separate buffer for each channel\n        // We need to interleave them: L0, R0, L1, R1, ...\n        let mut channel_data: Vec<Vec<f32>> = Vec::new();\n        let mut max_samples = 0;\n        \n        for buffer in &buffers {\n            let data_bytes = buffer.data();\n            let num_samples = data_bytes.len() / std::mem::size_of::<f32>();\n            \n            if num_samples > 0 {\n                unsafe {\n                    let data_ptr = data_bytes.as_ptr() as *const f32;\n                    let data = std::slice::from_raw_parts(data_ptr, num_samples);\n                    channel_data.push(data.to_vec());\n                    max_samples = max_samples.max(num_samples);\n                }\n            }\n        }\n        \n        // Interleave the channels\n        let mut interleaved = Vec::with_capacity(max_samples * num_buffers);\n        for i in 0..max_samples {\n            for channel in &channel_data {\n                if i < channel.len() {\n                    interleaved.push(channel[i]);\n                } else {\n                    interleaved.push(0.0); // Pad with silence if needed\n                }\n            }\n        }\n        \n        return Ok(interleaved);\n    }\n\n    Ok(Vec::new())\n}\n\nfn samples_to_wav(samples: &[f32], sample_rate: u32, channels: u16) -> Result<Vec<u8>, String> {\n    let mut buffer = Vec::new();\n    let cursor = Cursor::new(&mut buffer);\n    \n    let spec = WavSpec {\n        channels,\n        sample_rate,\n        bits_per_sample: 16,\n        sample_format: hound::SampleFormat::Int,\n    };\n\n    let mut writer = WavWriter::new(cursor, spec)\n        .map_err(|e| format!(\"Failed to create WAV writer: {}\", e))?;\n\n    // Convert f32 samples to i16\n    for sample in samples {\n        let clamped = sample.clamp(-1.0, 1.0);\n        let i16_sample = (clamped * 32767.0) as i16;\n        writer.write_sample(i16_sample)\n            .map_err(|e| format!(\"Failed to write sample: {}\", e))?;\n    }\n\n    writer.finalize()\n        .map_err(|e| format!(\"Failed to finalize WAV: {}\", e))?;\n\n    Ok(buffer)\n}\n"
  },
  {
    "path": "tauri/src-tauri/src/audio_capture/mod.rs",
    "content": "#[cfg(target_os = \"macos\")]\nmod macos;\n#[cfg(target_os = \"windows\")]\nmod windows;\n#[cfg(target_os = \"linux\")]\nmod linux;\n\n#[cfg(target_os = \"macos\")]\npub use macos::*;\n#[cfg(target_os = \"windows\")]\npub use windows::*;\n#[cfg(target_os = \"linux\")]\npub use linux::*;\n\nuse std::sync::{Arc, Mutex};\n\n#[cfg(target_os = \"macos\")]\nuse screencapturekit::stream::sc_stream::SCStream;\n\npub struct AudioCaptureState {\n    pub samples: Arc<Mutex<Vec<f32>>>,\n    pub sample_rate: Arc<Mutex<u32>>,\n    pub channels: Arc<Mutex<u16>>,\n    pub stop_tx: Arc<Mutex<Option<tokio::sync::mpsc::Sender<()>>>>,\n    pub error: Arc<Mutex<Option<String>>>,\n    #[cfg(target_os = \"macos\")]\n    pub stream: Arc<Mutex<Option<SCStream>>>,\n}\n\nimpl AudioCaptureState {\n    pub fn new() -> Self {\n        Self {\n            samples: Arc::new(Mutex::new(Vec::new())),\n            sample_rate: Arc::new(Mutex::new(44100)),\n            channels: Arc::new(Mutex::new(2)),\n            stop_tx: Arc::new(Mutex::new(None)),\n            error: Arc::new(Mutex::new(None)),\n            #[cfg(target_os = \"macos\")]\n            stream: Arc::new(Mutex::new(None)),\n        }\n    }\n\n    pub fn reset(&self) {\n        *self.samples.lock().unwrap() = Vec::new();\n        *self.error.lock().unwrap() = None;\n    }\n}\n"
  },
  {
    "path": "tauri/src-tauri/src/audio_capture/windows.rs",
    "content": "use crate::audio_capture::AudioCaptureState;\nuse base64::{engine::general_purpose, Engine as _};\nuse hound::{WavSpec, WavWriter};\nuse std::io::Cursor;\nuse std::sync::Arc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::thread;\nuse wasapi::*;\nuse windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_MULTITHREADED};\n\npub async fn start_capture(\n    state: &AudioCaptureState,\n    max_duration_secs: u32,\n) -> Result<(), String> {\n    // Reset previous samples\n    state.reset();\n\n    let samples = state.samples.clone();\n    let sample_rate_arc = state.sample_rate.clone();\n    let channels_arc = state.channels.clone();\n    let stop_tx = state.stop_tx.clone();\n    let error_arc = state.error.clone();\n\n    // Use AtomicBool for stop signal (works with non-Send types)\n    let stop_flag = Arc::new(AtomicBool::new(false));\n    let stop_flag_clone = stop_flag.clone();\n\n    // Create tokio channel and spawn a task to bridge it to the AtomicBool\n    let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);\n    *stop_tx.lock().unwrap() = Some(tx);\n\n    tokio::spawn(async move {\n        rx.recv().await;\n        stop_flag_clone.store(true, Ordering::Relaxed);\n    });\n\n    // Spawn capture task on a dedicated thread (WASAPI COM objects are not Send)\n    // All WASAPI objects must be created and used on the same thread\n    thread::spawn(move || {\n        // Initialize COM for this thread\n        unsafe {\n            let hr = CoInitializeEx(None, COINIT_MULTITHREADED);\n            if hr.is_err() {\n                eprintln!(\"Failed to initialize COM: {:?}\", hr);\n                return;\n            }\n        }\n\n        // Ensure COM is uninitialized when thread exits\n        let _com_guard = scopeguard::guard((), |_| unsafe {\n            CoUninitialize();\n        });\n\n        // Initialize WASAPI on this thread\n        let device = match DeviceEnumerator::new()\n            .and_then(|enumerator| enumerator.get_default_device(&Direction::Render))\n        {\n            Ok(d) => d,\n            Err(e) => {\n                let error_msg = format!(\"Failed to get audio device: {}\", e);\n                eprintln!(\"{}\", error_msg);\n                *error_arc.lock().unwrap() = Some(error_msg);\n                return;\n            }\n        };\n\n        let mut audio_client = match device.get_iaudioclient() {\n            Ok(client) => client,\n            Err(e) => {\n                let error_msg = format!(\"Failed to get audio client: {}\", e);\n                eprintln!(\"{}\", error_msg);\n                *error_arc.lock().unwrap() = Some(error_msg);\n                return;\n            }\n        };\n\n        let mix_format = match audio_client.get_mixformat() {\n            Ok(format) => format,\n            Err(e) => {\n                let error_msg = format!(\"Failed to get mix format: {}\", e);\n                eprintln!(\"{}\", error_msg);\n                *error_arc.lock().unwrap() = Some(error_msg);\n                return;\n            }\n        };\n\n        // Set sample rate and channels\n        let channels = mix_format.get_nchannels() as usize;\n        let bytes_per_sample = (mix_format.get_bitspersample() / 8) as usize;\n        *sample_rate_arc.lock().unwrap() = mix_format.get_samplespersec();\n        *channels_arc.lock().unwrap() = mix_format.get_nchannels();\n\n        // Get device period\n        let (_def_period, min_period) = match audio_client.get_device_period() {\n            Ok(periods) => periods,\n            Err(e) => {\n                eprintln!(\"Failed to get device period: {}\", e);\n                return;\n            }\n        };\n\n        // Initialize audio client for loopback with StreamMode\n        // For loopback mode: get Render device, initialize with Capture direction\n        // This triggers AUDCLNT_STREAMFLAGS_LOOPBACK in the wasapi crate\n        let stream_mode = StreamMode::EventsShared {\n            autoconvert: true,  // Enable automatic format conversion\n            buffer_duration_hns: min_period, // Use minimum period\n        };\n\n        if let Err(e) = audio_client.initialize_client(&mix_format, &Direction::Capture, &stream_mode) {\n            let error_msg = format!(\"Failed to initialize audio client: {}\", e);\n            eprintln!(\"{}\", error_msg);\n            *error_arc.lock().unwrap() = Some(error_msg);\n            return;\n        }\n\n        // Set up event handle for EventsShared mode\n        let h_event = match audio_client.set_get_eventhandle() {\n            Ok(event) => event,\n            Err(e) => {\n                eprintln!(\"Failed to set event handle: {}\", e);\n                return;\n            }\n        };\n\n        let capture_client = match audio_client.get_audiocaptureclient() {\n            Ok(client) => client,\n            Err(e) => {\n                let error_msg = format!(\"Failed to get capture client: {}\", e);\n                eprintln!(\"{}\", error_msg);\n                *error_arc.lock().unwrap() = Some(error_msg);\n                return;\n            }\n        };\n\n        if let Err(e) = audio_client.start_stream() {\n            let error_msg = format!(\"Failed to start stream: {}\", e);\n            eprintln!(\"{}\", error_msg);\n            *error_arc.lock().unwrap() = Some(error_msg);\n            return;\n        }\n\n        loop {\n            // Check if stop signal was received\n            if stop_flag.load(Ordering::Relaxed) {\n                break;\n            }\n\n            // Try to get available data\n            match capture_client.get_next_packet_size() {\n                Ok(Some(frames_available)) => {\n                    if frames_available > 0 {\n                        // Calculate buffer size needed (frames * channels * bytes_per_sample)\n                        let buffer_size = frames_available as usize * channels * bytes_per_sample;\n\n                        let mut buffer = vec![0u8; buffer_size];\n                        match capture_client.read_from_device(&mut buffer) {\n                            Ok((frames_read, _buffer_info)) => {\n                                if frames_read > 0 {\n                                    // Convert bytes to f32 samples\n                                    let samples_read = (frames_read as usize * channels) as usize;\n                                    let mut samples_guard = samples.lock().unwrap();\n\n                                    // Assuming 32-bit float format\n                                    if bytes_per_sample == 4 {\n                                        for i in 0..samples_read {\n                                            let byte_offset = i * 4;\n                                            if byte_offset + 4 <= buffer.len() {\n                                                let sample = f32::from_le_bytes([\n                                                    buffer[byte_offset],\n                                                    buffer[byte_offset + 1],\n                                                    buffer[byte_offset + 2],\n                                                    buffer[byte_offset + 3],\n                                                ]);\n                                                samples_guard.push(sample);\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                            Err(e) => {\n                                eprintln!(\"Error reading from device: {}\", e);\n                            }\n                        }\n                    }\n                }\n                Ok(None) => {\n                    // Exclusive mode - handle differently if needed\n                }\n                Err(e) => {\n                    eprintln!(\"Error getting next packet size: {}\", e);\n                }\n            }\n\n            // Wait for event signal (with timeout to allow checking stop flag)\n            if h_event.wait_for_event(100).is_err() {\n                // Timeout is expected - just continue to check stop flag\n            }\n        }\n\n        // Stop the stream when done\n        audio_client.stop_stream().ok();\n    });\n\n    // Spawn timeout task\n    let stop_tx_clone = state.stop_tx.clone();\n    tokio::spawn(async move {\n        tokio::time::sleep(tokio::time::Duration::from_secs(max_duration_secs as u64)).await;\n        // Take the sender out of the mutex before awaiting\n        let tx = stop_tx_clone.lock().unwrap().take();\n        if let Some(tx) = tx {\n            let _ = tx.send(()).await;\n        }\n    });\n\n    Ok(())\n}\n\npub async fn stop_capture(state: &AudioCaptureState) -> Result<String, String> {\n    // Signal stop\n    if let Some(tx) = state.stop_tx.lock().unwrap().take() {\n        let _ = tx.send(());\n    }\n\n    // Wait a bit for capture to stop\n    tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;\n\n    // Check if there was an error during capture\n    if let Some(error) = state.error.lock().unwrap().as_ref() {\n        return Err(error.clone());\n    }\n\n    // Get samples\n    let samples = state.samples.lock().unwrap().clone();\n    let sample_rate = *state.sample_rate.lock().unwrap();\n    let channels = *state.channels.lock().unwrap();\n\n    if samples.is_empty() {\n        return Err(\"No audio samples captured. Make sure audio is playing on your system during recording.\".to_string());\n    }\n\n    // Convert to WAV\n    let wav_data = samples_to_wav(&samples, sample_rate, channels)?;\n    \n    // Encode to base64\n    let base64_data = general_purpose::STANDARD.encode(&wav_data);\n    \n    Ok(base64_data)\n}\n\npub fn is_supported() -> bool {\n    #[cfg(target_os = \"windows\")]\n    {\n        true\n    }\n    #[cfg(not(target_os = \"windows\"))]\n    {\n        false\n    }\n}\n\nfn samples_to_wav(samples: &[f32], sample_rate: u32, channels: u16) -> Result<Vec<u8>, String> {\n    let mut buffer = Vec::new();\n    let cursor = Cursor::new(&mut buffer);\n    \n    let spec = WavSpec {\n        channels,\n        sample_rate,\n        bits_per_sample: 16,\n        sample_format: hound::SampleFormat::Int,\n    };\n\n    let mut writer = WavWriter::new(cursor, spec)\n        .map_err(|e| format!(\"Failed to create WAV writer: {}\", e))?;\n\n    // Convert f32 samples to i16\n    for sample in samples {\n        let clamped = sample.clamp(-1.0, 1.0);\n        let i16_sample = (clamped * 32767.0) as i16;\n        writer.write_sample(i16_sample)\n            .map_err(|e| format!(\"Failed to write sample: {}\", e))?;\n    }\n\n    writer.finalize()\n        .map_err(|e| format!(\"Failed to finalize WAV: {}\", e))?;\n\n    Ok(buffer)\n}\n"
  },
  {
    "path": "tauri/src-tauri/src/audio_output.rs",
    "content": "use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};\nuse cpal::{Device, Host, SampleFormat, StreamConfig};\nuse std::sync::{Arc, Mutex};\nuse std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};\n\n#[derive(Debug, Clone, serde::Serialize)]\npub struct AudioOutputDevice {\n    pub id: String,\n    pub name: String,\n    pub is_default: bool,\n}\n\npub struct AudioOutputState {\n    host: Host,\n    stop_flag: Arc<AtomicBool>,\n}\n\nimpl AudioOutputState {\n    pub fn new() -> Self {\n        Self {\n            host: cpal::default_host(),\n            stop_flag: Arc::new(AtomicBool::new(false)),\n        }\n    }\n\n    pub fn stop_all_playback(&self) -> Result<(), String> {\n        eprintln!(\"stop_all_playback: Setting stop flag\");\n        self.stop_flag.store(true, Ordering::Relaxed);\n        eprintln!(\"stop_all_playback: Stop flag set - active streams will output silence\");\n        Ok(())\n    }\n\n    pub fn list_output_devices(&self) -> Result<Vec<AudioOutputDevice>, String> {\n        let devices = self\n            .host\n            .output_devices()\n            .map_err(|e| format!(\"Failed to enumerate output devices: {}\", e))?;\n\n        let default_device = self.host.default_output_device();\n\n        let mut result = Vec::new();\n        for device in devices {\n            let name = device\n                .name()\n                .map_err(|e| format!(\"Failed to get device name: {}\", e))?;\n\n            // Generate a stable ID from the device name (cpal doesn't provide stable IDs)\n            let id = format!(\"device_{}\", name.replace(' ', \"_\").to_lowercase());\n\n            let is_default = default_device\n                .as_ref()\n                .map(|d| d.name().unwrap_or_default() == name)\n                .unwrap_or(false);\n\n            result.push(AudioOutputDevice {\n                id,\n                name,\n                is_default,\n            });\n        }\n\n        Ok(result)\n    }\n\n    pub async fn play_audio_to_devices(\n        &self,\n        audio_data: Vec<u8>,\n        device_ids: Vec<String>,\n    ) -> Result<(), String> {\n        eprintln!(\"play_audio_to_devices called with {} bytes, {} device IDs\", audio_data.len(), device_ids.len());\n        eprintln!(\"Requested device IDs: {:?}\", device_ids);\n        \n        // Decode audio file (assuming WAV format)\n        eprintln!(\"Decoding audio data...\");\n        let (samples, sample_rate, channels) = self.decode_wav(&audio_data)?;\n        eprintln!(\"Audio decoded: {} samples, {}Hz, {} channels\", samples.len(), sample_rate, channels);\n\n        // Find devices by ID\n        eprintln!(\"Enumerating output devices...\");\n        let devices: Vec<Device> = self\n            .host\n            .output_devices()\n            .map_err(|e| format!(\"Failed to enumerate devices: {}\", e))?\n            .filter_map(|device| {\n                let name = device.name().ok()?;\n                let id = format!(\"device_{}\", name.replace(' ', \"_\").to_lowercase());\n                eprintln!(\"Found device: {} (id: {})\", name, id);\n                if device_ids.contains(&id) {\n                    eprintln!(\"  -> Matched! Will play to this device\");\n                    Some(device)\n                } else {\n                    None\n                }\n            })\n            .collect();\n\n        if devices.is_empty() {\n            eprintln!(\"ERROR: No matching devices found\");\n            return Err(\"No matching devices found\".to_string());\n        }\n\n        eprintln!(\"Playing to {} device(s)\", devices.len());\n        \n        // Stop any existing playback first\n        self.stop_all_playback().ok();\n        \n        // Reset stop flag for new playback\n        self.stop_flag.store(false, Ordering::Relaxed);\n        \n        // Play to each device\n        for (i, device) in devices.iter().enumerate() {\n            let device_name = device.name().unwrap_or_else(|_| \"unknown\".to_string());\n            eprintln!(\"Playing to device {}/{}: {}\", i + 1, devices.len(), device_name);\n            self.play_to_device(device, samples.clone(), sample_rate, channels, self.stop_flag.clone())\n                .map_err(|e| format!(\"Failed to play to device {}: {}\", device_name, e))?;\n            eprintln!(\"Successfully started playback on device: {}\", device_name);\n        }\n\n        eprintln!(\"play_audio_to_devices completed successfully\");\n        Ok(())\n    }\n\n    fn decode_wav(&self, data: &[u8]) -> Result<(Vec<f32>, u32, u16), String> {\n        use symphonia::core::formats::FormatOptions;\n        use symphonia::core::io::MediaSourceStream;\n        use symphonia::core::meta::MetadataOptions;\n\n        eprintln!(\"decode_wav: Creating MediaSourceStream from {} bytes\", data.len());\n        let mss = MediaSourceStream::new(\n            Box::new(std::io::Cursor::new(data.to_vec())),\n            Default::default(),\n        );\n\n        eprintln!(\"decode_wav: Probing audio format...\");\n        let mut format = symphonia::default::get_probe()\n            .format(\n                &Default::default(),\n                mss,\n                &FormatOptions::default(),\n                &MetadataOptions::default(),\n            )\n            .map_err(|e| {\n                eprintln!(\"decode_wav: Failed to probe audio: {}\", e);\n                format!(\"Failed to probe audio: {}\", e)\n            })?\n            .format;\n        \n        eprintln!(\"decode_wav: Audio format probed successfully\");\n\n        eprintln!(\"decode_wav: Finding audio track...\");\n        let track = format\n            .tracks()\n            .iter()\n            .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)\n            .ok_or_else(|| {\n                eprintln!(\"decode_wav: No audio track found\");\n                \"No audio track found\".to_string()\n            })?;\n\n        let sample_rate = track\n            .codec_params\n            .sample_rate\n            .ok_or_else(|| {\n                eprintln!(\"decode_wav: No sample rate found in track\");\n                \"No sample rate found\".to_string()\n            })?;\n\n        let channels = track\n            .codec_params\n            .channels\n            .ok_or_else(|| {\n                eprintln!(\"decode_wav: No channels found in track\");\n                \"No channels found\".to_string()\n            })?\n            .count() as u16;\n\n        eprintln!(\"decode_wav: Track info - sample_rate: {}, channels: {}\", sample_rate, channels);\n\n        eprintln!(\"decode_wav: Creating decoder...\");\n        let mut decoder = symphonia::default::get_codecs()\n            .make(&track.codec_params, &Default::default())\n            .map_err(|e| {\n                eprintln!(\"decode_wav: Failed to create decoder: {}\", e);\n                format!(\"Failed to create decoder: {}\", e)\n            })?;\n        \n        eprintln!(\"decode_wav: Decoder created successfully\");\n\n        let mut samples = Vec::new();\n        let mut packet_count = 0;\n        eprintln!(\"decode_wav: Starting packet decoding loop...\");\n        loop {\n            let packet = match format.next_packet() {\n                Ok(packet) => packet,\n                Err(e) => {\n                    eprintln!(\"decode_wav: End of stream or error: {:?}\", e);\n                    break;\n                }\n            };\n\n            packet_count += 1;\n            let decoded = decoder\n                .decode(&packet)\n                .map_err(|e| {\n                    eprintln!(\"decode_wav: Decode error on packet {}: {}\", packet_count, e);\n                    format!(\"Decode error: {}\", e)\n                })?;\n\n            // Convert to f32 samples by matching on the buffer type\n            use symphonia::core::audio::{AudioBufferRef, Signal};\n            use symphonia::core::conv::FromSample;\n\n            let spec = *decoded.spec();\n            let num_channels = spec.channels.count();\n            let num_frames = decoded.frames();\n\n            eprintln!(\"decode_wav: Packet {} - {} frames, {} channels\", packet_count, num_frames, num_channels);\n\n            // Interleave samples from all channels\n            for frame_idx in 0..num_frames {\n                for ch in 0..num_channels {\n                    let sample_f32 = match &decoded {\n                        AudioBufferRef::U8(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),\n                        AudioBufferRef::U16(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),\n                        AudioBufferRef::U24(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),\n                        AudioBufferRef::U32(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),\n                        AudioBufferRef::S8(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),\n                        AudioBufferRef::S16(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),\n                        AudioBufferRef::S24(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),\n                        AudioBufferRef::S32(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),\n                        AudioBufferRef::F32(buf) => buf.chan(ch)[frame_idx],\n                        AudioBufferRef::F64(buf) => buf.chan(ch)[frame_idx] as f32,\n                    };\n                    samples.push(sample_f32);\n                }\n            }\n        }\n\n        eprintln!(\"decode_wav: Decoded {} packets, total {} samples\", packet_count, samples.len());\n        eprintln!(\"decode_wav: Returning sample_rate={}, channels={}\", sample_rate, channels);\n        Ok((samples, sample_rate, channels))\n    }\n\n    fn play_to_device(\n        &self,\n        device: &Device,\n        samples: Vec<f32>,\n        sample_rate: u32,\n        channels: u16,\n        stop_flag: Arc<AtomicBool>,\n    ) -> Result<(), String> {\n        let device_name = device.name().unwrap_or_else(|_| \"unknown\".to_string());\n        eprintln!(\"play_to_device: Starting playback to device: {}\", device_name);\n        eprintln!(\"play_to_device: Input - {} samples, {}Hz, {} channels\", samples.len(), sample_rate, channels);\n        \n        let config = device\n            .default_output_config()\n            .map_err(|e| format!(\"Failed to get default config: {}\", e))?;\n\n        // Prepare samples for the device's format\n        let device_sample_rate = config.sample_rate().0;\n        let device_channels = config.channels();\n        let device_sample_format = config.sample_format();\n        \n        eprintln!(\"play_to_device: Device config - {}Hz, {} channels, format: {:?}\", \n                  device_sample_rate, device_channels, device_sample_format);\n\n        // Resample if needed (simple linear interpolation for now)\n        let resampled = if device_sample_rate != sample_rate {\n            eprintln!(\"play_to_device: Resampling from {}Hz to {}Hz\", sample_rate, device_sample_rate);\n            let result = self.resample(&samples, sample_rate, device_sample_rate);\n            eprintln!(\"play_to_device: Resampled {} samples to {} samples\", samples.len(), result.len());\n            result\n        } else {\n            eprintln!(\"play_to_device: No resampling needed\");\n            samples\n        };\n\n        // Interleave/convert channels if needed\n        eprintln!(\"play_to_device: Interleaving channels from {} to {} channels\", channels, device_channels);\n        let interleaved = self.interleave_channels(&resampled, channels, device_channels);\n        eprintln!(\"play_to_device: Interleaved to {} samples\", interleaved.len());\n\n        // Create shared buffer for playback\n        let buffer: Arc<Mutex<Vec<f32>>> = Arc::new(Mutex::new(interleaved));\n        let position = Arc::new(AtomicUsize::new(0));\n        let buffer_clone = buffer.clone();\n        let position_clone = position.clone();\n\n        let err_fn = |err| eprintln!(\"Playback error: {}\", err);\n\n        let stream_config = StreamConfig {\n            channels: device_channels,\n            sample_rate: cpal::SampleRate(device_sample_rate),\n            buffer_size: cpal::BufferSize::Default,\n        };\n\n        let stop_flag_clone = stop_flag.clone();\n        let stream = match config.sample_format() {\n            SampleFormat::F32 => {\n                let buffer = buffer_clone.clone();\n                let pos = position_clone.clone();\n                device\n                    .build_output_stream(\n                        &stream_config,\n                        move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {\n                            // Check stop flag - if set, output silence\n                            if stop_flag_clone.load(Ordering::Relaxed) {\n                                for sample in data.iter_mut() {\n                                    *sample = 0.0;\n                                }\n                                return;\n                            }\n                            \n                            let mut idx = pos.load(Ordering::Relaxed);\n                            let buf = buffer.lock().unwrap();\n                            for sample in data.iter_mut() {\n                                if idx < buf.len() {\n                                    *sample = buf[idx];\n                                    idx += 1;\n                                } else {\n                                    *sample = 0.0;\n                                }\n                            }\n                            pos.store(idx, Ordering::Relaxed);\n                        },\n                        err_fn,\n                        None,\n                    )\n                    .map_err(|e| format!(\"Failed to build stream: {}\", e))?\n            }\n            SampleFormat::I16 => {\n                let buffer = buffer_clone.clone();\n                let pos = position_clone.clone();\n                device\n                    .build_output_stream(\n                        &stream_config,\n                        move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {\n                            // Check stop flag - if set, output silence\n                            if stop_flag_clone.load(Ordering::Relaxed) {\n                                for sample in data.iter_mut() {\n                                    *sample = 0;\n                                }\n                                return;\n                            }\n                            \n                            let mut idx = pos.load(Ordering::Relaxed);\n                            let buf = buffer.lock().unwrap();\n                            for sample in data.iter_mut() {\n                                if idx < buf.len() {\n                                    *sample = (buf[idx] * 32767.0) as i16;\n                                    idx += 1;\n                                } else {\n                                    *sample = 0;\n                                }\n                            }\n                            pos.store(idx, Ordering::Relaxed);\n                        },\n                        err_fn,\n                        None,\n                    )\n                    .map_err(|e| format!(\"Failed to build stream: {}\", e))?\n            }\n            SampleFormat::U16 => {\n                let buffer = buffer_clone.clone();\n                let pos = position_clone.clone();\n                device\n                    .build_output_stream(\n                        &stream_config,\n                        move |data: &mut [u16], _: &cpal::OutputCallbackInfo| {\n                            // Check stop flag - if set, output silence\n                            if stop_flag_clone.load(Ordering::Relaxed) {\n                                for sample in data.iter_mut() {\n                                    *sample = 32768;\n                                }\n                                return;\n                            }\n                            \n                            let mut idx = pos.load(Ordering::Relaxed);\n                            let buf = buffer.lock().unwrap();\n                            for sample in data.iter_mut() {\n                                if idx < buf.len() {\n                                    *sample = ((buf[idx] + 1.0) * 32767.5) as u16;\n                                    idx += 1;\n                                } else {\n                                    *sample = 32768;\n                                }\n                            }\n                            pos.store(idx, Ordering::Relaxed);\n                        },\n                        err_fn,\n                        None,\n                    )\n                    .map_err(|e| format!(\"Failed to build stream: {}\", e))?\n            }\n            _ => return Err(\"Unsupported sample format\".to_string()),\n        };\n\n        eprintln!(\"play_to_device: Starting stream playback...\");\n        stream.play().map_err(|e| {\n            eprintln!(\"play_to_device: Failed to play stream: {}\", e);\n            format!(\"Failed to play stream: {}\", e)\n        })?;\n        \n        eprintln!(\"play_to_device: Stream started successfully\");\n\n        eprintln!(\"play_to_device: Function completed successfully\");\n        Ok(())\n    }\n\n    fn resample(&self, samples: &[f32], from_rate: u32, to_rate: u32) -> Vec<f32> {\n        if from_rate == to_rate {\n            return samples.to_vec();\n        }\n\n        let ratio = to_rate as f64 / from_rate as f64;\n        let new_len = (samples.len() as f64 * ratio) as usize;\n        let mut resampled = Vec::with_capacity(new_len);\n\n        for i in 0..new_len {\n            let src_idx = (i as f64 / ratio) as usize;\n            if src_idx < samples.len() {\n                resampled.push(samples[src_idx]);\n            } else {\n                resampled.push(0.0);\n            }\n        }\n\n        resampled\n    }\n\n    fn interleave_channels(\n        &self,\n        samples: &[f32],\n        src_channels: u16,\n        dst_channels: u16,\n    ) -> Vec<f32> {\n        if src_channels == dst_channels {\n            return samples.to_vec();\n        }\n\n        let mut interleaved = Vec::new();\n        let samples_per_channel = samples.len() / src_channels as usize;\n\n        for i in 0..samples_per_channel {\n            for ch in 0..dst_channels {\n                let src_ch = if ch < src_channels { ch } else { src_channels - 1 };\n                let idx = (i * src_channels as usize) + src_ch as usize;\n                if idx < samples.len() {\n                    interleaved.push(samples[idx]);\n                } else {\n                    interleaved.push(0.0);\n                }\n            }\n        }\n\n        interleaved\n    }\n}\n\nimpl Default for AudioOutputState {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n"
  },
  {
    "path": "tauri/src-tauri/src/lib.rs",
    "content": "pub mod audio_capture;\n"
  },
  {
    "path": "tauri/src-tauri/src/main.rs",
    "content": "// Prevents additional console window on Windows in release, DO NOT REMOVE!!\n#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n\nmod audio_capture;\nmod audio_output;\n\nuse std::sync::Mutex;\nuse tauri::{command, State, Manager, WindowEvent, Emitter, Listener, RunEvent};\nuse tauri_plugin_shell::ShellExt;\nuse tokio::sync::mpsc;\n\nconst LEGACY_PORT: u16 = 8000;\nconst SERVER_PORT: u16 = 17493;\n\n/// Find a voicebox-server process listening on a given port (Windows only).\n///\n/// Uses PowerShell `Get-NetTCPConnection` to look up the PID owning the port,\n/// then verifies via `tasklist` that it's a voicebox process. The caller is\n/// responsible for checking port occupancy first (e.g. `TcpStream::connect_timeout`).\n/// Replaces the previous `netstat -ano` approach which failed on systems with\n/// corrupted system DLLs (see #277).\n#[cfg(windows)]\nfn find_voicebox_pid_on_port(port: u16) -> Option<u32> {\n    use std::process::Command;\n\n    // Use PowerShell's Get-NetTCPConnection to find the PID listening on the port.\n    // This is a built-in cmdlet that doesn't depend on netstat.exe.\n    let ps_script = format!(\n        \"Get-NetTCPConnection -LocalPort {} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess\",\n        port\n    );\n    if let Ok(output) = Command::new(\"powershell\")\n        .args([\"-NoProfile\", \"-Command\", &ps_script])\n        .output()\n    {\n        let output_str = String::from_utf8_lossy(&output.stdout);\n        for line in output_str.lines() {\n            if let Ok(pid) = line.trim().parse::<u32>() {\n                // Verify this PID is a voicebox process\n                if let Ok(tasklist_output) = Command::new(\"tasklist\")\n                    .args([\"/FI\", &format!(\"PID eq {}\", pid), \"/FO\", \"CSV\", \"/NH\"])\n                    .output()\n                {\n                    let tasklist_str = String::from_utf8_lossy(&tasklist_output.stdout);\n                    if tasklist_str.to_lowercase().contains(\"voicebox\") {\n                        return Some(pid);\n                    }\n                }\n            }\n        }\n    }\n\n    None\n}\n\nstruct ServerState {\n    child: Mutex<Option<tauri_plugin_shell::process::CommandChild>>,\n    server_pid: Mutex<Option<u32>>,\n    keep_running_on_close: Mutex<bool>,\n    models_dir: Mutex<Option<String>>,\n}\n\n#[command]\nasync fn start_server(\n    app: tauri::AppHandle,\n    state: State<'_, ServerState>,\n    remote: Option<bool>,\n    models_dir: Option<String>,\n) -> Result<String, String> {\n    // Store models_dir for use on restart (empty string means reset to default)\n    if let Some(ref dir) = models_dir {\n        if dir.is_empty() {\n            *state.models_dir.lock().unwrap() = None;\n        } else {\n            *state.models_dir.lock().unwrap() = Some(dir.clone());\n        }\n    }\n    // Check if server is already running (managed by this app instance)\n    if state.child.lock().unwrap().is_some() {\n        return Ok(format!(\"http://127.0.0.1:{}\", SERVER_PORT));\n    }\n\n    // Check if a voicebox server is already running on our port (from previous session with keep_running=true)\n    #[cfg(unix)]\n    {\n        use std::process::Command;\n        if let Ok(output) = Command::new(\"lsof\")\n            .args([\"-i\", &format!(\":{}\", SERVER_PORT), \"-sTCP:LISTEN\"])\n            .output()\n        {\n            let output_str = String::from_utf8_lossy(&output.stdout);\n            for line in output_str.lines().skip(1) {\n                let parts: Vec<&str> = line.split_whitespace().collect();\n                if parts.len() >= 2 {\n                    let command = parts[0];\n                    let pid_str = parts[1];\n                    if command.contains(\"voicebox\") {\n                        if let Ok(pid) = pid_str.parse::<u32>() {\n                            println!(\"Found existing voicebox-server on port {} (PID: {}), reusing it\", SERVER_PORT, pid);\n                            // Store the PID so we can kill it on exit if needed\n                            *state.server_pid.lock().unwrap() = Some(pid);\n                            return Ok(format!(\"http://127.0.0.1:{}\", SERVER_PORT));\n                        }\n                    }\n                }\n            }\n        }\n    }\n    \n    #[cfg(windows)]\n    {\n        use std::net::TcpStream;\n        if TcpStream::connect_timeout(\n            &format!(\"127.0.0.1:{}\", SERVER_PORT).parse().unwrap(),\n            std::time::Duration::from_secs(1),\n        ).is_ok() {\n            // Port is in use — check if it's a voicebox process\n            if let Some(pid) = find_voicebox_pid_on_port(SERVER_PORT) {\n                println!(\"Found existing voicebox-server on port {} (PID: {}), reusing it\", SERVER_PORT, pid);\n                *state.server_pid.lock().unwrap() = Some(pid);\n                return Ok(format!(\"http://127.0.0.1:{}\", SERVER_PORT));\n            } else {\n                return Err(format!(\n                    \"Port {} is already in use by another application. \\\n                     Close the other application or change the Voicebox port.\",\n                    SERVER_PORT\n                ));\n            }\n        }\n    }\n\n    // Kill any orphaned voicebox-server from previous session on legacy port 8000\n    // This handles upgrades from older versions that used a fixed port\n    #[cfg(unix)]\n    {\n        use std::process::Command;\n        if let Ok(output) = Command::new(\"lsof\")\n            .args([\"-i\", &format!(\":{}\", LEGACY_PORT), \"-sTCP:LISTEN\"])\n            .output()\n        {\n            let output_str = String::from_utf8_lossy(&output.stdout);\n            for line in output_str.lines().skip(1) {\n                let parts: Vec<&str> = line.split_whitespace().collect();\n                if parts.len() >= 2 {\n                    let command = parts[0];\n                    let pid_str = parts[1];\n                    \n                    if command.contains(\"voicebox\") {\n                        if let Ok(pid) = pid_str.parse::<i32>() {\n                            println!(\"Found orphaned voicebox-server on legacy port {} (PID: {}, CMD: {}), killing it...\", LEGACY_PORT, pid, command);\n                            let _ = Command::new(\"kill\")\n                                .args([\"-9\", \"--\", &format!(\"-{}\", pid)])\n                                .output();\n                            let _ = Command::new(\"kill\")\n                                .args([\"-9\", &pid.to_string()])\n                                .output();\n                        }\n                    } else {\n                        println!(\"Legacy port {} is in use by non-voicebox process: {} (PID: {}), not killing\", LEGACY_PORT, command, pid_str);\n                    }\n                }\n            }\n        }\n    }\n    \n    #[cfg(windows)]\n    {\n        use std::net::TcpStream;\n        if TcpStream::connect_timeout(\n            &format!(\"127.0.0.1:{}\", LEGACY_PORT).parse().unwrap(),\n            std::time::Duration::from_secs(1),\n        ).is_ok() {\n            if let Some(pid) = find_voicebox_pid_on_port(LEGACY_PORT) {\n                println!(\"Found orphaned voicebox-server on legacy port {} (PID: {}), killing it...\", LEGACY_PORT, pid);\n                let _ = std::process::Command::new(\"taskkill\")\n                    .args([\"/PID\", &pid.to_string(), \"/T\", \"/F\"])\n                    .output();\n            }\n        }\n    }\n    \n    // Brief wait for port to be released\n    std::thread::sleep(std::time::Duration::from_millis(200));\n\n    // Get app data directory\n    let data_dir = app\n        .path()\n        .app_data_dir()\n        .map_err(|e| format!(\"Failed to get app data dir: {}\", e))?;\n\n    // Ensure data directory exists\n    std::fs::create_dir_all(&data_dir)\n        .map_err(|e| format!(\"Failed to create data dir: {}\", e))?;\n\n    println!(\"=================================================================\");\n    println!(\"Starting voicebox-server sidecar\");\n    println!(\"Data directory: {:?}\", data_dir);\n    println!(\"Remote mode: {}\", remote.unwrap_or(false));\n\n    // Check for CUDA backend in data directory (onedir layout: backends/cuda/)\n    let cuda_binary = {\n        let cuda_dir = data_dir.join(\"backends\").join(\"cuda\");\n        let cuda_name = if cfg!(windows) {\n            \"voicebox-server-cuda.exe\"\n        } else {\n            \"voicebox-server-cuda\"\n        };\n        let exe_path = cuda_dir.join(cuda_name);\n        if exe_path.exists() {\n            println!(\"Found CUDA backend at {:?}\", cuda_dir);\n\n            // Version check: run --version from the onedir directory so\n            // PyInstaller can find its support files for the fast --version path\n            let app_version = app.config().version.clone().unwrap_or_default();\n            let version_ok = match std::process::Command::new(&exe_path)\n                .arg(\"--version\")\n                .current_dir(&cuda_dir)\n                .output()\n            {\n                Ok(output) => {\n                    // Output format: \"voicebox-server X.Y.Z\\n\"\n                    let version_str = String::from_utf8_lossy(&output.stdout);\n                    let binary_version = version_str.trim().split_whitespace().last().unwrap_or(\"\");\n                    if binary_version == app_version {\n                        println!(\"CUDA binary version {} matches app version\", binary_version);\n                        true\n                    } else {\n                        println!(\n                            \"CUDA binary version mismatch: binary={}, app={}. Falling back to CPU.\",\n                            binary_version, app_version\n                        );\n                        false\n                    }\n                }\n                Err(e) => {\n                    println!(\"Failed to check CUDA binary version: {}. Falling back to CPU.\", e);\n                    false\n                }\n            };\n\n            if version_ok {\n                Some(exe_path)\n            } else {\n                None\n            }\n        } else {\n            println!(\"No CUDA backend found, using bundled CPU binary\");\n            None\n        }\n    };\n\n    let sidecar_result = app.shell().sidecar(\"voicebox-server\");\n\n    let mut sidecar = match sidecar_result {\n        Ok(s) => s,\n        Err(e) => {\n            eprintln!(\"Failed to get sidecar: {}\", e);\n\n            // In dev mode, check if the server is already running (started manually)\n            #[cfg(debug_assertions)]\n            {\n                eprintln!(\"Dev mode: Checking if server is already running on port {}...\", SERVER_PORT);\n\n                // Try to connect to the server port\n                use std::net::TcpStream;\n                if TcpStream::connect_timeout(\n                    &format!(\"127.0.0.1:{}\", SERVER_PORT).parse().unwrap(),\n                    std::time::Duration::from_secs(1),\n                ).is_ok() {\n                    println!(\"Found server already running on port {}\", SERVER_PORT);\n                    return Ok(format!(\"http://127.0.0.1:{}\", SERVER_PORT));\n                }\n\n                eprintln!(\"\");\n                eprintln!(\"=================================================================\");\n                eprintln!(\"DEV MODE: No server found on port {}\", SERVER_PORT);\n                eprintln!(\"\");\n                eprintln!(\"Start the Python server in a separate terminal:\");\n                eprintln!(\"  bun run dev:server\");\n                eprintln!(\"=================================================================\");\n                eprintln!(\"\");\n            }\n\n            return Err(format!(\"Failed to start server. In dev mode, run 'bun run dev:server' in a separate terminal.\"));\n        }\n    };\n\n    println!(\"Sidecar command created successfully\");\n\n    // Build common args\n    let data_dir_str = data_dir\n        .to_str()\n        .ok_or_else(|| \"Invalid data dir path\".to_string())?\n        .to_string();\n    let port_str = SERVER_PORT.to_string();\n    let parent_pid_str = std::process::id().to_string();\n    let is_remote = remote.unwrap_or(false);\n\n    // Resolve the custom models directory from the parameter or stored state\n    let effective_models_dir = models_dir.or_else(|| state.models_dir.lock().unwrap().clone());\n    if let Some(ref dir) = effective_models_dir {\n        println!(\"Custom models directory: {}\", dir);\n    }\n\n    // If CUDA binary exists, launch it from the onedir directory.\n    // .current_dir() is critical: PyInstaller onedir expects all DLLs and\n    // support files (nvidia/, _internal/, etc.) relative to the exe.\n    let spawn_result = if let Some(ref cuda_path) = cuda_binary {\n        let cuda_dir = cuda_path.parent().unwrap();\n        println!(\"Launching CUDA backend: {:?} (cwd: {:?})\", cuda_path, cuda_dir);\n        let mut cmd = app.shell().command(cuda_path.to_str().unwrap());\n        cmd = cmd.current_dir(cuda_dir);\n        cmd = cmd.args([\"--data-dir\", &data_dir_str, \"--port\", &port_str, \"--parent-pid\", &parent_pid_str]);\n        if is_remote {\n            cmd = cmd.args([\"--host\", \"0.0.0.0\"]);\n        }\n        if let Some(ref dir) = effective_models_dir {\n            cmd = cmd.env(\"VOICEBOX_MODELS_DIR\", dir);\n        }\n        cmd.spawn()\n    } else {\n        // Use the bundled CPU sidecar\n        sidecar = sidecar.args([\"--data-dir\", &data_dir_str, \"--port\", &port_str, \"--parent-pid\", &parent_pid_str]);\n        if is_remote {\n            sidecar = sidecar.args([\"--host\", \"0.0.0.0\"]);\n        }\n        if let Some(ref dir) = effective_models_dir {\n            sidecar = sidecar.env(\"VOICEBOX_MODELS_DIR\", dir);\n        }\n        println!(\"Spawning server process...\");\n        sidecar.spawn()\n    };\n\n    let (mut rx, child) = match spawn_result {\n        Ok(result) => result,\n        Err(e) => {\n            eprintln!(\"Failed to spawn server process: {}\", e);\n\n            // In dev mode, check if a manually-started server is available\n            #[cfg(debug_assertions)]\n            {\n                use std::net::TcpStream;\n                if TcpStream::connect_timeout(\n                    &format!(\"127.0.0.1:{}\", SERVER_PORT).parse().unwrap(),\n                    std::time::Duration::from_secs(1),\n                ).is_ok() {\n                    println!(\"Found manually-started server on port {}\", SERVER_PORT);\n                    return Ok(format!(\"http://127.0.0.1:{}\", SERVER_PORT));\n                }\n\n                eprintln!(\"\");\n                eprintln!(\"=================================================================\");\n                eprintln!(\"DEV MODE: Server binary failed to start\");\n                eprintln!(\"\");\n                eprintln!(\"Start the Python server in a separate terminal:\");\n                eprintln!(\"  bun run dev:server\");\n                eprintln!(\"=================================================================\");\n                eprintln!(\"\");\n                return Err(\"Dev mode: Start server manually with 'bun run dev:server'\".to_string());\n            }\n\n            #[cfg(not(debug_assertions))]\n            {\n                eprintln!(\"This could be due to:\");\n                eprintln!(\"  - Missing or corrupted binary\");\n                eprintln!(\"  - Missing execute permissions\");\n                eprintln!(\"  - Code signing issues on macOS\");\n                eprintln!(\"  - Missing dependencies\");\n                return Err(format!(\"Failed to spawn: {}\", e));\n            }\n        }\n    };\n\n    println!(\"Server process spawned, waiting for ready signal...\");\n    println!(\"=================================================================\");\n\n    // Store child process and PID\n    let process_pid = child.pid();\n    *state.server_pid.lock().unwrap() = Some(process_pid);\n    *state.child.lock().unwrap() = Some(child);\n\n    // Wait for server to be ready by listening for startup log\n    // PyInstaller bundles can be slow on first import, especially torch/transformers\n    let timeout = tokio::time::Duration::from_secs(120);\n    let start_time = tokio::time::Instant::now();\n    let mut error_output = Vec::new();\n\n    loop {\n        if start_time.elapsed() > timeout {\n            eprintln!(\"Server startup timeout after 120 seconds\");\n            if !error_output.is_empty() {\n                eprintln!(\"Collected error output:\");\n                for line in &error_output {\n                    eprintln!(\"  {}\", line);\n                }\n            }\n\n            // In dev mode, check if a manual server came up during the wait\n            #[cfg(debug_assertions)]\n            {\n                use std::net::TcpStream;\n                if TcpStream::connect_timeout(\n                    &format!(\"127.0.0.1:{}\", SERVER_PORT).parse().unwrap(),\n                    std::time::Duration::from_secs(1),\n                ).is_ok() {\n                    // Kill the placeholder process\n                    let _ = state.child.lock().unwrap().take();\n                    println!(\"Found manually-started server on port {}\", SERVER_PORT);\n                    return Ok(format!(\"http://127.0.0.1:{}\", SERVER_PORT));\n                }\n            }\n\n            return Err(\"Server startup timeout - check Console.app for detailed logs\".to_string());\n        }\n\n        match tokio::time::timeout(tokio::time::Duration::from_millis(100), rx.recv()).await {\n            Ok(Some(event)) => {\n                match event {\n                    tauri_plugin_shell::process::CommandEvent::Stdout(line) => {\n                        let line_str = String::from_utf8_lossy(&line);\n                        println!(\"Server output: {}\", line_str);\n                        let _ = app.emit(\"server-log\", serde_json::json!({\n                            \"stream\": \"stdout\",\n                            \"line\": line_str.trim_end(),\n                        }));\n\n                        if line_str.contains(\"Uvicorn running\") || line_str.contains(\"Application startup complete\") {\n                            println!(\"Server is ready!\");\n                            break;\n                        }\n                    }\n                    tauri_plugin_shell::process::CommandEvent::Stderr(line) => {\n                        let line_str = String::from_utf8_lossy(&line).to_string();\n                        eprintln!(\"Server: {}\", line_str);\n                        let _ = app.emit(\"server-log\", serde_json::json!({\n                            \"stream\": \"stderr\",\n                            \"line\": line_str.trim_end(),\n                        }));\n\n                        // Collect error lines for debugging\n                        if line_str.contains(\"ERROR\") || line_str.contains(\"Error\") || line_str.contains(\"Failed\") {\n                            error_output.push(line_str.clone());\n                        }\n\n                        // Uvicorn logs to stderr, so check there too\n                        if line_str.contains(\"Uvicorn running\") || line_str.contains(\"Application startup complete\") {\n                            println!(\"Server is ready!\");\n                            break;\n                        }\n                    }\n                    _ => {}\n                }\n            }\n            Ok(None) => {\n                // In dev mode, this is expected when using the placeholder binary\n                #[cfg(debug_assertions)]\n                {\n                    use std::net::TcpStream;\n                    eprintln!(\"Server process ended (dev mode placeholder detected)\");\n\n                    // Check if a manually-started server is available\n                    if TcpStream::connect_timeout(\n                        &format!(\"127.0.0.1:{}\", SERVER_PORT).parse().unwrap(),\n                        std::time::Duration::from_secs(1),\n                    ).is_ok() {\n                        // Clean up state\n                        let _ = state.child.lock().unwrap().take();\n                        let _ = state.server_pid.lock().unwrap().take();\n                        println!(\"Found manually-started server on port {}\", SERVER_PORT);\n                        return Ok(format!(\"http://127.0.0.1:{}\", SERVER_PORT));\n                    }\n\n                    eprintln!(\"\");\n                    eprintln!(\"=================================================================\");\n                    eprintln!(\"DEV MODE: No bundled server binary available\");\n                    eprintln!(\"\");\n                    eprintln!(\"Start the Python server in a separate terminal:\");\n                    eprintln!(\"  bun run dev:server\");\n                    eprintln!(\"=================================================================\");\n                    eprintln!(\"\");\n                    return Err(\"Dev mode: Start server manually with 'bun run dev:server'\".to_string());\n                }\n\n                #[cfg(not(debug_assertions))]\n                {\n                    eprintln!(\"Server process ended unexpectedly during startup!\");\n                    eprintln!(\"The server binary may have crashed or exited with an error.\");\n                    eprintln!(\"Check Console.app logs for more details (search for 'voicebox')\");\n                    return Err(\"Server process ended unexpectedly\".to_string());\n                }\n            }\n            Err(_) => {\n                // Timeout on this recv, continue loop\n                continue;\n            }\n        }\n    }\n\n    // Spawn task to continue reading output and emit to frontend\n    let app_handle = app.clone();\n    tokio::spawn(async move {\n        while let Some(event) = rx.recv().await {\n            match event {\n                tauri_plugin_shell::process::CommandEvent::Stdout(line) => {\n                    let line_str = String::from_utf8_lossy(&line);\n                    println!(\"Server: {}\", line_str);\n                    let _ = app_handle.emit(\"server-log\", serde_json::json!({\n                        \"stream\": \"stdout\",\n                        \"line\": line_str.trim_end(),\n                    }));\n                }\n                tauri_plugin_shell::process::CommandEvent::Stderr(line) => {\n                    let line_str = String::from_utf8_lossy(&line);\n                    eprintln!(\"Server error: {}\", line_str);\n                    let _ = app_handle.emit(\"server-log\", serde_json::json!({\n                        \"stream\": \"stderr\",\n                        \"line\": line_str.trim_end(),\n                    }));\n                }\n                _ => {}\n            }\n        }\n    });\n\n    Ok(format!(\"http://127.0.0.1:{}\", SERVER_PORT))\n}\n\n#[command]\nasync fn stop_server(state: State<'_, ServerState>) -> Result<(), String> {\n    let pid = state.server_pid.lock().unwrap().take();\n    let _child = state.child.lock().unwrap().take();\n    \n    if let Some(pid) = pid {\n        println!(\"stop_server: Stopping server with PID: {}\", pid);\n        \n        #[cfg(unix)]\n        {\n            use std::process::Command;\n            // Kill process group with SIGTERM first\n            let _ = Command::new(\"kill\")\n                .args([\"-TERM\", \"--\", &format!(\"-{}\", pid)])\n                .output();\n            \n            // Brief wait then force kill\n            std::thread::sleep(std::time::Duration::from_millis(100));\n            \n            let _ = Command::new(\"kill\")\n                .args([\"-9\", \"--\", &format!(\"-{}\", pid)])\n                .output();\n            let _ = Command::new(\"kill\")\n                .args([\"-9\", &pid.to_string()])\n                .output();\n            \n            println!(\"stop_server: Process group kill completed\");\n        }\n        \n        #[cfg(windows)]\n        {\n            // Send graceful shutdown via HTTP — the server's parent-pid watchdog\n            // will also handle cleanup if this app process exits.\n            println!(\"Sending graceful shutdown via HTTP...\");\n            let client = reqwest::blocking::Client::builder()\n                .timeout(std::time::Duration::from_secs(2))\n                .build()\n                .unwrap();\n\n            let _ = client\n                .post(&format!(\"http://127.0.0.1:{}/shutdown\", SERVER_PORT))\n                .send();\n\n            println!(\"Shutdown request sent (server watchdog will handle cleanup)\");\n        }\n    }\n    \n    Ok(())\n}\n\n#[command]\nasync fn restart_server(\n    app: tauri::AppHandle,\n    state: State<'_, ServerState>,\n    models_dir: Option<String>,\n) -> Result<String, String> {\n    println!(\"restart_server: stopping current server...\");\n\n    // Update stored models_dir: empty string means reset to default, non-empty means set\n    if let Some(ref dir) = models_dir {\n        if dir.is_empty() {\n            *state.models_dir.lock().unwrap() = None;\n        } else {\n            *state.models_dir.lock().unwrap() = Some(dir.clone());\n        }\n    }\n\n    // Stop the current server\n    stop_server(state.clone()).await?;\n\n    // Wait for port to be released\n    println!(\"restart_server: waiting for port release...\");\n    tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;\n\n    // Start server again (will auto-detect CUDA binary and use stored models_dir)\n    println!(\"restart_server: starting server...\");\n    start_server(app, state, None, None).await\n}\n\n#[command]\nfn set_keep_server_running(state: State<'_, ServerState>, keep_running: bool) {\n    println!(\"set_keep_server_running called with: {}\", keep_running);\n    *state.keep_running_on_close.lock().unwrap() = keep_running;\n}\n\n#[command]\nasync fn start_system_audio_capture(\n    state: State<'_, audio_capture::AudioCaptureState>,\n    max_duration_secs: u32,\n) -> Result<(), String> {\n    audio_capture::start_capture(&state, max_duration_secs).await\n}\n\n#[command]\nasync fn stop_system_audio_capture(\n    state: State<'_, audio_capture::AudioCaptureState>,\n) -> Result<String, String> {\n    audio_capture::stop_capture(&state).await\n}\n\n#[command]\nfn is_system_audio_supported() -> bool {\n    audio_capture::is_supported()\n}\n\n#[command]\nfn list_audio_output_devices(\n    state: State<'_, audio_output::AudioOutputState>,\n) -> Result<Vec<audio_output::AudioOutputDevice>, String> {\n    state.list_output_devices()\n}\n\n#[command]\nasync fn play_audio_to_devices(\n    state: State<'_, audio_output::AudioOutputState>,\n    audio_data: Vec<u8>,\n    device_ids: Vec<String>,\n) -> Result<(), String> {\n    state.play_audio_to_devices(audio_data, device_ids).await\n}\n\n#[command]\nfn stop_audio_playback(\n    state: State<'_, audio_output::AudioOutputState>,\n) -> Result<(), String> {\n    state.stop_all_playback()\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n    tauri::Builder::default()\n        .plugin(tauri_plugin_dialog::init())\n        .plugin(tauri_plugin_fs::init())\n        .plugin(tauri_plugin_shell::init())\n        .manage(ServerState {\n            child: Mutex::new(None),\n            server_pid: Mutex::new(None),\n            keep_running_on_close: Mutex::new(false),\n            models_dir: Mutex::new(None),\n        })\n        .manage(audio_capture::AudioCaptureState::new())\n        .manage(audio_output::AudioOutputState::new())\n        .setup(|app| {\n            #[cfg(desktop)]\n            {\n                app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;\n                app.handle().plugin(tauri_plugin_process::init())?;\n            }\n\n            // Hide title bar icon on Windows\n            #[cfg(windows)]\n            {\n                use windows::Win32::Foundation::HWND;\n                use windows::Win32::UI::WindowsAndMessaging::{SetClassLongPtrW, GCLP_HICON, GCLP_HICONSM};\n                \n                if let Some((_, window)) = app.webview_windows().iter().next() {\n                    if let Ok(hwnd) = window.hwnd() {\n                        let hwnd = HWND(hwnd.0);\n                        unsafe {\n                            // Set both small and regular icons to NULL to hide the title bar icon\n                            SetClassLongPtrW(hwnd, GCLP_HICON, 0);\n                            SetClassLongPtrW(hwnd, GCLP_HICONSM, 0);\n                        }\n                    }\n                }\n            }\n\n            // Enable microphone access on Linux (WebKitGTK denies getUserMedia by default)\n            #[cfg(target_os = \"linux\")]\n            {\n                use tauri::Manager;\n                if let Some(window) = app.get_webview_window(\"main\") {\n                    let _ = window.with_webview(|webview| {\n                        use webkit2gtk::{WebViewExt, SettingsExt, PermissionRequestExt};\n                        use webkit2gtk::glib::ObjectExt;\n                        let wk_webview = webview.inner();\n\n                        // Enable media stream support in WebKitGTK settings\n                        if let Some(settings) = WebViewExt::settings(&wk_webview) {\n                            settings.set_enable_media_stream(true);\n                        }\n\n                        // Auto-grant UserMediaPermissionRequest (microphone access)\n                        // Only for trusted local origins (Tauri dev server or custom protocol)\n                        wk_webview.connect_permission_request(move |webview, request: &webkit2gtk::PermissionRequest| {\n                            if request.is::<webkit2gtk::UserMediaPermissionRequest>() {\n                                let uri = WebViewExt::uri(webview).unwrap_or_default();\n                                let is_trusted = uri.starts_with(\"tauri://\")\n                                    || uri.starts_with(\"https://tauri.localhost\")\n                                    || uri.starts_with(\"http://localhost\")\n                                    || uri.starts_with(\"http://127.0.0.1\");\n                                if is_trusted {\n                                    request.allow();\n                                    return true;\n                                }\n                                request.deny();\n                                return true;\n                            }\n                            false\n                        });\n                    });\n                }\n            }\n\n            Ok(())\n        })\n        .invoke_handler(tauri::generate_handler![\n            start_server,\n            stop_server,\n            restart_server,\n            set_keep_server_running,\n            start_system_audio_capture,\n            stop_system_audio_capture,\n            is_system_audio_supported,\n            list_audio_output_devices,\n            play_audio_to_devices,\n            stop_audio_playback\n        ])\n        .on_window_event({\n            let closing = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));\n            move |window, event| {\n            if let WindowEvent::CloseRequested { api, .. } = event {\n                // If we're already in the close flow, let it proceed\n                if closing.load(std::sync::atomic::Ordering::SeqCst) {\n                    return;\n                }\n                closing.store(true, std::sync::atomic::Ordering::SeqCst);\n\n                // Prevent automatic close so frontend can clean up\n                api.prevent_close();\n\n                // Emit event to frontend to check setting and stop server if needed\n                let app_handle = window.app_handle();\n\n                if let Err(e) = app_handle.emit(\"window-close-requested\", ()) {\n                    eprintln!(\"Failed to emit window-close-requested event: {}\", e);\n                    window.close().ok();\n                    return;\n                }\n\n                // Set up listener for frontend response\n                let window_for_close = window.clone();\n                let closing_for_timeout = closing.clone();\n                let (tx, mut rx) = mpsc::unbounded_channel::<()>();\n\n                let listener_id = window.listen(\"window-close-allowed\", move |_| {\n                    let _ = tx.send(());\n                });\n\n                tauri::async_runtime::spawn(async move {\n                    tokio::select! {\n                        _ = rx.recv() => {\n                            window_for_close.close().ok();\n                        }\n                        _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {\n                            eprintln!(\"Window close timeout, closing anyway\");\n                            window_for_close.close().ok();\n                        }\n                    }\n                    window_for_close.unlisten(listener_id);\n                    closing_for_timeout.store(false, std::sync::atomic::Ordering::SeqCst);\n                });\n            }\n        }})\n        .build(tauri::generate_context!())\n        .expect(\"error while building tauri application\")\n        .run(|app, event| {\n            let _ = &app; // used on unix\n            match &event {\n                RunEvent::Exit => {\n                    let state = app.state::<ServerState>();\n                    let keep_running = *state.keep_running_on_close.lock().unwrap();\n                    let has_pid = state.server_pid.lock().unwrap().is_some();\n                    println!(\"RunEvent::Exit — keep_running={}, has_pid={}\", keep_running, has_pid);\n\n                    if keep_running {\n                        // Tell the server to disable its watchdog so it survives\n                        // after this process exits.\n                        println!(\"Keep server running: disabling watchdog...\");\n                        let client = reqwest::blocking::Client::builder()\n                            .timeout(std::time::Duration::from_secs(2))\n                            .build()\n                            .unwrap();\n                        match client\n                            .post(&format!(\"http://127.0.0.1:{}/watchdog/disable\", SERVER_PORT))\n                            .send()\n                        {\n                            Ok(resp) => println!(\"Watchdog disable response: {}\", resp.status()),\n                            Err(e) => eprintln!(\"Failed to disable watchdog: {}\", e),\n                        }\n                    } else {\n                        // Server will self-terminate via parent-pid watchdog when\n                        // this process exits. On Unix, also send SIGTERM for\n                        // immediate cleanup.\n                        println!(\"RunEvent::Exit - server will self-terminate via watchdog\");\n\n                        #[cfg(unix)]\n                        {\n                            if let Some(pid) = state.server_pid.lock().unwrap().take() {\n                                use std::process::Command;\n                                let _ = Command::new(\"kill\")\n                                    .args([\"-TERM\", \"--\", &format!(\"-{}\", pid)])\n                                    .output();\n                                std::thread::sleep(std::time::Duration::from_millis(100));\n                                let _ = Command::new(\"kill\")\n                                    .args([\"-9\", \"--\", &format!(\"-{}\", pid)])\n                                    .output();\n                                let _ = Command::new(\"kill\")\n                                    .args([\"-9\", &pid.to_string()])\n                                    .output();\n                            }\n                        }\n                    }\n                }\n                RunEvent::ExitRequested { api, .. } => {\n                    println!(\"RunEvent::ExitRequested received\");\n                    // Don't prevent exit, just log it\n                    let _ = api;\n                }\n                _ => {}\n            }\n        });\n}\n\nfn main() {\n    run();\n}\n"
  },
  {
    "path": "tauri/src-tauri/tauri.conf.json",
    "content": "{\n  \"$schema\": \"https://schema.tauri.app/config/2\",\n  \"productName\": \"Voicebox\",\n  \"version\": \"0.3.1\",\n  \"identifier\": \"sh.voicebox.app\",\n  \"build\": {\n    \"beforeDevCommand\": \"bun run dev\",\n    \"beforeBuildCommand\": \"bun run build\",\n    \"frontendDist\": \"../dist\",\n    \"devUrl\": \"http://localhost:5173\"\n  },\n  \"bundle\": {\n    \"active\": true,\n    \"targets\": \"all\",\n    \"createUpdaterArtifacts\": \"v1Compatible\",\n    \"externalBin\": [\"binaries/voicebox-server\"],\n    \"icon\": [\n      \"icons/32x32.png\",\n      \"icons/128x128.png\",\n      \"icons/128x128@2x.png\",\n      \"icons/icon.icns\",\n      \"icons/icon.ico\"\n    ],\n    \"macOS\": {\n      \"frameworks\": [],\n      \"minimumSystemVersion\": \"11.0\",\n      \"infoPlist\": \"Info.plist\",\n      \"entitlements\": \"Entitlements.plist\"\n    },\n    \"resources\": {\n      \"gen/Assets.car\": \"./\",\n      \"gen/voicebox.icns\": \"./\",\n      \"gen/partial.plist\": \"./\"\n    }\n  },\n  \"app\": {\n    \"security\": {\n      \"csp\": null,\n      \"capabilities\": [\"default\"]\n    },\n    \"windows\": [\n      {\n        \"title\": \"\",\n        \"width\": 1200,\n        \"height\": 800,\n        \"minWidth\": 800,\n        \"minHeight\": 600,\n        \"resizable\": true,\n        \"fullscreen\": false,\n        \"devtools\": true,\n        \"userAgent\": null,\n        \"titleBarStyle\": \"Overlay\"\n      }\n    ],\n    \"withGlobalTauri\": true\n  },\n  \"plugins\": {\n    \"shell\": {\n      \"open\": \".*\"\n    },\n    \"updater\": {\n      \"pubkey\": \"dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUxRENBQkRBQjdBNTM1OTIKUldTU05hVzMycXZjNGJGcUxmcVVocll2QjdSaTJNdlFxR2M3VDJsMnVvbDdyZGRPMmRlOW9aWTcK\",\n      \"endpoints\": [\"https://github.com/jamiepine/voicebox/releases/latest/download/latest.json\"]\n    }\n  }\n}\n"
  },
  {
    "path": "tauri/src-tauri/tests/audio_capture_test.rs",
    "content": "// NOTE: This test requires system audio to be playing during execution.\n// To run this test successfully:\n//   1. Start playing audio (music, video, etc.)\n//   2. Run: cargo test --test audio_capture_test -- --nocapture\n//   3. The test will capture audio for 5 seconds and verify the output\n\nuse voicebox::audio_capture::{AudioCaptureState, start_capture, stop_capture};\nuse base64::Engine;\n\n#[tokio::test]\nasync fn test_system_audio_capture() {\n    // Create AudioCaptureState\n    let state = AudioCaptureState::new();\n\n    println!(\"Starting system audio capture with 5 second max duration...\");\n\n    // Start capture with 5 second max duration\n    let result = start_capture(&state, 5).await;\n\n    if let Err(e) = result {\n        panic!(\"Failed to start capture: {}\", e);\n    }\n\n    println!(\"Capture started, waiting 5 seconds...\");\n\n    // Wait 5 seconds for capture to complete\n    tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;\n\n    println!(\"Stopping capture...\");\n\n    // Stop capture and get the result\n    let audio_data = stop_capture(&state).await;\n\n    match audio_data {\n        Ok(base64_wav) => {\n            println!(\"Capture stopped successfully\");\n\n            // Validate the returned base64 WAV data\n            println!(\"Validating base64 WAV data...\");\n\n            // Decode base64 to bytes\n            let decoded_bytes = base64::engine::general_purpose::STANDARD\n                .decode(&base64_wav)\n                .expect(\"Failed to decode base64 data\");\n\n            // Verify bytes array is not empty\n            assert!(!decoded_bytes.is_empty(), \"Decoded bytes array is empty\");\n\n            // Confirm data has content (length > 0)\n            println!(\"WAV data length: {} bytes\", decoded_bytes.len());\n            assert!(decoded_bytes.len() > 0, \"WAV data has no content\");\n\n            println!(\"✓ Test passed: Audio capture produced valid WAV data\");\n        }\n        Err(e) => {\n            panic!(\"Failed to stop capture or get audio data: {}\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "tauri/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"types\": [\"vite/client\"],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"../app/src/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "tauri/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "tauri/vite.config.ts",
    "content": "import path from 'node:path';\nimport tailwindcss from '@tailwindcss/vite';\nimport react from '@vitejs/plugin-react';\nimport { defineConfig } from 'vite';\nimport { changelogPlugin } from '../app/plugins/changelog';\n\nexport default defineConfig({\n  plugins: [react(), tailwindcss(), changelogPlugin(path.resolve(__dirname, '..'))],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, '../app/src'),\n      react: path.resolve(__dirname, '../app/node_modules/react'),\n      'react-dom': path.resolve(__dirname, '../app/node_modules/react-dom'),\n      '@tanstack/react-query': path.resolve(__dirname, '../app/node_modules/@tanstack/react-query'),\n      '@tanstack/react-query-devtools': path.resolve(\n        __dirname,\n        '../app/node_modules/@tanstack/react-query-devtools',\n      ),\n      zustand: path.resolve(__dirname, '../app/node_modules/zustand'),\n    },\n    dedupe: ['react', 'react-dom', '@tanstack/react-query', 'zustand'],\n  },\n  root: path.resolve(__dirname),\n  clearScreen: false,\n  server: {\n    port: 5173,\n    strictPort: true,\n    // Watch files in the app directory for changes\n    watch: {\n      ignored: ['!**/../app/**'],\n    },\n  },\n  envPrefix: ['VITE_', 'TAURI_'],\n  build: {\n    target: 'es2021',\n    minify: !process.env.TAURI_DEBUG,\n    sourcemap: !!process.env.TAURI_DEBUG,\n    outDir: 'dist',\n  },\n});\n"
  },
  {
    "path": "web/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>voicebox</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "web/package.json",
    "content": "{\n  \"name\": \"@voicebox/web\",\n  \"private\": true,\n  \"version\": \"0.3.1\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"preview\": \"vite preview\",\n    \"lint\": \"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.3.0\",\n    \"react-dom\": \"^18.3.0\",\n    \"@tanstack/react-query\": \"^5.0.0\",\n    \"zustand\": \"^4.5.0\",\n    \"wavesurfer.js\": \"^7.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.3.0\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.0.0\",\n    \"@typescript-eslint/parser\": \"^7.0.0\",\n    \"@tailwindcss/vite\": \"^4.0.0\",\n    \"@vitejs/plugin-react\": \"^4.3.0\",\n    \"eslint\": \"^8.57.0\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.0\",\n    \"typescript\": \"^5.6.0\",\n    \"vite\": \"^5.4.0\"\n  }\n}\n"
  },
  {
    "path": "web/src/main.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport App from '../../app/src/App';\nimport '../../app/src/index.css';\nimport { PlatformProvider } from '../../app/src/platform/PlatformContext';\nimport { webPlatform } from './platform';\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 1000 * 60 * 5, // 5 minutes\n      gcTime: 1000 * 60 * 10, // 10 minutes\n      retry: 1,\n      refetchOnWindowFocus: false,\n    },\n  },\n});\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <React.StrictMode>\n    <QueryClientProvider client={queryClient}>\n      <PlatformProvider platform={webPlatform}>\n        <App />\n      </PlatformProvider>\n    </QueryClientProvider>\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "web/src/platform/audio.ts",
    "content": "import type { PlatformAudio, AudioDevice } from '@/platform/types';\n\nexport const webAudio: PlatformAudio = {\n  isSystemAudioSupported(): boolean {\n    return false; // System audio capture not supported in web\n  },\n\n  async startSystemAudioCapture(_maxDurationSecs: number): Promise<void> {\n    throw new Error('System audio capture is only available in the desktop app.');\n  },\n\n  async stopSystemAudioCapture(): Promise<Blob> {\n    throw new Error('System audio capture is only available in the desktop app.');\n  },\n\n  async listOutputDevices(): Promise<AudioDevice[]> {\n    return []; // No native device routing in web\n  },\n\n  async playToDevices(_audioData: Uint8Array, _deviceIds: string[]): Promise<void> {\n    throw new Error('Native audio device routing is only available in the desktop app.');\n  },\n\n  stopPlayback(): void {\n    // No-op for web\n  },\n};\n"
  },
  {
    "path": "web/src/platform/filesystem.ts",
    "content": "import type { FileFilter, PlatformFilesystem } from '@/platform/types';\n\nexport const webFilesystem: PlatformFilesystem = {\n  async saveFile(filename: string, blob: Blob, _filters?: FileFilter[]) {\n    // Browser: trigger download\n    const url = window.URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.href = url;\n    a.download = filename;\n    document.body.appendChild(a);\n    a.click();\n    window.URL.revokeObjectURL(url);\n    document.body.removeChild(a);\n  },\n\n  async openPath(_path: string) {\n    // No filesystem access in browser\n  },\n\n  async pickDirectory(_title: string) {\n    return null;\n  },\n};\n"
  },
  {
    "path": "web/src/platform/index.ts",
    "content": "import type { Platform } from '@/platform/types';\nimport { webFilesystem } from './filesystem';\nimport { webUpdater } from './updater';\nimport { webAudio } from './audio';\nimport { webLifecycle } from './lifecycle';\nimport { webMetadata } from './metadata';\n\nexport const webPlatform: Platform = {\n  filesystem: webFilesystem,\n  updater: webUpdater,\n  audio: webAudio,\n  lifecycle: webLifecycle,\n  metadata: webMetadata,\n};\n"
  },
  {
    "path": "web/src/platform/lifecycle.ts",
    "content": "import type { PlatformLifecycle, ServerLogEntry } from '@/platform/types';\n\nclass WebLifecycle implements PlatformLifecycle {\n  onServerReady?: () => void;\n\n  async startServer(_remote = false, _modelsDir?: string | null): Promise<string> {\n    // Web assumes server is running externally\n    // Return a default URL - this should be configured via env vars\n    const serverUrl = import.meta.env.VITE_SERVER_URL || 'http://localhost:17493';\n    this.onServerReady?.();\n    return serverUrl;\n  }\n\n  async stopServer(): Promise<void> {\n    // No-op for web - server is managed externally\n  }\n\n  async restartServer(_modelsDir?: string | null): Promise<string> {\n    // No-op for web - server is managed externally\n    return import.meta.env.VITE_SERVER_URL || 'http://localhost:17493';\n  }\n\n  async setKeepServerRunning(_keep: boolean): Promise<void> {\n    // No-op for web\n  }\n\n  async setupWindowCloseHandler(): Promise<void> {\n    // No-op for web - no window close handling needed\n  }\n\n  subscribeToServerLogs(_callback: (_entry: ServerLogEntry) => void): () => void {\n    // No-op for web - server logs are not available\n    return () => {};\n  }\n}\n\nexport const webLifecycle = new WebLifecycle();\n"
  },
  {
    "path": "web/src/platform/metadata.ts",
    "content": "import type { PlatformMetadata } from '@/platform/types';\n\nexport const webMetadata: PlatformMetadata = {\n  async getVersion(): Promise<string> {\n    // Return version from env var or package.json\n    return import.meta.env.VITE_APP_VERSION || '0.1.0';\n  },\n  isTauri: false,\n};\n"
  },
  {
    "path": "web/src/platform/updater.ts",
    "content": "import type { PlatformUpdater, UpdateStatus } from '@/platform/types';\n\nclass WebUpdater implements PlatformUpdater {\n  private status: UpdateStatus = {\n    checking: false,\n    available: false,\n    downloading: false,\n    installing: false,\n    readyToInstall: false,\n  };\n\n  private subscribers: Set<(status: UpdateStatus) => void> = new Set();\n\n  private notifySubscribers() {\n    this.subscribers.forEach((callback) => callback(this.status));\n  }\n\n  subscribe(callback: (status: UpdateStatus) => void): () => void {\n    this.subscribers.add(callback);\n    callback(this.status);\n    return () => {\n      this.subscribers.delete(callback);\n    };\n  }\n\n  getStatus(): UpdateStatus {\n    return { ...this.status };\n  }\n\n  async checkForUpdates(): Promise<void> {\n    // Web apps don't need client-side updates\n    // Updates are handled by redeploying the web app\n  }\n\n  async downloadAndInstall(): Promise<void> {\n    // No-op for web\n  }\n\n  async restartAndInstall(): Promise<void> {\n    // No-op for web\n  }\n}\n\nexport const webUpdater = new WebUpdater();\n"
  },
  {
    "path": "web/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"../app/src/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "web/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "web/vite.config.ts",
    "content": "import path from 'node:path';\nimport tailwindcss from '@tailwindcss/vite';\nimport react from '@vitejs/plugin-react';\nimport { defineConfig } from 'vite';\nimport { changelogPlugin } from '../app/plugins/changelog';\n\nexport default defineConfig({\n  plugins: [react(), tailwindcss(), changelogPlugin(path.resolve(__dirname, '..'))],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, '../app/src'),\n    },\n  },\n  build: {\n    outDir: 'dist',\n  },\n});\n"
  }
]