Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ src-tauri/sidecar/__pycache__/
src-tauri/sidecar/dist/
src-tauri/sidecar/build/
src-tauri/sidecar/.venv/
src-tauri/sidecar/data/
.sidecar-venv/

# Superpowers
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Changelog

## [0.2.0] - 2026-04-08

**57 files | 3,086 additions | 608 deletions | 3 PRs | 29 commits**

### 🌟 New Features

- **Model Settings panel** — dedicated sidebar page to browse, download, and manage AI models with category tags, recommended badges, and Whisper model selection (#21)
- **Video rename** — rename recordings directly from the library view (#20)
- **Auto-detect `ELEVENLABS_API_KEY`** from environment variables — no manual entry needed if already exported (#20)
- **Version bump tooling** — `bump.sh` script for consistent release versioning

### 🐛 Bug Fixes

- **Slow/deep voice on first generation** after switching voices — TTS engine now reinitializes properly on voice change
- **Video clipping** in both TTS and voice conversion modes — output duration now matches source (#20)
- **TTS timing and transcription accuracy** improvements — better duration probing and segment alignment (#20)
- **Model cache not refreshing** after download completes — cache invalidation added (#21)
- **CosyVoice3 missing** from prerequisites UI and download JSON parsing error (#20)
- **Sidecar code signing** added to PyInstaller build for macOS Gatekeeper (#20)
- **84 findings** from comprehensive codebase review resolved — spanning error handling, edge cases, and code quality
- **22 model-related bugs** fixed in consolidated review (#21)
- **`whisper-small` removed** — too low quality for production use (#21)
- **Models nav alignment** fixed in sidebar (#21)
- **`yt-dlp` options** corrected for voice extraction

### 🛠️ Other Improvements

- Models page moved from settings sub-tab to dedicated sidebar route with `ModelInfo` struct enriched with `category` and `recommended` fields (#21)
- `cleanup_stale_recordings` test made deterministic (#21)
- README updated: Linux references removed (macOS-only app), visual architecture diagram replaces ASCII, TTS audio cutoff added to known issues (#19, #20)
- Integration tests updated for CosyVoice3 three-model setup (#20)
- Unused CI workflow files cleaned up (#20)

## [0.1.0] - 2026-04-02

**86 files | 12,367 lines | 18 PRs | 99 commits**
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,8 @@ These are the IPC commands exposed by the Rust backend, invoked from the fronten
| `sidecar_fetch` | Proxy a GET/POST request to the sidecar |
| `sidecar_upload` | Proxy a file upload to the sidecar |
| `get_sidecar_status` | Get sidecar process status |
| `check_models_downloaded` | Check if required models are available |
| `download_model` | Download a model from HuggingFace |
| `delete_model` | Delete a downloaded model from the cache |
| `get_models_disk_usage` | Get disk usage of downloaded models |
| `google_drive_connect` | Start Google Drive OAuth2 flow |
| `google_drive_disconnect` | Disconnect Google Drive |
Expand Down Expand Up @@ -404,6 +404,7 @@ HTTP endpoints served by the Python FastAPI sidecar on localhost.
| `/profiles/{id}` | DELETE | Delete a voice profile |
| `/models/status` | GET | Get model download/load status |
| `/models/download` | POST | Download a model (streams progress as SSE) |
| `/models/{model_name}` | DELETE | Delete a downloaded model |

## Debugging

Expand Down
2 changes: 1 addition & 1 deletion docs/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,6 @@ cd src-tauri/sidecar && pyinstaller voiceover-tts.spec

## Notes

- **Unsigned app**: The .dmg is not code-signed. Users will see a macOS Gatekeeper warning on first launch. To open: right-click the app → "Open" → click "Open" in the dialog. This only needs to be done once.
- **Code signing**: The build is configured with `signingIdentity: "Developer ID Application"` and hardened runtime. If a valid Developer ID certificate is in your keychain, the app will be signed automatically. Without the certificate, the app will be unsigned and users will see a macOS Gatekeeper warning on first launch (right-click → "Open" → "Open").
- **ffmpeg is bundled**: The static ffmpeg binary is included inside the .app so users don't need to install it separately. It's ~60MB and excluded from git via `.gitignore`.
- **Settings location**: App config is stored at `~/Library/Application Support/com.voiceover.app/config.json`, not inside the .app bundle.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "voiceover",
"private": true,
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite dev",
Expand All @@ -19,14 +19,13 @@
"test:all:coverage": "pnpm test:coverage && pnpm test:rust:coverage"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tauri-apps/cli": "^2.10.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
"@types/node": "^25.5.0",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^4.1.0",
"jsdom": "^29.0.1",
"lefthook": "^2.1.4",
Expand Down
40 changes: 40 additions & 0 deletions scripts/bump-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Bump version across all three config files simultaneously.
# Usage: ./scripts/bump-version.sh 0.2.0

set -euo pipefail

if [ $# -ne 1 ]; then
echo "Usage: $0 <version>"
echo "Example: $0 0.2.0"
exit 1
fi

VERSION="$1"

# Validate semver format
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "Error: '$VERSION' is not a valid semver (expected X.Y.Z or X.Y.Z-pre.N)"
exit 1
fi

ROOT="$(cd "$(dirname "$0")/.." && pwd)"

# 1. package.json
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" "$ROOT/package.json"

# 2. Cargo.toml (only the package version, line 3)
sed -i '' "3s/version = \"[^\"]*\"/version = \"$VERSION\"/" "$ROOT/src-tauri/Cargo.toml"

# 3. tauri.conf.json
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" "$ROOT/src-tauri/tauri.conf.json"

echo "Bumped to $VERSION in:"
echo " package.json"
echo " src-tauri/Cargo.toml"
echo " src-tauri/tauri.conf.json"

# Show the changes
grep -n "\"version\"" "$ROOT/package.json" | head -1
grep -n "^version" "$ROOT/src-tauri/Cargo.toml" | head -1
grep -n "\"version\"" "$ROOT/src-tauri/tauri.conf.json" | head -1
34 changes: 31 additions & 3 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "voiceover"
version = "0.1.0"
version = "0.2.0"
description = "Screen recorder with ElevenLabs voice replacement"
authors = ["dave"]
license = "MIT"
Expand Down Expand Up @@ -28,10 +28,10 @@ tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
reqwest = { version = "0.12", features = ["multipart", "json", "stream"] }
futures-util = "0.3"
tokio = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "process", "sync", "fs"] }
dirs = "6"
tauri-plugin-global-shortcut = "2"
rand = "0.8"
rand = "0.9"
sha2 = "0.10"
base64 = "0.22"
url = "2"
Expand Down
6 changes: 6 additions & 0 deletions src-tauri/entitlements.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required for MLX/PyTorch JIT compilation on Apple Silicon -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Required for MLX Metal compute kernels -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Required for PyInstaller frozen binaries (sidecar) -->
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<!-- Required for microphone recording -->
<key>com.apple.security.device.audio-input</key>
<true/>
<!-- Required for webcam overlay -->
<key>com.apple.security.device.camera</key>
<true/>
<!-- Required for Python sidecar's dynamic .so loading -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
Expand Down
33 changes: 28 additions & 5 deletions src-tauri/sidecar/chunked_tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
Short text (≤ max_chunk_chars) uses the single-shot fast path with zero overhead.
"""

from __future__ import annotations

import logging
import re
from typing import List, Tuple

import numpy as np

Expand All @@ -30,15 +31,15 @@
_PARA_TAG_RE = re.compile(r"\[[^\]]*\]")


def split_text_into_chunks(text: str, max_chars: int = DEFAULT_MAX_CHUNK_CHARS) -> List[str]:
def split_text_into_chunks(text: str, max_chars: int = DEFAULT_MAX_CHUNK_CHARS) -> list[str]:
"""Split text at natural boundaries into chunks of at most max_chars."""
text = text.strip()
if not text:
return []
if len(text) <= max_chars:
return [text]

chunks: List[str] = []
chunks: list[str] = []
remaining = text

while remaining:
Expand Down Expand Up @@ -115,7 +116,7 @@ def _safe_hard_cut(segment: str, max_chars: int) -> int:


def concatenate_audio_chunks(
chunks: List[np.ndarray],
chunks: list[np.ndarray],
sample_rate: int,
crossfade_ms: int = 50,
) -> np.ndarray:
Expand Down Expand Up @@ -282,13 +283,35 @@ def pad_audio_to_match_timing(
return np.concatenate(result_pieces)


# ---------------------------------------------------------------------------
# Pad / truncate to target duration
# ---------------------------------------------------------------------------


def pad_or_truncate_to_duration(audio: np.ndarray, sample_rate: int, target_duration: float, fade_ms: float = 300) -> np.ndarray:
"""Pad (with fade-out) or truncate audio to match target duration."""
if target_duration <= 0 or not sample_rate:
return audio
target_samples = int(target_duration * sample_rate)
if len(audio) < target_samples:
fade_samples = min(int(fade_ms / 1000 * sample_rate), len(audio) // 2)
if fade_samples > 0:
fade = np.linspace(1.0, 0.0, fade_samples, dtype=np.float32)
audio = audio.copy()
audio[-fade_samples:] *= fade
audio = np.pad(audio, (0, target_samples - len(audio)))
elif len(audio) > target_samples:
audio = audio[:target_samples]
return audio


# ---------------------------------------------------------------------------
# Timestamp-aware audio assembly
# ---------------------------------------------------------------------------


def assemble_timed_segments(
tts_segments: List[Tuple[np.ndarray, float, float]],
tts_segments: list[tuple[np.ndarray, float, float]],
original_duration: float,
sample_rate: int,
) -> np.ndarray:
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/sidecar/entitlements.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required for NumPy/PyTorch JIT on Apple Silicon -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Required for MLX Metal compute kernels -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Required for PyInstaller frozen binary loader -->
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<!-- Required for loading Python extension .so modules -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
Expand Down
10 changes: 8 additions & 2 deletions src-tauri/sidecar/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@
- samples/{uuid}.txt: transcript for each audio file
"""

from __future__ import annotations

import json
import logging
import re
import shutil
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

logger = logging.getLogger("voiceover-tts.profiles")

__all__ = [
"list_profiles", "get_profile", "create_profile", "delete_profile",
"add_sample", "get_samples",
]


def _validate_profile_id(profile_id: str) -> None:
"""Validate profile_id is safe for use as a path component."""
Expand Down Expand Up @@ -45,7 +51,7 @@ def list_profiles(data_dir: Path) -> list[dict]:
return profiles


def get_profile(data_dir: Path, profile_id: str) -> Optional[dict]:
def get_profile(data_dir: Path, profile_id: str) -> dict | None:
"""Get a single profile by ID."""
_validate_profile_id(profile_id)
meta_file = _profiles_dir(data_dir) / profile_id / "profile.json"
Expand Down
Loading
Loading