diff --git a/app/package.json b/app/package.json index dbba8d6d..61e6d4e3 100644 --- a/app/package.json +++ b/app/package.json @@ -13,6 +13,7 @@ "check": "biome check --write src" }, "dependencies": { + "@tauri-apps/api": "^2.0.0", "react": "^18.3.0", "react-dom": "^18.3.0", "@tanstack/react-query": "^5.0.0", diff --git a/app/src/App.tsx b/app/src/App.tsx index ed912a55..52c6a18b 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { GenerationForm } from '@/components/Generation/GenerationForm'; import { HistoryTable } from '@/components/History/HistoryTable'; import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm'; @@ -7,14 +7,63 @@ import { ModelManagement } from '@/components/ServerSettings/ModelManagement'; import { Toaster } from '@/components/ui/toaster'; import { ProfileList } from '@/components/VoiceProfiles/ProfileList'; import { Sidebar } from '@/components/Sidebar'; +import { isTauri, startServer, stopServer } from '@/lib/tauri'; + +// Track if server is starting to prevent duplicate starts +let serverStarting = false; function App() { const [activeTab, setActiveTab] = useState('profiles'); + const [serverReady, setServerReady] = useState(false); + + // Auto-start server when running in Tauri + useEffect(() => { + if (!isTauri() || serverStarting) { + return; + } + + serverStarting = true; + console.log('Running in Tauri, starting bundled server...'); + + startServer(false) + .then(() => { + console.log('Server is ready'); + setServerReady(true); + }) + .catch((error) => { + console.error('Failed to auto-start server:', error); + serverStarting = false; + }); + + // Cleanup: stop server on actual unmount (not StrictMode remount) + return () => { + // In production builds, we want to stop the server on unmount + // In dev mode, React StrictMode causes remounts, so we skip cleanup + if (import.meta.env?.PROD) { + stopServer().catch((error) => { + console.error('Failed to stop server on cleanup:', error); + }); + serverStarting = false; + } + }; + }, []); + + // Show loading screen while server is starting in Tauri + if (isTauri() && !serverReady) { + return ( +
+
+
+

Starting server...

+
+
+ ); + } return (
- +
{activeTab === 'profiles' && ( diff --git a/app/src/components/History/HistoryTable.tsx b/app/src/components/History/HistoryTable.tsx index 996672fa..af9fde58 100644 --- a/app/src/components/History/HistoryTable.tsx +++ b/app/src/components/History/HistoryTable.tsx @@ -14,13 +14,11 @@ import { useToast } from '@/components/ui/use-toast'; import { apiClient } from '@/lib/api/client'; import { useDeleteGeneration, useHistory } from '@/lib/hooks/useHistory'; import { formatDate, formatDuration } from '@/lib/utils/format'; -import { useServerStore } from '@/stores/serverStore'; export function HistoryTable() { const [page, setPage] = useState(0); const limit = 20; const { toast } = useToast(); - const _serverUrl = useServerStore((state) => state.serverUrl); const { data: historyData, isLoading } = useHistory({ limit, diff --git a/app/src/components/VoiceProfiles/ProfileList.tsx b/app/src/components/VoiceProfiles/ProfileList.tsx index 06e25d6e..9c1f6109 100644 --- a/app/src/components/VoiceProfiles/ProfileList.tsx +++ b/app/src/components/VoiceProfiles/ProfileList.tsx @@ -1,14 +1,13 @@ import { Mic, Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { useDeleteProfile, useProfiles } from '@/lib/hooks/useProfiles'; +import { useProfiles } from '@/lib/hooks/useProfiles'; import { useUIStore } from '@/stores/uiStore'; import { ProfileCard } from './ProfileCard'; import { ProfileForm } from './ProfileForm'; export function ProfileList() { const { data: profiles, isLoading, error } = useProfiles(); - const _deleteProfile = useDeleteProfile(); const setDialogOpen = useUIStore((state) => state.setProfileDialogOpen); if (isLoading) { diff --git a/app/src/lib/tauri.ts b/app/src/lib/tauri.ts new file mode 100644 index 00000000..89c596d9 --- /dev/null +++ b/app/src/lib/tauri.ts @@ -0,0 +1,47 @@ +/** + * Tauri integration utilities + */ + +import { invoke } from '@tauri-apps/api/core'; + +/** + * Check if running in Tauri environment + */ +export function isTauri(): boolean { + return '__TAURI_INTERNALS__' in window; +} + +/** + * Start the bundled Python server (Tauri only) + */ +export async function startServer(remote = false): Promise { + if (!isTauri()) { + throw new Error('Not running in Tauri environment'); + } + + try { + const result = await invoke('start_server', { remote }); + console.log('Server started:', result); + return result; + } catch (error) { + console.error('Failed to start server:', error); + throw error; + } +} + +/** + * Stop the bundled Python server (Tauri only) + */ +export async function stopServer(): Promise { + if (!isTauri()) { + throw new Error('Not running in Tauri environment'); + } + + try { + await invoke('stop_server'); + console.log('Server stopped'); + } catch (error) { + console.error('Failed to stop server:', error); + throw error; + } +} diff --git a/backend/build_binary.py b/backend/build_binary.py index b9375684..6563e6dd 100644 --- a/backend/build_binary.py +++ b/backend/build_binary.py @@ -3,7 +3,6 @@ PyInstaller build script for creating standalone Python server binary. """ import PyInstaller.__main__ -import sys import os from pathlib import Path @@ -11,13 +10,30 @@ from pathlib import Path def build_server(): """Build Python server as standalone binary.""" backend_dir = Path(__file__).parent - + + # Find qwen_tts source directory (it's an editable install) + qwen_tts_path = Path('/Users/jamespine/Projects/voice/Qwen3-TTS') + # PyInstaller arguments args = [ - 'main.py', + 'server.py', # Use server.py as entry point instead of main.py '--onefile', '--name', 'voicebox-server', - '--add-data', f'utils{os.pathsep}utils', # Include utils package + '--paths', str(qwen_tts_path), # Add qwen_tts source to paths + '--hidden-import', 'backend', + '--hidden-import', 'backend.main', + '--hidden-import', 'backend.config', + '--hidden-import', 'backend.database', + '--hidden-import', 'backend.models', + '--hidden-import', 'backend.profiles', + '--hidden-import', 'backend.history', + '--hidden-import', 'backend.tts', + '--hidden-import', 'backend.transcribe', + '--hidden-import', 'backend.utils.audio', + '--hidden-import', 'backend.utils.cache', + '--hidden-import', 'backend.utils.progress', + '--hidden-import', 'backend.utils.hf_progress', + '--hidden-import', 'backend.utils.validation', '--hidden-import', 'torch', '--hidden-import', 'transformers', '--hidden-import', 'fastapi', @@ -25,7 +41,15 @@ def build_server(): '--hidden-import', 'sqlalchemy', '--hidden-import', 'librosa', '--hidden-import', 'soundfile', - '--collect-all', 'qwen-tts', + '--hidden-import', 'qwen_tts', + '--hidden-import', 'qwen_tts.inference', + '--hidden-import', 'qwen_tts.inference.qwen3_tts_model', + '--hidden-import', 'qwen_tts.inference.qwen3_tts_tokenizer', + '--hidden-import', 'qwen_tts.core', + '--hidden-import', 'qwen_tts.cli', + '--copy-metadata', 'qwen-tts', + '--collect-submodules', 'qwen_tts', + '--collect-data', 'qwen_tts', '--noconfirm', '--clean', ] diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 00000000..a4718207 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,59 @@ +""" +Configuration module for voicebox backend. + +Handles data directory configuration for production bundling. +""" + +from pathlib import Path + +# Default data directory (used in development) +_data_dir = Path("data") + +def set_data_dir(path: str | Path): + """ + Set the data directory path. + + Args: + path: Path to the data directory + """ + global _data_dir + _data_dir = Path(path) + _data_dir.mkdir(parents=True, exist_ok=True) + print(f"Data directory set to: {_data_dir.absolute()}") + +def get_data_dir() -> Path: + """ + Get the data directory path. + + Returns: + Path to the data directory + """ + return _data_dir + +def get_db_path() -> Path: + """Get database file path.""" + return _data_dir / "voicebox.db" + +def get_profiles_dir() -> Path: + """Get profiles directory path.""" + path = _data_dir / "profiles" + path.mkdir(parents=True, exist_ok=True) + return path + +def get_generations_dir() -> Path: + """Get generations directory path.""" + path = _data_dir / "generations" + path.mkdir(parents=True, exist_ok=True) + return path + +def get_cache_dir() -> Path: + """Get cache directory path.""" + path = _data_dir / "cache" + path.mkdir(parents=True, exist_ok=True) + return path + +def get_models_dir() -> Path: + """Get models directory path.""" + path = _data_dir / "models" + path.mkdir(parents=True, exist_ok=True) + return path diff --git a/backend/database.py b/backend/database.py index 658de607..2fdf9394 100644 --- a/backend/database.py +++ b/backend/database.py @@ -9,6 +9,8 @@ from datetime import datetime import uuid from pathlib import Path +from . import config + Base = declarative_base() @@ -59,20 +61,25 @@ class Project(Base): updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) -# Database setup -_db_path = Path("data/voicebox.db") -_db_path.parent.mkdir(parents=True, exist_ok=True) - -engine = create_engine( - f"sqlite:///{_db_path}", - connect_args={"check_same_thread": False}, -) - -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +# Database setup will be initialized in init_db() +engine = None +SessionLocal = None +_db_path = None def init_db(): """Initialize database tables.""" + global engine, SessionLocal, _db_path + + _db_path = config.get_db_path() + _db_path.parent.mkdir(parents=True, exist_ok=True) + + engine = create_engine( + f"sqlite:///{_db_path}", + connect_args={"check_same_thread": False}, + ) + + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base.metadata.create_all(bind=engine) diff --git a/backend/history.py b/backend/history.py index 03c35342..d981b55b 100644 --- a/backend/history.py +++ b/backend/history.py @@ -12,11 +12,12 @@ from sqlalchemy import or_ from .models import GenerationRequest, GenerationResponse, HistoryQuery, HistoryResponse, HistoryListResponse from .database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile +from . import config -# Generations storage directory -GENERATIONS_DIR = Path("data/generations") -GENERATIONS_DIR.mkdir(parents=True, exist_ok=True) +def _get_generations_dir() -> Path: + """Get generations directory from config.""" + return config.get_generations_dir() async def create_generation( diff --git a/backend/main.py b/backend/main.py index 7a56ba30..06c8298c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -17,13 +17,10 @@ import tempfile from pathlib import Path import uuid -from . import database, models, profiles, history, tts, transcribe -from .database import get_db, init_db, Generation as DBGeneration, VoiceProfile as DBVoiceProfile +from . import database, models, profiles, history, tts, transcribe, config +from .database import get_db, Generation as DBGeneration, VoiceProfile as DBVoiceProfile from .utils.progress import get_progress_manager -# Initialize database -init_db() - app = FastAPI( title="voicebox API", description="Production-quality Qwen3-TTS voice cloning API", @@ -269,7 +266,7 @@ async def generate_speech( # Save audio generation_id = str(uuid.uuid4()) - audio_path = history.GENERATIONS_DIR / f"{generation_id}.wav" + audio_path = config.get_generations_dir() / f"{generation_id}.wav" from .utils.audio import save_audio save_audio(audio, str(audio_path), sample_rate) @@ -740,10 +737,23 @@ if __name__ == "__main__": default=8000, help="Port to bind to", ) + parser.add_argument( + "--data-dir", + type=str, + default=None, + help="Data directory for database, profiles, and generated audio", + ) args = parser.parse_args() + # Set data directory if provided + if args.data_dir: + config.set_data_dir(args.data_dir) + + # Initialize database after data directory is set + database.init_db() + uvicorn.run( - "main:app", + "backend.main:app", host=args.host, port=args.port, reload=False, # Disable reload in production diff --git a/backend/profiles.py b/backend/profiles.py index 963729d2..49fcd521 100644 --- a/backend/profiles.py +++ b/backend/profiles.py @@ -22,11 +22,12 @@ from .database import ( ) from .utils.audio import validate_reference_audio, load_audio, save_audio from .tts import get_tts_model +from . import config -# Profile storage directory -PROFILES_DIR = Path("data/profiles") -PROFILES_DIR.mkdir(parents=True, exist_ok=True) +def _get_profiles_dir() -> Path: + """Get profiles directory from config.""" + return config.get_profiles_dir() async def create_profile( @@ -58,7 +59,7 @@ async def create_profile( db.refresh(db_profile) # Create profile directory - profile_dir = PROFILES_DIR / db_profile.id + profile_dir = _get_profiles_dir() / db_profile.id profile_dir.mkdir(parents=True, exist_ok=True) return VoiceProfileResponse.model_validate(db_profile) @@ -94,7 +95,7 @@ async def add_profile_sample( # Create sample ID and directory sample_id = str(uuid.uuid4()) - profile_dir = PROFILES_DIR / profile_id + profile_dir = _get_profiles_dir() / profile_id profile_dir.mkdir(parents=True, exist_ok=True) # Copy audio file to profile directory @@ -235,7 +236,7 @@ async def delete_profile( db.commit() # Delete profile directory - profile_dir = PROFILES_DIR / profile_id + profile_dir = _get_profiles_dir() / profile_id if profile_dir.exists(): shutil.rmtree(profile_dir) diff --git a/backend/server.py b/backend/server.py new file mode 100644 index 00000000..2dfb0cd9 --- /dev/null +++ b/backend/server.py @@ -0,0 +1,49 @@ +""" +Entry point for PyInstaller-bundled voicebox server. + +This module provides an entry point that works with PyInstaller by using +absolute imports instead of relative imports. +""" + +import argparse +import uvicorn + +# Import the FastAPI app from the backend package +from backend.main import app +from backend import config, database + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="voicebox backend server") + parser.add_argument( + "--host", + type=str, + default="127.0.0.1", + help="Host to bind to (use 0.0.0.0 for remote access)", + ) + parser.add_argument( + "--port", + type=int, + default=8000, + help="Port to bind to", + ) + parser.add_argument( + "--data-dir", + type=str, + default=None, + help="Data directory for database, profiles, and generated audio", + ) + args = parser.parse_args() + + # Set data directory if provided + if args.data_dir: + config.set_data_dir(args.data_dir) + + # Initialize database after data directory is set + database.init_db() + + uvicorn.run( + app, + host=args.host, + port=args.port, + log_level="info", + ) diff --git a/backend/tts.py b/backend/tts.py index 852946ae..d135bbdd 100644 --- a/backend/tts.py +++ b/backend/tts.py @@ -13,6 +13,7 @@ from .utils.cache import get_cache_key, get_cached_voice_prompt, cache_voice_pro from .utils.audio import normalize_audio from .utils.progress import get_progress_manager from .utils.hf_progress import HFProgressTracker, create_hf_progress_callback +from . import config class TTSModel: @@ -63,7 +64,7 @@ class TTSModel: raise ValueError(f"Unknown model size: {model_size}") # Check if model exists locally (backwards compatibility) - local_path = Path("data/models") / local_model_map[model_size] + local_path = config.get_models_dir() / local_model_map[model_size] if local_path.exists(): print(f"Found local model at {local_path}") return str(local_path) diff --git a/backend/utils/cache.py b/backend/utils/cache.py index 2c0ecb1c..cc2f3bd6 100644 --- a/backend/utils/cache.py +++ b/backend/utils/cache.py @@ -5,12 +5,15 @@ Voice prompt caching utilities. import hashlib import torch from pathlib import Path -from typing import Optional, Tuple -import soundfile as sf +from typing import Optional + +from .. import config -_cache_dir = Path("data/cache") -_cache_dir.mkdir(parents=True, exist_ok=True) +def _get_cache_dir() -> Path: + """Get cache directory from config.""" + return config.get_cache_dir() + # In-memory cache _memory_cache: dict[str, torch.Tensor] = {} @@ -19,21 +22,21 @@ _memory_cache: dict[str, torch.Tensor] = {} def get_cache_key(audio_path: str, reference_text: str) -> str: """ Generate cache key from audio file and reference text. - + Args: audio_path: Path to audio file reference_text: Reference text - + Returns: Cache key (MD5 hash) """ # Read audio file with open(audio_path, "rb") as f: audio_bytes = f.read() - + # Combine audio bytes and text combined = audio_bytes + reference_text.encode("utf-8") - + # Generate hash return hashlib.md5(combined).hexdigest() @@ -43,19 +46,19 @@ def get_cached_voice_prompt( ) -> Optional[torch.Tensor]: """ Get cached voice prompt if available. - + Args: cache_key: Cache key - + Returns: Cached voice prompt tensor or None """ # Check in-memory cache if cache_key in _memory_cache: return _memory_cache[cache_key] - + # Check disk cache - cache_file = _cache_dir / f"{cache_key}.prompt" + cache_file = _get_cache_dir() / f"{cache_key}.prompt" if cache_file.exists(): try: prompt = torch.load(cache_file) @@ -64,7 +67,7 @@ def get_cached_voice_prompt( except Exception: # Cache file corrupted, delete it cache_file.unlink() - + return None @@ -74,14 +77,14 @@ def cache_voice_prompt( ) -> None: """ Cache voice prompt to memory and disk. - + Args: cache_key: Cache key voice_prompt: Voice prompt tensor """ # Store in memory _memory_cache[cache_key] = voice_prompt - + # Store on disk - cache_file = _cache_dir / f"{cache_key}.prompt" + cache_file = _get_cache_dir() / f"{cache_key}.prompt" torch.save(voice_prompt, cache_file) diff --git a/backend/voicebox-server.spec b/backend/voicebox-server.spec new file mode 100644 index 00000000..85069188 --- /dev/null +++ b/backend/voicebox-server.spec @@ -0,0 +1,47 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_data_files +from PyInstaller.utils.hooks import collect_submodules +from PyInstaller.utils.hooks import copy_metadata + +datas = [] +hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli'] +datas += collect_data_files('qwen_tts') +datas += copy_metadata('qwen-tts') +hiddenimports += collect_submodules('qwen_tts') + + +a = Analysis( + ['server.py'], + pathex=['/Users/jamespine/Projects/voice/Qwen3-TTS'], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='voicebox-server', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/bun.lock b/bun.lock index 8326aeb0..4d3095be 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "@radix-ui/react-toast": "^1.2.1", "@tanstack/react-query": "^5.0.0", "@tanstack/react-query-devtools": "^5.0.0", + "@tauri-apps/api": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 9277a244..33427c7f 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -15,11 +15,34 @@ async fn start_server( state: State<'_, ServerState>, remote: Option, ) -> Result { + // Check if server is already running + if state.child.lock().unwrap().is_some() { + return Ok("Server already running on http://localhost:8000".to_string()); + } + + // Get app data directory + let data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to get app data dir: {}", e))?; + + // Ensure data directory exists + std::fs::create_dir_all(&data_dir) + .map_err(|e| format!("Failed to create data dir: {}", e))?; + let mut sidecar = app .shell() .sidecar("voicebox-server") .map_err(|e| format!("Failed to get sidecar: {}", e))?; + // Pass data directory to Python server + sidecar = sidecar.args([ + "--data-dir", + data_dir + .to_str() + .ok_or_else(|| "Invalid data dir path".to_string())?, + ]); + if remote.unwrap_or(false) { sidecar = sidecar.args(["--host", "0.0.0.0"]); } @@ -31,13 +54,61 @@ async fn start_server( // Store child process *state.child.lock().unwrap() = Some(child); - // Wait for server to be ready (listen for startup log) + // Wait for server to be ready by listening for startup log + let timeout = tokio::time::Duration::from_secs(30); + let start_time = tokio::time::Instant::now(); + + loop { + if start_time.elapsed() > timeout { + return Err("Server startup timeout".to_string()); + } + + match tokio::time::timeout(tokio::time::Duration::from_millis(100), rx.recv()).await { + Ok(Some(event)) => { + match event { + tauri_plugin_shell::process::CommandEvent::Stdout(line) => { + let line_str = String::from_utf8_lossy(&line); + println!("Server output: {}", line_str); + + if line_str.contains("Uvicorn running") || line_str.contains("Application startup complete") { + println!("Server is ready!"); + break; + } + } + tauri_plugin_shell::process::CommandEvent::Stderr(line) => { + let line_str = String::from_utf8_lossy(&line); + eprintln!("Server: {}", line_str); + + // Uvicorn logs to stderr, so check there too + if line_str.contains("Uvicorn running") || line_str.contains("Application startup complete") { + println!("Server is ready!"); + break; + } + } + _ => {} + } + } + Ok(None) => { + return Err("Server process ended unexpectedly".to_string()); + } + Err(_) => { + // Timeout on this recv, continue loop + continue; + } + } + } + + // Spawn task to continue reading output tokio::spawn(async move { while let Some(event) = rx.recv().await { - if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event { - if String::from_utf8_lossy(&line).contains("Uvicorn running") { - break; + match event { + tauri_plugin_shell::process::CommandEvent::Stdout(line) => { + println!("Server: {}", String::from_utf8_lossy(&line)); } + tauri_plugin_shell::process::CommandEvent::Stderr(line) => { + eprintln!("Server error: {}", String::from_utf8_lossy(&line)); + } + _ => {} } } }); @@ -61,11 +132,11 @@ pub fn run() { child: Mutex::new(None), }) .invoke_handler(tauri::generate_handler![start_server, stop_server]) - .setup(|app| { + .setup(|_app| { #[cfg(debug_assertions)] { // Get all windows and open devtools on the first one - if let Some((_, window)) = app.webview_windows().iter().next() { + if let Some((_, window)) = _app.webview_windows().iter().next() { window.open_devtools(); println!("Dev tools opened"); } else { diff --git a/tauri/src-tauri/tauri.conf.json b/tauri/src-tauri/tauri.conf.json index 90b37339..88270043 100644 --- a/tauri/src-tauri/tauri.conf.json +++ b/tauri/src-tauri/tauri.conf.json @@ -12,6 +12,9 @@ "bundle": { "active": true, "targets": "all", + "externalBin": [ + "binaries/voicebox-server" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/tauri/tsconfig.json b/tauri/tsconfig.json index 629bcb7f..1c148061 100644 --- a/tauri/tsconfig.json +++ b/tauri/tsconfig.json @@ -15,6 +15,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, + "types": ["vite/client"], "baseUrl": ".", "paths": { "@/*": ["../app/src/*"]