diff --git a/backend/app/main.py b/backend/app/main.py index 90d8a79..cbd993d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -68,6 +68,14 @@ async def _start_model_reaper(): except Exception: logger.exception("Model reaper encountered an error") + # Log eviction configuration at startup + logger.info( + "Model Eviction -> enabled: %s | idle_timeout: %ss | check_interval: %ss", + getattr(config, "MODEL_EVICTION_ENABLED", True), + getattr(config, "MODEL_IDLE_TIMEOUT_SECONDS", 0), + getattr(config, "MODEL_IDLE_CHECK_INTERVAL_SECONDS", 60), + ) + app.state._model_reaper_task = asyncio.create_task(reaper()) diff --git a/backend/start_server.py b/backend/start_server.py index 3b7a82c..76c899f 100644 --- a/backend/start_server.py +++ b/backend/start_server.py @@ -14,6 +14,14 @@ if __name__ == "__main__": print(f"CORS Origins: {config.CORS_ORIGINS}") print(f"Project Root: {config.PROJECT_ROOT}") print(f"Device: {config.DEVICE}") + # Idle eviction settings + print( + "Model Eviction -> enabled: {} | idle_timeout: {}s | check_interval: {}s".format( + getattr(config, "MODEL_EVICTION_ENABLED", True), + getattr(config, "MODEL_IDLE_TIMEOUT_SECONDS", 0), + getattr(config, "MODEL_IDLE_CHECK_INTERVAL_SECONDS", 60), + ) + ) uvicorn.run( "app.main:app", diff --git a/frontend/js/app.js b/frontend/js/app.js index a0d9981..18f5a88 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,6 +1,10 @@ import { getSpeakers, addSpeaker, deleteSpeaker, generateDialog } from './api.js'; import { API_BASE_URL, API_BASE_URL_FOR_FILES } from './config.js'; +// Shared per-line audio playback state to prevent overlapping playback +let currentLineAudio = null; +let currentLineAudioBtn = null; + // --- Global Inline Notification Helpers --- // const noticeEl = document.getElementById('global-notice'); const noticeContentEl = document.getElementById('global-notice-content'); @@ -399,10 +403,46 @@ async function initializeDialogEditor() { playBtn.disabled = !item.audioUrl; playBtn.onclick = () => { if (!item.audioUrl) return; - let audioUrl = item.audioUrl.startsWith('http') ? item.audioUrl : `${API_BASE_URL_FOR_FILES}${item.audioUrl}`; - // Use a shared audio element or create one per play - let audio = new window.Audio(audioUrl); - audio.play(); + const audioUrl = item.audioUrl.startsWith('http') ? item.audioUrl : `${API_BASE_URL_FOR_FILES}${item.audioUrl}`; + + // If something is already playing + if (currentLineAudio && !currentLineAudio.paused) { + if (currentLineAudioBtn === playBtn) { + // Same line: ignore click to prevent overlapping + return; + } + // Stop previous audio and re-enable its button + try { + currentLineAudio.pause(); + currentLineAudio.currentTime = 0; + } catch (e) { /* noop */ } + if (currentLineAudioBtn) { + try { currentLineAudioBtn.disabled = false; } catch (e) { /* detached */ } + } + } + + const audio = new window.Audio(audioUrl); + currentLineAudio = audio; + currentLineAudioBtn = playBtn; + // Disable this play button while playing + playBtn.disabled = true; + + const clearState = () => { + if (currentLineAudio === audio) { + currentLineAudio = null; + currentLineAudioBtn = null; + } + try { playBtn.disabled = false; } catch (e) { /* detached */ } + }; + + audio.addEventListener('ended', clearState, { once: true }); + audio.addEventListener('error', clearState, { once: true }); + + audio.play().catch(err => { + console.error('Audio play failed:', err); + clearState(); + showNotice('Could not play audio.', 'error', { timeout: 2000 }); + }); }; actionsTd.appendChild(playBtn);