-
Notifications
You must be signed in to change notification settings - Fork 466
fix: Windows port conflict + HF_HUB_OFFLINE for offline model loading #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7a4a02a
43b4d63
16da0b0
81789e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||||||
| import React, { useState, useRef, useEffect, useCallback, Suspense, lazy } from 'react'; | ||||||||||||||||||
| import { useTranslation } from 'react-i18next'; | ||||||||||||||||||
| import './index.css'; | ||||||||||||||||||
| import { useAppStore } from './store'; | ||||||||||||||||||
| import SearchableSelect from './components/SearchableSelect'; | ||||||||||||||||||
|
|
@@ -43,7 +44,10 @@ import useProfiles from './hooks/useProfiles'; | |||||||||||||||||
| import useTTS from './hooks/useTTS'; | ||||||||||||||||||
| import useDubWorkflow from './hooks/useDubWorkflow'; | ||||||||||||||||||
|
|
||||||||||||||||||
| const LazyFallback = () => <div className="app-lazy-fallback">Loading…</div>; | ||||||||||||||||||
| const LazyFallback = () => { | ||||||||||||||||||
| const { t } = useTranslation(); | ||||||||||||||||||
| return <div className="app-lazy-fallback">{t('common.loading')}</div>; | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { Toaster, toast } from 'react-hot-toast'; | ||||||||||||||||||
| import { | ||||||||||||||||||
|
|
@@ -59,6 +63,7 @@ import { exportAction, exportReveal, exportRecord } from './api/exports'; | |||||||||||||||||
| import { isTauri, doubleClickMaximize, fileToMediaUrl, playBlobAudio, playPing } from './utils/media'; | ||||||||||||||||||
|
|
||||||||||||||||||
| function App() { | ||||||||||||||||||
| const { t } = useTranslation(); | ||||||||||||||||||
| // First-run bootstrap: Rust spawns uv sync in a background thread and | ||||||||||||||||||
| // publishes progress via the `bootstrap_status` Tauri command. Hook below | ||||||||||||||||||
| // polls every 1 s; until `ready`, we render BootstrapSplash instead of the | ||||||||||||||||||
|
|
@@ -208,7 +213,9 @@ function App() { | |||||||||||||||||
| const [isCompareModalOpen, setIsCompareModalOpen] = useState(false); | ||||||||||||||||||
| const [compareVoiceA, setCompareVoiceA] = useState(""); | ||||||||||||||||||
| const [compareVoiceB, setCompareVoiceB] = useState(""); | ||||||||||||||||||
| const [compareText, setCompareText] = useState("The quick brown fox jumps over the lazy dog, proving that this voice sounds much better."); | ||||||||||||||||||
| const [compareText, setCompareText] = useState(() => { | ||||||||||||||||||
| try { return i18n?.t?.('voice.default_test_phrase'); } catch { return "The quick brown fox jumps over the lazy dog."; } | ||||||||||||||||||
| }); | ||||||||||||||||||
|
Comment on lines
+216
to
+218
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -nP '\bi18n\?\.t\?\(' frontend/src/App.jsx
rg -nP '^import .*\\bi18n\\b' frontend/src/App.jsxRepository: debpalash/OmniVoice-Studio Length of output: 52 🏁 Script executed: #!/bin/bash
# Check if file exists and get line count
ls -la frontend/src/App.jsx 2>/dev/null || echo "File not found at frontend/src/App.jsx"
wc -l frontend/src/App.jsx 2>/dev/null || echo "Cannot count lines"Repository: debpalash/OmniVoice-Studio Length of output: 167 🏁 Script executed: #!/bin/bash
# Read lines 216-218 and surrounding context
if [ -f frontend/src/App.jsx ]; then
echo "=== Lines 210-225 ==="
sed -n '210,225p' frontend/src/App.jsx | cat -n
fiRepository: debpalash/OmniVoice-Studio Length of output: 985 🏁 Script executed: #!/bin/bash
# Search for import statements related to i18n, translation, or useTranslation
if [ -f frontend/src/App.jsx ]; then
echo "=== Import statements for i18n/translation ==="
head -50 frontend/src/App.jsx | grep -nE 'import.*i18n|import.*useTranslation|import.*translation'
fiRepository: debpalash/OmniVoice-Studio Length of output: 167 🏁 Script executed: #!/bin/bash
# Check for all imports at the top of the file
if [ -f frontend/src/App.jsx ]; then
echo "=== First 60 lines (imports section) ==="
head -60 frontend/src/App.jsx | cat -n
fiRepository: debpalash/OmniVoice-Studio Length of output: 3581 🏁 Script executed: #!/bin/bash
# Search for useTranslation usage in App.jsx
rg -n 'useTranslation\|const \{ t \}' frontend/src/App.jsx | head -20Repository: debpalash/OmniVoice-Studio Length of output: 52 Undefined Line 217 references Call Suggested fix+ const { t } = useTranslation();
+
const [compareText, setCompareText] = useState(() => {
- try { return i18n?.t?.('voice.default_test_phrase'); } catch { return "The quick brown fox jumps over the lazy dog."; }
+ return t('voice.default_test_phrase', { defaultValue: 'The quick brown fox jumps over the lazy dog.' });
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| const [compareResultA, setCompareResultA] = useState(null); | ||||||||||||||||||
| const [compareResultB, setCompareResultB] = useState(null); | ||||||||||||||||||
| const [isComparing, setIsComparing] = useState(false); | ||||||||||||||||||
|
|
@@ -384,8 +391,8 @@ function App() { | |||||||||||||||||
| const update = await check(); | ||||||||||||||||||
| if (cancelled || !update) return; | ||||||||||||||||||
| const proceed = await ask( | ||||||||||||||||||
| `A new version (${update.version}) of OmniVoice Studio is available.\n\nWhat's new:\n${update.body || '— see release notes'}\n\nDownload and install now?`, | ||||||||||||||||||
| { title: 'Update available', kind: 'info' }, | ||||||||||||||||||
| t('settings.update_available_prompt', { version: update.version, body: update.body || t('settings.see_release_notes') }), | ||||||||||||||||||
| { title: t('settings.update_available'), kind: 'info' }, | ||||||||||||||||||
| ); | ||||||||||||||||||
| if (!proceed) return; | ||||||||||||||||||
| await update.downloadAndInstall(); | ||||||||||||||||||
|
|
@@ -503,18 +510,18 @@ function App() { | |||||||||||||||||
| if (!destPath) return; // User cancelled | ||||||||||||||||||
|
|
||||||||||||||||||
| await exportAction({ source_filename: sourceIdentifier, destination_path: destPath, mode }); | ||||||||||||||||||
| toast.success(`Exported: ${fallbackName}`); | ||||||||||||||||||
| toast.success(t('settings.exported_file', { name: fallbackName })); | ||||||||||||||||||
| loadExportHistory(); | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| console.error(err); | ||||||||||||||||||
| toast.error(`Export failed: ${err?.message || err}`); | ||||||||||||||||||
| toast.error(t('settings.export_failed', { msg: err?.message || err })); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
| const revealInFolder = async (filePath) => { | ||||||||||||||||||
| try { | ||||||||||||||||||
| await exportReveal({ path: filePath }); | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| toast.error(`Could not open folder: ${err.message}`); | ||||||||||||||||||
| toast.error(t('settings.open_folder_failed', { msg: err.message })); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
| const parseFilenameFromContentDisposition = (header) => { | ||||||||||||||||||
|
|
@@ -540,29 +547,29 @@ function App() { | |||||||||||||||||
| filters: [{ name: modeGuess === 'video' ? 'Video' : 'Audio', extensions: [extGuess] }], | ||||||||||||||||||
| }); | ||||||||||||||||||
| if (!destPath) return; // user cancelled | ||||||||||||||||||
| toast.loading(`Saving ${fallbackName}...`, { id: fallbackName }); | ||||||||||||||||||
| toast.loading(t('settings.saving_file', { name: fallbackName }), { id: fallbackName }); | ||||||||||||||||||
| const sep = url.includes('?') ? '&' : '?'; | ||||||||||||||||||
| const res = await fetch(`${url}${sep}save_path=${encodeURIComponent(destPath)}`); | ||||||||||||||||||
| if (!res.ok) { | ||||||||||||||||||
| const err = await res.json().catch(() => ({})); | ||||||||||||||||||
| throw new Error(err.detail || 'Save failed'); | ||||||||||||||||||
| throw new Error(err.detail || t('settings.save_failed_generic')); | ||||||||||||||||||
| } | ||||||||||||||||||
| const data = await res.json(); | ||||||||||||||||||
| toast.success(`Saved: ${data.path}`, { id: fallbackName }); | ||||||||||||||||||
| toast.success(t('settings.saved_file', { path: data.path }), { id: fallbackName }); | ||||||||||||||||||
| try { | ||||||||||||||||||
| await exportRecord({ filename: data.display_name || fallbackName, destination_path: data.path, mode: modeGuess }); | ||||||||||||||||||
| loadExportHistory(); | ||||||||||||||||||
| } catch (err) { console.warn('exportRecord (Tauri save path) failed:', err); } | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| console.error(err); | ||||||||||||||||||
| toast.error(`Save error: ${err.message}`, { id: fallbackName }); | ||||||||||||||||||
| toast.error(t('settings.save_error', { msg: err.message }), { id: fallbackName }); | ||||||||||||||||||
| } | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Browser path: standard blob download. | ||||||||||||||||||
| try { | ||||||||||||||||||
| toast.loading(`Processing ${fallbackName}...`, { id: fallbackName }); | ||||||||||||||||||
| toast.loading(t('settings.processing_file', { name: fallbackName }), { id: fallbackName }); | ||||||||||||||||||
| const response = await fetch(url); | ||||||||||||||||||
| if (!response.ok) throw new Error("Download failed"); | ||||||||||||||||||
| const serverName = parseFilenameFromContentDisposition(response.headers.get('content-disposition')); | ||||||||||||||||||
|
|
@@ -576,14 +583,14 @@ function App() { | |||||||||||||||||
| a.click(); | ||||||||||||||||||
| document.body.removeChild(a); | ||||||||||||||||||
| URL.revokeObjectURL(localUrl); | ||||||||||||||||||
| toast.success(`Downloaded ${finalName}`, { id: fallbackName }); | ||||||||||||||||||
| toast.success(t('settings.downloaded_file', { name: finalName }), { id: fallbackName }); | ||||||||||||||||||
| try { | ||||||||||||||||||
| await exportRecord({ filename: finalName, destination_path: `~/Downloads/${finalName}`, mode: modeGuess }); | ||||||||||||||||||
| loadExportHistory(); | ||||||||||||||||||
| } catch (err) { console.warn('exportRecord (browser download path) failed:', err); } | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| console.error(err); | ||||||||||||||||||
| toast.error(`Download error: ${err.message}`, { id: fallbackName }); | ||||||||||||||||||
| toast.error(t('settings.download_error', { msg: err.message }), { id: fallbackName }); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
| // Pre-flight for audio/video exports. If any segments are at preview | ||||||||||||||||||
|
|
@@ -592,7 +599,7 @@ function App() { | |||||||||||||||||
| // artifacts. No-op when previewSegIds is empty. | ||||||||||||||||||
| const finalizeTtsBeforeExport = async () => { | ||||||||||||||||||
| if (!previewSegIds || previewSegIds.length === 0) return; | ||||||||||||||||||
| toast(`Upgrading ${previewSegIds.length} preview-quality segment${previewSegIds.length === 1 ? '' : 's'} to full quality…`, { icon: '✨' }); | ||||||||||||||||||
| toast(t('dub.upgrading_preview_segments', { n: previewSegIds.length }), { icon: '✨' }); | ||||||||||||||||||
| await handleDubGenerate({ regenOnly: previewSegIds, preview: false }); | ||||||||||||||||||
| }; | ||||||||||||||||||
| const handleDubDownload = async () => { | ||||||||||||||||||
|
|
@@ -631,10 +638,10 @@ function App() { | |||||||||||||||||
| // ═══ STUDIO PROJECT CRUD ═══ | ||||||||||||||||||
| const saveProject = async () => { | ||||||||||||||||||
| if (dubStep === 'idle') { | ||||||||||||||||||
| toast.error("Please click 'Upload & Transcribe' first so the video is processed on the server before saving."); | ||||||||||||||||||
| toast.error(t('toast.save_dub_first')); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| const name = activeProjectName || dubFilename || `Project ${new Date().toLocaleString()}`; | ||||||||||||||||||
| const name = activeProjectName || dubFilename || t('dub.untitled_project'); | ||||||||||||||||||
| const statePayload = { | ||||||||||||||||||
| name, | ||||||||||||||||||
| video_path: dubFilename || null, | ||||||||||||||||||
|
|
@@ -649,10 +656,10 @@ function App() { | |||||||||||||||||
| try { | ||||||||||||||||||
| const data = await apiSaveProject(statePayload, activeProjectId); | ||||||||||||||||||
| setActiveProject(data.id, name); | ||||||||||||||||||
| toast.success(activeProjectId ? 'Project saved' : 'Project created'); | ||||||||||||||||||
| toast.success(t('toast.project_saved')); | ||||||||||||||||||
| loadProjects(); | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| toast.error('Save failed: ' + err.message); | ||||||||||||||||||
| toast.error(t('voice.save_failed', { msg: err.message })); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -680,22 +687,22 @@ function App() { | |||||||||||||||||
| // the last generate. | ||||||||||||||||||
| setLastGenFingerprints(s.segHashes || {}); | ||||||||||||||||||
| setSpeakerClones(s.speakerClones || {}); | ||||||||||||||||||
| toast.success(`Opened: ${data.name}`); | ||||||||||||||||||
| toast.success(t('sidebar.load_profile', { name: data.name })); | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| toast.error(err.message); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const deleteProject = async (projectId, e) => { | ||||||||||||||||||
| if (e) e.stopPropagation(); | ||||||||||||||||||
| if (!(await askConfirm('Delete this project? This cannot be undone.'))) return; | ||||||||||||||||||
| if (!(await askConfirm(t('dub.delete_project_confirm')))) return; | ||||||||||||||||||
| try { | ||||||||||||||||||
| await apiDeleteProject(projectId); | ||||||||||||||||||
| if (activeProjectId === projectId) { | ||||||||||||||||||
| setActiveProject(null); | ||||||||||||||||||
| } | ||||||||||||||||||
| loadProjects(); | ||||||||||||||||||
| toast.success('Project deleted'); | ||||||||||||||||||
| toast.success(t('dub.project_deleted')); | ||||||||||||||||||
| } catch (err) { toast.error(err.message); } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -735,11 +742,11 @@ function App() { | |||||||||||||||||
|
|
||||||||||||||||||
| // Switch to studio tab | ||||||||||||||||||
| setSidebarTab('projects'); | ||||||||||||||||||
| toast.success('Restored previous generation state'); | ||||||||||||||||||
| toast.success(t('sidebar.history_restored')); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| const deleteHistory = async (id, type) => { | ||||||||||||||||||
| if (!(await askConfirm('Delete this history item?'))) return; | ||||||||||||||||||
| if (!(await askConfirm(t('sidebar.delete_history_confirm')))) return; | ||||||||||||||||||
| try { | ||||||||||||||||||
| const endpoint = type === 'dub' ? `${API}/dub/history/${id}` : `${API}/history/${id}`; | ||||||||||||||||||
| await fetch(endpoint, { method: 'DELETE' }); | ||||||||||||||||||
|
|
@@ -748,7 +755,7 @@ function App() { | |||||||||||||||||
| } else { | ||||||||||||||||||
| loadHistory(); | ||||||||||||||||||
| } | ||||||||||||||||||
| toast.success('History item deleted'); | ||||||||||||||||||
| toast.success(t('sidebar.history_item_deleted')); | ||||||||||||||||||
| } catch (err) { | ||||||||||||||||||
| toast.error(err.message); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
@@ -827,7 +834,7 @@ function App() { | |||||||||||||||||
| file={pendingTrimFile} | ||||||||||||||||||
| maxSeconds={CLONE_MAX_SECONDS} | ||||||||||||||||||
| onCancel={() => setPendingTrimFile(null)} | ||||||||||||||||||
| onConfirm={(trimmed) => { setPendingTrimFile(null); setRefAudio(trimmed); setSelectedProfile(null); toast.success('Trimmed audio loaded'); }} | ||||||||||||||||||
| onConfirm={(trimmed) => { setPendingTrimFile(null); setRefAudio(trimmed); setSelectedProfile(null); toast.success(t('voice.trimmed_audio_loaded')); }} | ||||||||||||||||||
| /> | ||||||||||||||||||
| </Suspense> | ||||||||||||||||||
| </ErrorBoundary> | ||||||||||||||||||
|
|
@@ -849,8 +856,8 @@ function App() { | |||||||||||||||||
| onFlushMemory={async (unloadModel) => { | ||||||||||||||||||
| try { | ||||||||||||||||||
| const r = await apiFlushMemory(unloadModel); | ||||||||||||||||||
| toast.success(`Flushed — RAM ${r.ram_after}G · VRAM ${r.vram_after}G${r.unloaded_model ? ' · model unloaded' : ''}`); | ||||||||||||||||||
| } catch (e) { toast.error('Flush failed: ' + e.message); } | ||||||||||||||||||
| toast.success(t('settings.flushed_memory', { ram: r.ram_after, vram: r.vram_after, unloaded: r.unloaded_model ? ` · ${t('settings.model_unloaded')}` : '' })); | ||||||||||||||||||
| } catch (e) { toast.error(t('settings.flush_failed', { msg: e.message })); } | ||||||||||||||||||
| }} | ||||||||||||||||||
| /> | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't
taskkillarbitrary listeners on this port.Line 135 force-kills whichever PID
netstatreports, but this helper never verifies that the process is actually our backend. If some other local service is bound to the configured port, startup can terminate an unrelated app instead of surfacing a port conflict. Please check the process command line/executable matches the spawned OmniVoice backend before killing it.🤖 Prompt for AI Agents