diff --git a/.gitignore b/.gitignore index 118735ec..5f87d802 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ logs/ # Generated files app/openapi.json tauri/src-tauri/binaries/* +tauri/src-tauri/gen/Assets.car # Temporary tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d662bd3e..f3cab820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,14 +66,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - OpenAPI client generator script now documents the local backend port and avoids an unused loop variable warning ### Added -- **Makefile** - Comprehensive development workflow automation with commands for setup, development, building, testing, and code quality checks - - Includes Python version detection and compatibility warnings - - Self-documenting help system with `make help` - - Colored output for better readability - - Supports parallel development server execution +- **justfile** - Comprehensive development workflow automation with commands for setup, development, building, testing, and code quality checks + - Cross-platform support (macOS, Linux, Windows) + - Python version detection and compatibility warnings + - Self-documenting help system with `just --list` ### Changed -- **README** - Added Makefile reference and updated Quick Start with Makefile-based setup instructions alongside manual setup +- **README** - Updated Quick Start with justfile-based setup instructions + +### Removed +- **Makefile** - Replaced by justfile (cross-platform, simpler syntax) --- diff --git a/Makefile b/Makefile deleted file mode 100644 index 38918613..00000000 --- a/Makefile +++ /dev/null @@ -1,250 +0,0 @@ -# Voicebox Makefile -# Unix-only (macOS/Linux). Windows users should use WSL. - -SHELL := /bin/bash -.DEFAULT_GOAL := help - -# Directories -BACKEND_DIR := backend -TAURI_DIR := tauri -WEB_DIR := web -APP_DIR := app - -# Python (prefer 3.12, fallback to 3.13, then python3) -PYTHON := $(shell command -v python3.12 2>/dev/null || command -v python3.13 2>/dev/null || echo python3) -VENV := $(CURDIR)/$(BACKEND_DIR)/venv -VENV_BIN := $(VENV)/bin -PIP := $(VENV_BIN)/pip -PYTHON_VENV := $(VENV_BIN)/python - -# Colors for output -BLUE := \033[0;34m -GREEN := \033[0;32m -YELLOW := \033[0;33m -NC := \033[0m # No Color - -.PHONY: help -help: ## Show this help message - @echo -e "$(BLUE)Voicebox$(NC) - Development Commands" - @echo "" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ - awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-20s$(NC) %s\n", $$1, $$2}' - -# ============================================================================= -# SETUP -# ============================================================================= - -.PHONY: setup setup-js setup-python setup-rust - -setup: setup-js setup-python ## Full project setup (all dependencies) - @echo -e "$(GREEN)✓ Setup complete!$(NC)" - @echo -e " Run $(YELLOW)make dev$(NC) to start development servers" - -setup-js: ## Install JavaScript dependencies (bun) - @echo -e "$(BLUE)Installing JavaScript dependencies...$(NC)" - bun install - -setup-python: $(VENV)/bin/activate ## Set up Python virtual environment and dependencies - @echo -e "$(BLUE)Installing Python dependencies...$(NC)" - $(PIP) install --upgrade pip - $(PIP) install -r $(BACKEND_DIR)/requirements.txt - $(PIP) install --no-deps chatterbox-tts - @if [ "$$(uname -m)" = "arm64" ] && [ "$$(uname)" = "Darwin" ]; then \ - echo -e "$(BLUE)Detected Apple Silicon - installing MLX dependencies...$(NC)"; \ - $(PIP) install -r $(BACKEND_DIR)/requirements-mlx.txt; \ - echo -e "$(GREEN)✓ MLX backend enabled (native Metal acceleration)$(NC)"; \ - fi - $(PIP) install git+https://github.com/QwenLM/Qwen3-TTS.git - @echo -e "$(GREEN)✓ Python environment ready$(NC)" - -$(VENV)/bin/activate: - @echo -e "$(BLUE)Creating Python virtual environment...$(NC)" - @PY_MINOR=$$($(PYTHON) -c "import sys; print(sys.version_info[1])"); \ - if [ "$$PY_MINOR" -gt 13 ]; then \ - echo -e "$(YELLOW)Warning: Python 3.$$PY_MINOR detected. ML packages may not be compatible.$(NC)"; \ - echo -e "$(YELLOW)Recommended: Use Python 3.12 or 3.13 (brew install python@3.12)$(NC)"; \ - fi - $(PYTHON) -m venv $(VENV) - -setup-rust: ## Install Rust toolchain (if not present) - @command -v rustc >/dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - -# ============================================================================= -# DEVELOPMENT -# ============================================================================= - -.PHONY: dev dev-backend dev-frontend dev-web kill-dev - -dev: ## Start backend + desktop app (parallel) - @echo -e "$(BLUE)Starting development servers...$(NC)" - @echo -e "$(YELLOW)Note: If Tauri fails, run 'make build-server' first or use separate terminals$(NC)" - @trap 'kill 0' EXIT; \ - $(MAKE) dev-backend & \ - sleep 2 && if [ "$$(uname)" = "Linux" ] && lspci 2>/dev/null | grep -qi nvidia; then \ - WEBKIT_DISABLE_DMABUF_RENDERER=1 $(MAKE) dev-frontend; \ - else \ - $(MAKE) dev-frontend; \ - fi & \ - wait - -dev-backend: ## Start FastAPI backend server - @echo -e "$(BLUE)Starting backend server on http://localhost:17493$(NC)" - $(VENV_BIN)/uvicorn backend.main:app --reload --port 17493 - -dev-frontend: ## Start Tauri desktop app - @echo -e "$(BLUE)Starting Tauri desktop app...$(NC)" - bun run dev - -dev-web: ## Start backend + web app (parallel) - @echo -e "$(BLUE)Starting web development servers...$(NC)" - @trap 'kill 0' EXIT; \ - $(MAKE) dev-backend & \ - sleep 2 && cd $(WEB_DIR) && bun run dev & \ - wait - -kill-dev: ## Kill all development processes - @echo -e "$(YELLOW)Killing development processes...$(NC)" - -pkill -f "uvicorn main:app" 2>/dev/null || true - -pkill -f "vite" 2>/dev/null || true - @echo -e "$(GREEN)✓ Processes killed$(NC)" - -# ============================================================================= -# BUILD -# ============================================================================= - -.PHONY: build build-server build-tauri build-web - -build: build-server build-tauri ## Build everything (server binary + desktop app) - @echo -e "$(GREEN)✓ Build complete!$(NC)" - -build-server: ## Build Python server binary - @echo -e "$(BLUE)Building server binary...$(NC)" - PATH="$(VENV_BIN):$$PATH" ./scripts/build-server.sh - -build-tauri: ## Build Tauri desktop app - @echo -e "$(BLUE)Building Tauri desktop app...$(NC)" - cd $(TAURI_DIR) && bun run tauri build - -build-web: ## Build web app - @echo -e "$(BLUE)Building web app...$(NC)" - cd $(WEB_DIR) && bun run build - @echo -e "$(GREEN)✓ Web build output in $(WEB_DIR)/dist/$(NC)" - -# ============================================================================= -# DATABASE & API -# ============================================================================= - -.PHONY: db-init db-reset generate-api - -db-init: $(VENV)/bin/activate ## Initialize SQLite database - @echo -e "$(BLUE)Initializing database...$(NC)" - cd $(BACKEND_DIR) && $(PYTHON_VENV) -c "from database import init_db; init_db()" - @echo -e "$(GREEN)✓ Database created at $(BACKEND_DIR)/data/voicebox.db$(NC)" - -db-reset: ## Reset database (delete and reinitialize) - @echo -e "$(YELLOW)Resetting database...$(NC)" - rm -f $(BACKEND_DIR)/data/voicebox.db - $(MAKE) db-init - -generate-api: ## Generate TypeScript API client from OpenAPI schema - @echo -e "$(BLUE)Generating API client...$(NC)" - @echo -e "$(YELLOW)Note: Backend must be running (make dev-backend)$(NC)" - ./scripts/generate-api.sh - @echo -e "$(GREEN)✓ API client generated in $(APP_DIR)/src/lib/api/$(NC)" - -# ============================================================================= -# CODE QUALITY -# ============================================================================= - -.PHONY: lint format typecheck check - -lint: ## Run linter (Biome) - @echo -e "$(BLUE)Linting...$(NC)" - bun run lint - -format: ## Format code (Biome) - @echo -e "$(BLUE)Formatting...$(NC)" - bun run format - -typecheck: ## Run TypeScript type checking - @echo -e "$(BLUE)Type checking...$(NC)" - bun run tsc --noEmit - -check: ## Run all checks (Biome lint + format + type check) - @echo -e "$(BLUE)Running all checks...$(NC)" - bun run check - @echo -e "$(GREEN)✓ All checks passed$(NC)" - -# ============================================================================= -# TESTING -# ============================================================================= - -.PHONY: test test-backend test-frontend - -test: test-backend test-frontend ## Run all tests - @echo -e "$(GREEN)✓ All tests passed$(NC)" - -test-backend: ## Run Python backend tests (requires pytest) - @echo -e "$(BLUE)Running backend tests...$(NC)" - @if [ -f "$(VENV_BIN)/pytest" ]; then \ - cd $(BACKEND_DIR) && $(VENV_BIN)/pytest -v; \ - else \ - echo -e "$(YELLOW)pytest not installed. Run: $(PIP) install pytest$(NC)"; \ - exit 1; \ - fi - -test-frontend: ## Run frontend tests (requires test script in package.json) - @echo -e "$(BLUE)Running frontend tests...$(NC)" - @if bun run test --help >/dev/null 2>&1; then \ - bun run test; \ - else \ - echo -e "$(YELLOW)No test script configured$(NC)"; \ - exit 1; \ - fi - -# ============================================================================= -# LOGS & DEBUGGING -# ============================================================================= - -.PHONY: logs docs - -logs: ## Tail backend logs - @echo -e "$(BLUE)Tailing logs (Ctrl+C to stop)...$(NC)" - tail -f $(BACKEND_DIR)/logs/*.log 2>/dev/null || echo "No log files found" - -docs: ## Open API documentation (backend must be running) - @echo -e "$(BLUE)Opening API docs...$(NC)" - open http://localhost:17493/docs 2>/dev/null || xdg-open http://localhost:17493/docs - -# ============================================================================= -# CLEAN -# ============================================================================= - -.PHONY: clean clean-python clean-build clean-all - -clean: ## Clean build artifacts - @echo -e "$(BLUE)Cleaning build artifacts...$(NC)" - rm -rf $(TAURI_DIR)/src-tauri/target/release - rm -rf $(WEB_DIR)/dist - rm -rf $(APP_DIR)/dist - @echo -e "$(GREEN)✓ Build artifacts cleaned$(NC)" - -clean-python: ## Clean Python cache and virtual environment - @echo -e "$(BLUE)Cleaning Python files...$(NC)" - rm -rf $(VENV) - find $(BACKEND_DIR) -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - find $(BACKEND_DIR) -type f -name "*.pyc" -delete 2>/dev/null || true - @echo -e "$(GREEN)✓ Python environment cleaned$(NC)" - -clean-build: ## Clean Rust/Tauri build cache - @echo -e "$(BLUE)Cleaning Rust build cache...$(NC)" - cd $(TAURI_DIR)/src-tauri && cargo clean - @echo -e "$(GREEN)✓ Rust cache cleaned$(NC)" - -clean-all: clean clean-python clean-build ## Nuclear clean (everything) - @echo -e "$(BLUE)Cleaning node_modules...$(NC)" - rm -rf node_modules - rm -rf $(APP_DIR)/node_modules - rm -rf $(TAURI_DIR)/node_modules - rm -rf $(WEB_DIR)/node_modules - @echo -e "$(GREEN)✓ Full clean complete$(NC)" diff --git a/PATCH_NOTES.md b/PATCH_NOTES.md index e5c08175..2e0c983e 100644 --- a/PATCH_NOTES.md +++ b/PATCH_NOTES.md @@ -31,7 +31,7 @@ Two-part fix: ## Testing To test this fix: -1. Build Voicebox from source: `make build` +1. Build Voicebox from source: `just build` 2. Disconnect from internet 3. Try generating speech 4. Should work without network requests @@ -40,13 +40,13 @@ To test this fix: ```bash # Install dependencies -pip install -r requirements.txt +just setup # Build the app -make build +just build # Or build just the server -make build-server +just build-server ``` ## Notes diff --git a/app/src/components/Generation/EngineModelSelector.tsx b/app/src/components/Generation/EngineModelSelector.tsx new file mode 100644 index 00000000..77dac03f --- /dev/null +++ b/app/src/components/Generation/EngineModelSelector.tsx @@ -0,0 +1,103 @@ +import type { UseFormReturn } from 'react-hook-form'; +import { FormControl } from '@/components/ui/form'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { getLanguageOptionsForEngine } from '@/lib/constants/languages'; +import type { GenerationFormValues } from '@/lib/hooks/useGenerationForm'; + +/** + * Engine/model options and their display metadata. + * Adding a new engine means adding one entry here. + */ +const ENGINE_OPTIONS = [ + { value: 'qwen:1.7B', label: 'Qwen3-TTS 1.7B' }, + { value: 'qwen:0.6B', label: 'Qwen3-TTS 0.6B' }, + { value: 'luxtts', label: 'LuxTTS' }, + { value: 'chatterbox', label: 'Chatterbox' }, + { value: 'chatterbox_turbo', label: 'Chatterbox Turbo' }, +] as const; + +const ENGINE_DESCRIPTIONS: Record = { + qwen: 'Multi-language, two sizes', + luxtts: 'Fast, English-focused', + chatterbox: '23 languages, incl. Hebrew', + chatterbox_turbo: 'English, [laugh] [cough] tags', +}; + +/** Engines that only support English and should force language to 'en' on select. */ +const ENGLISH_ONLY_ENGINES = new Set(['luxtts', 'chatterbox_turbo']); + +function getSelectValue(engine: string, modelSize?: string): string { + if (engine === 'qwen') return `qwen:${modelSize || '1.7B'}`; + return engine; +} + +function handleEngineChange(form: UseFormReturn, value: string) { + if (value.startsWith('qwen:')) { + const [, modelSize] = value.split(':'); + form.setValue('engine', 'qwen'); + form.setValue('modelSize', modelSize as '1.7B' | '0.6B'); + // Validate language is supported by Qwen + const currentLang = form.getValues('language'); + const available = getLanguageOptionsForEngine('qwen'); + if (!available.some((l) => l.value === currentLang)) { + form.setValue('language', available[0]?.value ?? 'en'); + } + } else { + form.setValue('engine', value as GenerationFormValues['engine']); + form.setValue('modelSize', undefined as unknown as '1.7B' | '0.6B'); + if (ENGLISH_ONLY_ENGINES.has(value)) { + form.setValue('language', 'en'); + } else { + // If current language isn't supported by the new engine, reset to first available + const currentLang = form.getValues('language'); + const available = getLanguageOptionsForEngine(value); + if (!available.some((l) => l.value === currentLang)) { + form.setValue('language', available[0]?.value ?? 'en'); + } + } + } +} + +interface EngineModelSelectorProps { + form: UseFormReturn; + compact?: boolean; +} + +export function EngineModelSelector({ form, compact }: EngineModelSelectorProps) { + const engine = form.watch('engine') || 'qwen'; + const modelSize = form.watch('modelSize'); + const selectValue = getSelectValue(engine, modelSize); + + const itemClass = compact ? 'text-xs text-muted-foreground' : undefined; + const triggerClass = compact + ? 'h-8 text-xs bg-card border-border rounded-full hover:bg-background/50 transition-all' + : undefined; + + return ( + + ); +} + +/** Returns a human-readable description for the currently selected engine. */ +export function getEngineDescription(engine: string): string { + return ENGINE_DESCRIPTIONS[engine] ?? ''; +} diff --git a/app/src/components/Generation/FloatingGenerateBox.tsx b/app/src/components/Generation/FloatingGenerateBox.tsx index 005bcdca..96e8f553 100644 --- a/app/src/components/Generation/FloatingGenerateBox.tsx +++ b/app/src/components/Generation/FloatingGenerateBox.tsx @@ -1,8 +1,8 @@ +import { useQuery } from '@tanstack/react-query'; import { useMatchRoute } from '@tanstack/react-router'; import { AnimatePresence, motion } from 'framer-motion'; -import { Loader2, SlidersHorizontal, Sparkles } from 'lucide-react'; +import { Loader2, Sparkles } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; -import { EffectsChainEditor } from '@/components/Effects/EffectsChainEditor'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; import { @@ -13,7 +13,7 @@ import { SelectValue, } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; -import type { EffectConfig } from '@/lib/api/types'; +import { apiClient } from '@/lib/api/client'; import { getLanguageOptionsForEngine, type LanguageCode } from '@/lib/constants/languages'; import { useGenerationForm } from '@/lib/hooks/useGenerationForm'; import { useProfile, useProfiles } from '@/lib/hooks/useProfiles'; @@ -22,6 +22,7 @@ import { cn } from '@/lib/utils/cn'; import { useGenerationStore } from '@/stores/generationStore'; import { useStoryStore } from '@/stores/storyStore'; import { useUIStore } from '@/stores/uiStore'; +import { EngineModelSelector } from './EngineModelSelector'; import { ParalinguisticInput } from './ParalinguisticInput'; interface FloatingGenerateBoxProps { @@ -38,8 +39,7 @@ export function FloatingGenerateBox({ const { data: selectedProfile } = useProfile(selectedProfileId || ''); const { data: profiles } = useProfiles(); const [isExpanded, setIsExpanded] = useState(false); - const [isInstructMode, setIsInstructMode] = useState(false); - const [effectsChain, setEffectsChain] = useState([]); + const [selectedPresetId, setSelectedPresetId] = useState(null); const containerRef = useRef(null); const textareaRef = useRef(null); const matchRoute = useMatchRoute(); @@ -49,18 +49,28 @@ export function FloatingGenerateBox({ const { data: currentStory } = useStory(selectedStoryId); const addPendingStoryAdd = useGenerationStore((s) => s.addPendingStoryAdd); + // Fetch effect presets for the dropdown + const { data: effectPresets } = useQuery({ + queryKey: ['effectPresets'], + queryFn: () => apiClient.listEffectPresets(), + }); + // Calculate if track editor is visible (on stories route with items) const hasTrackEditor = isStoriesRoute && currentStory && currentStory.items.length > 0; const { form, handleSubmit, isPending } = useGenerationForm({ onSuccess: async (generationId) => { setIsExpanded(false); - // Defer the story add until TTS completes — useGenerationProgress handles it + // Defer the story add until TTS completes -- useGenerationProgress handles it if (isStoriesRoute && selectedStoryId && generationId) { addPendingStoryAdd(generationId, selectedStoryId); } }, - getEffectsChain: () => (effectsChain.length > 0 ? effectsChain : undefined), + getEffectsChain: () => { + if (!selectedPresetId || !effectPresets) return undefined; + const preset = effectPresets.find((p) => p.id === selectedPresetId); + return preset?.effects_chain; + }, }); // Click away handler to collapse the box @@ -188,111 +198,57 @@ export function FloatingGenerateBox({
- - {/* Text field - hidden when in instruct mode */} -
- ( - - - - {form.watch('engine') === 'chatterbox_turbo' ? ( - setIsExpanded(true)} - onFocus={() => setIsExpanded(true)} - /> - ) : ( -