fixed some UI problems and added a clear dialog button.
This commit is contained in:
parent
f095bb14e5
commit
4f47d69aaa
|
@ -22,3 +22,4 @@ backend/tts_generated_dialogs/
|
||||||
|
|
||||||
# Node.js dependencies
|
# Node.js dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.aider*
|
||||||
|
|
|
@ -2,6 +2,32 @@ import asyncio
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import gc
|
||||||
|
import os
|
||||||
|
|
||||||
|
_proc = None
|
||||||
|
try:
|
||||||
|
import psutil # type: ignore
|
||||||
|
_proc = psutil.Process(os.getpid())
|
||||||
|
except Exception:
|
||||||
|
psutil = None # type: ignore
|
||||||
|
|
||||||
|
def _rss_mb() -> float:
|
||||||
|
"""Return current process RSS in MB, or -1.0 if unavailable."""
|
||||||
|
global _proc
|
||||||
|
try:
|
||||||
|
if _proc is None and psutil is not None:
|
||||||
|
_proc = psutil.Process(os.getpid())
|
||||||
|
if _proc is not None:
|
||||||
|
return _proc.memory_info().rss / (1024 * 1024)
|
||||||
|
except Exception:
|
||||||
|
return -1.0
|
||||||
|
return -1.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
import torch # Optional; used for cache cleanup metrics
|
||||||
|
except Exception: # pragma: no cover - torch may not be present in some envs
|
||||||
|
torch = None # type: ignore
|
||||||
|
|
||||||
from app import config
|
from app import config
|
||||||
from app.services.tts_service import TTSService
|
from app.services.tts_service import TTSService
|
||||||
|
@ -47,8 +73,20 @@ class ModelManager:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
await self._ensure_service()
|
await self._ensure_service()
|
||||||
if self._service and self._service.model is None:
|
if self._service and self._service.model is None:
|
||||||
logger.info("Loading TTS model (device=%s)...", self._service.device)
|
before_mb = _rss_mb()
|
||||||
|
logger.info(
|
||||||
|
"Loading TTS model (device=%s)... (rss_before=%.1f MB)",
|
||||||
|
self._service.device,
|
||||||
|
before_mb,
|
||||||
|
)
|
||||||
self._service.load_model()
|
self._service.load_model()
|
||||||
|
after_mb = _rss_mb()
|
||||||
|
if after_mb >= 0 and before_mb >= 0:
|
||||||
|
logger.info(
|
||||||
|
"TTS model loaded (rss_after=%.1f MB, delta=%.1f MB)",
|
||||||
|
after_mb,
|
||||||
|
after_mb - before_mb,
|
||||||
|
)
|
||||||
self._last_used = time.time()
|
self._last_used = time.time()
|
||||||
|
|
||||||
async def unload(self) -> None:
|
async def unload(self) -> None:
|
||||||
|
@ -59,8 +97,39 @@ class ModelManager:
|
||||||
logger.debug("Skip unload: %d active operations", self._active)
|
logger.debug("Skip unload: %d active operations", self._active)
|
||||||
return
|
return
|
||||||
if self._service.model is not None:
|
if self._service.model is not None:
|
||||||
logger.info("Unloading idle TTS model...")
|
before_mb = _rss_mb()
|
||||||
|
logger.info(
|
||||||
|
"Unloading idle TTS model... (rss_before=%.1f MB, active=%d)",
|
||||||
|
before_mb,
|
||||||
|
self._active,
|
||||||
|
)
|
||||||
self._service.unload_model()
|
self._service.unload_model()
|
||||||
|
# Drop the service instance as well to release any lingering refs
|
||||||
|
self._service = None
|
||||||
|
# Force GC and attempt allocator cache cleanup
|
||||||
|
try:
|
||||||
|
gc.collect()
|
||||||
|
finally:
|
||||||
|
if torch is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(torch, "cuda") and torch.cuda.is_available():
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
except Exception:
|
||||||
|
logger.debug("cuda.empty_cache() failed", exc_info=True)
|
||||||
|
try:
|
||||||
|
# MPS empty_cache may exist depending on torch version
|
||||||
|
mps = getattr(torch, "mps", None)
|
||||||
|
if mps is not None and hasattr(mps, "empty_cache"):
|
||||||
|
mps.empty_cache()
|
||||||
|
except Exception:
|
||||||
|
logger.debug("mps.empty_cache() failed", exc_info=True)
|
||||||
|
after_mb = _rss_mb()
|
||||||
|
if after_mb >= 0 and before_mb >= 0:
|
||||||
|
logger.info(
|
||||||
|
"Idle unload complete (rss_after=%.1f MB, delta=%.1f MB)",
|
||||||
|
after_mb,
|
||||||
|
after_mb - before_mb,
|
||||||
|
)
|
||||||
self._last_used = time.time()
|
self._last_used = time.time()
|
||||||
|
|
||||||
async def get_service(self) -> TTSService:
|
async def get_service(self) -> TTSService:
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/antinomyhq/forge/refs/heads/main/forge.schema.json
|
||||||
|
model: qwen/qwen3-coder
|
|
@ -55,7 +55,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1100px;
|
max-width: 1280px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 18px;
|
padding: 0 18px;
|
||||||
}
|
}
|
||||||
|
@ -465,7 +465,7 @@ footer {
|
||||||
|
|
||||||
/* Inline Notification */
|
/* Inline Notification */
|
||||||
.notice {
|
.notice {
|
||||||
max-width: 1100px;
|
max-width: 1280px;
|
||||||
margin: 16px auto 0;
|
margin: 16px auto 0;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
|
@ -219,6 +219,48 @@ async function initializeDialogEditor() {
|
||||||
// Continue without speakers - they'll be loaded when needed
|
// Continue without speakers - they'll be loaded when needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- LocalStorage persistence helpers ---
|
||||||
|
const LS_KEY = 'dialogEditor.items.v1';
|
||||||
|
|
||||||
|
function saveDialogToLocalStorage() {
|
||||||
|
try {
|
||||||
|
const exportData = dialogItems.map(item => {
|
||||||
|
const obj = { type: item.type };
|
||||||
|
if (item.type === 'speech') {
|
||||||
|
obj.speaker_id = item.speaker_id;
|
||||||
|
obj.text = item.text;
|
||||||
|
if (item.exaggeration !== undefined) obj.exaggeration = item.exaggeration;
|
||||||
|
if (item.cfg_weight !== undefined) obj.cfg_weight = item.cfg_weight;
|
||||||
|
if (item.temperature !== undefined) obj.temperature = item.temperature;
|
||||||
|
if (item.audioUrl) obj.audioUrl = item.audioUrl; // keep existing audio reference if present
|
||||||
|
} else if (item.type === 'silence') {
|
||||||
|
obj.duration = item.duration;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
localStorage.setItem(LS_KEY, JSON.stringify({ items: exportData }));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to save dialog to localStorage:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDialogFromLocalStorage() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(LS_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed || !Array.isArray(parsed.items)) return;
|
||||||
|
const loaded = parsed.items.map(normalizeDialogItem);
|
||||||
|
dialogItems.splice(0, dialogItems.length, ...loaded);
|
||||||
|
console.log(`Restored ${loaded.length} dialog items from localStorage`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load dialog from localStorage:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to restore saved dialog before first render
|
||||||
|
loadDialogFromLocalStorage();
|
||||||
|
|
||||||
// Function to render the current dialogItems array to the DOM as table rows
|
// Function to render the current dialogItems array to the DOM as table rows
|
||||||
function renderDialogItems() {
|
function renderDialogItems() {
|
||||||
if (!dialogItemsContainer) return;
|
if (!dialogItemsContainer) return;
|
||||||
|
@ -251,6 +293,8 @@ async function initializeDialogEditor() {
|
||||||
});
|
});
|
||||||
speakerSelect.onchange = (e) => {
|
speakerSelect.onchange = (e) => {
|
||||||
dialogItems[index].speaker_id = e.target.value;
|
dialogItems[index].speaker_id = e.target.value;
|
||||||
|
// Persist change
|
||||||
|
saveDialogToLocalStorage();
|
||||||
};
|
};
|
||||||
speakerTd.appendChild(speakerSelect);
|
speakerTd.appendChild(speakerSelect);
|
||||||
} else {
|
} else {
|
||||||
|
@ -310,6 +354,8 @@ async function initializeDialogEditor() {
|
||||||
if (!isNaN(val) && val > 0) dialogItems[index].duration = val;
|
if (!isNaN(val) && val > 0) dialogItems[index].duration = val;
|
||||||
dialogItems[index].audioUrl = null;
|
dialogItems[index].audioUrl = null;
|
||||||
}
|
}
|
||||||
|
// Persist changes before re-render
|
||||||
|
saveDialogToLocalStorage();
|
||||||
renderDialogItems();
|
renderDialogItems();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -328,6 +374,7 @@ async function initializeDialogEditor() {
|
||||||
upBtn.onclick = () => {
|
upBtn.onclick = () => {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
[dialogItems[index - 1], dialogItems[index]] = [dialogItems[index], dialogItems[index - 1]];
|
[dialogItems[index - 1], dialogItems[index]] = [dialogItems[index], dialogItems[index - 1]];
|
||||||
|
saveDialogToLocalStorage();
|
||||||
renderDialogItems();
|
renderDialogItems();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -342,6 +389,7 @@ async function initializeDialogEditor() {
|
||||||
downBtn.onclick = () => {
|
downBtn.onclick = () => {
|
||||||
if (index < dialogItems.length - 1) {
|
if (index < dialogItems.length - 1) {
|
||||||
[dialogItems[index], dialogItems[index + 1]] = [dialogItems[index + 1], dialogItems[index]];
|
[dialogItems[index], dialogItems[index + 1]] = [dialogItems[index + 1], dialogItems[index]];
|
||||||
|
saveDialogToLocalStorage();
|
||||||
renderDialogItems();
|
renderDialogItems();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -355,6 +403,7 @@ async function initializeDialogEditor() {
|
||||||
removeBtn.title = 'Remove';
|
removeBtn.title = 'Remove';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.onclick = () => {
|
||||||
dialogItems.splice(index, 1);
|
dialogItems.splice(index, 1);
|
||||||
|
saveDialogToLocalStorage();
|
||||||
renderDialogItems();
|
renderDialogItems();
|
||||||
};
|
};
|
||||||
actionsTd.appendChild(removeBtn);
|
actionsTd.appendChild(removeBtn);
|
||||||
|
@ -381,6 +430,8 @@ async function initializeDialogEditor() {
|
||||||
if (result && result.audio_url) {
|
if (result && result.audio_url) {
|
||||||
dialogItems[index].audioUrl = result.audio_url;
|
dialogItems[index].audioUrl = result.audio_url;
|
||||||
console.log('Set audioUrl to:', result.audio_url);
|
console.log('Set audioUrl to:', result.audio_url);
|
||||||
|
// Persist newly generated audio reference
|
||||||
|
saveDialogToLocalStorage();
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid result structure:', result);
|
console.error('Invalid result structure:', result);
|
||||||
throw new Error('Invalid response: missing audio_url');
|
throw new Error('Invalid response: missing audio_url');
|
||||||
|
@ -578,6 +629,7 @@ async function initializeDialogEditor() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dialogItems.push(normalizeDialogItem({ type: 'speech', speaker_id: speakerId, text: text }));
|
dialogItems.push(normalizeDialogItem({ type: 'speech', speaker_id: speakerId, text: text }));
|
||||||
|
saveDialogToLocalStorage();
|
||||||
renderDialogItems();
|
renderDialogItems();
|
||||||
clearTempInputArea();
|
clearTempInputArea();
|
||||||
};
|
};
|
||||||
|
@ -620,6 +672,7 @@ async function initializeDialogEditor() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dialogItems.push(normalizeDialogItem({ type: 'silence', duration: duration }));
|
dialogItems.push(normalizeDialogItem({ type: 'silence', duration: duration }));
|
||||||
|
saveDialogToLocalStorage();
|
||||||
renderDialogItems();
|
renderDialogItems();
|
||||||
clearTempInputArea();
|
clearTempInputArea();
|
||||||
};
|
};
|
||||||
|
@ -819,6 +872,8 @@ async function initializeDialogEditor() {
|
||||||
|
|
||||||
// Replace current dialog
|
// Replace current dialog
|
||||||
dialogItems.splice(0, dialogItems.length, ...loadedItems);
|
dialogItems.splice(0, dialogItems.length, ...loadedItems);
|
||||||
|
// Persist loaded script
|
||||||
|
saveDialogToLocalStorage();
|
||||||
renderDialogItems();
|
renderDialogItems();
|
||||||
|
|
||||||
console.log(`Loaded ${loadedItems.length} dialog items from script`);
|
console.log(`Loaded ${loadedItems.length} dialog items from script`);
|
||||||
|
@ -898,6 +953,40 @@ async function initializeDialogEditor() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Clear Dialog Button ---
|
||||||
|
// Prefer an existing button with id if present; otherwise, create and insert beside Save/Load.
|
||||||
|
let clearDialogBtn = document.getElementById('clear-dialog-btn');
|
||||||
|
if (!clearDialogBtn) {
|
||||||
|
clearDialogBtn = document.createElement('button');
|
||||||
|
clearDialogBtn.id = 'clear-dialog-btn';
|
||||||
|
clearDialogBtn.textContent = 'Clear Dialog';
|
||||||
|
// Insert next to Save/Load if possible
|
||||||
|
const saveLoadContainer = saveScriptBtn ? saveScriptBtn.parentElement : null;
|
||||||
|
if (saveLoadContainer) {
|
||||||
|
saveLoadContainer.appendChild(clearDialogBtn);
|
||||||
|
} else {
|
||||||
|
// Fallback: append near the add buttons container
|
||||||
|
const addBtnsContainer = addSpeechLineBtn ? addSpeechLineBtn.parentElement : null;
|
||||||
|
if (addBtnsContainer) addBtnsContainer.appendChild(clearDialogBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDialogBtn.addEventListener('click', async () => {
|
||||||
|
if (dialogItems.length === 0) {
|
||||||
|
showNotice('Dialog is already empty.', 'info', { timeout: 2500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = await confirmAction(`This will remove ${dialogItems.length} dialog item(s). Continue?`);
|
||||||
|
if (!ok) return;
|
||||||
|
// Clear any transient input UI
|
||||||
|
if (typeof clearTempInputArea === 'function') clearTempInputArea();
|
||||||
|
// Clear state and persistence
|
||||||
|
dialogItems.splice(0, dialogItems.length);
|
||||||
|
try { localStorage.removeItem(LS_KEY); } catch (e) { /* ignore */ }
|
||||||
|
renderDialogItems();
|
||||||
|
showNotice('Dialog cleared.', 'success', { timeout: 2500 });
|
||||||
|
});
|
||||||
|
|
||||||
console.log('Dialog Editor Initialized');
|
console.log('Dialog Editor Initialized');
|
||||||
renderDialogItems(); // Initial render (empty)
|
renderDialogItems(); // Initial render (empty)
|
||||||
|
|
||||||
|
@ -944,6 +1033,8 @@ async function initializeDialogEditor() {
|
||||||
dialogItems[index].audioUrl = null;
|
dialogItems[index].audioUrl = null;
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
|
// Persist settings change
|
||||||
|
saveDialogToLocalStorage();
|
||||||
renderDialogItems(); // Re-render to reflect changes
|
renderDialogItems(); // Re-render to reflect changes
|
||||||
console.log('TTS settings updated for item:', dialogItems[index]);
|
console.log('TTS settings updated for item:', dialogItems[index]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,3 +31,6 @@ dd3552d9-f4e8-49ed-9892-f9e67afcf23c:
|
||||||
3d3e85db-3d67-4488-94b2-ffc189fbb287:
|
3d3e85db-3d67-4488-94b2-ffc189fbb287:
|
||||||
name: RCB
|
name: RCB
|
||||||
sample_path: speaker_samples/3d3e85db-3d67-4488-94b2-ffc189fbb287.wav
|
sample_path: speaker_samples/3d3e85db-3d67-4488-94b2-ffc189fbb287.wav
|
||||||
|
f754cf35-892c-49b6-822a-f2e37246623b:
|
||||||
|
name: Jim
|
||||||
|
sample_path: speaker_samples/f754cf35-892c-49b6-822a-f2e37246623b.wav
|
||||||
|
|
Loading…
Reference in New Issue