From 28a24da0c1cbf73c330fbde1bc232c2fe046ee64 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Sun, 20 Sep 2026 12:35:13 +0800 Subject: [PATCH] feat(audio): complete audio sidebar, audio vault gallery, and domain navigation --- ARCHITECTURE.md | 8 +- api/__init__.py | 2 + api/audio_catalog.py | 91 ++++++- web/modules/browser.js | 34 ++- web/modules/locales.js | 42 +++- web/modules/ui_audio_gallery.js | 359 +++++++++++++++++++++++++++ web/modules/ui_audio_sidebar.js | 163 ++++++++++++ web/modules/ui_audio_studio.js | 308 ++++++++++++----------- web/modules/ui_browser_navigation.js | 1 + web/modules/ui_sidebar.js | 73 +++++- web/styles.css | 1 + web/styles/11-audio-studio.css | 217 ++++++++++++++++ 12 files changed, 1139 insertions(+), 160 deletions(-) create mode 100644 web/modules/ui_audio_gallery.js create mode 100644 web/modules/ui_audio_sidebar.js create mode 100644 web/styles/11-audio-studio.css diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1b5769f..4be92aa 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -84,7 +84,7 @@ DOM or live LiteGraph state. `api/folder_types.py` own the formerly mixed utility route families. - `model_policies.py` owns shared backend rename and protected-category policy. - `model_identity.py` owns file SHA-256 evidence shared with the standalone scanner. -- `api/audio_catalog.py` owns audio preset scanning, voice character grouping, emotion slice indexing, and streaming. +- `api/audio_catalog.py` owns audio preset scanning, voice character grouping, emotion slice indexing, audio streaming, and audio gallery output history listing and deletion. ### Frontend @@ -93,8 +93,10 @@ DOM or live LiteGraph state. browser instance plus floating/topbar/menu entry behavior; and `interface_settings.js` owns language and theme preferences. - `ui_domain_switcher.js` owns the dual-domain mode toggle (Visual Studio 🎨 vs Audio & Voice Studio 🎙️) embedded in the brand header. -- `ui_audio_studio.js` owns the Audio & Voice Studio workspace, presenting character voice cards, emotion slice playback, prompt syntax copying, and canvas drag-and-drop. -- `ui_sidebar.js` creates the browser shell and folder navigation. +- `ui_audio_studio.js` owns the Audio & Voice Studio workspace, presenting character voice cards, emotion slice playback, prompt syntax copying, real-time search, animated equalizers, and canvas drag-and-drop. +- `ui_audio_sidebar.js` owns the Audio Sidebar navigation, rendering trees for All Voices, Character Presets (e.g. Arona) with count badges, and Output History Vault. +- `ui_audio_gallery.js` owns the Generated Audio Vault, supporting track playback, scrubber progress, duration timecodes, audio download, deletion, and canvas drag-and-drop. +- `ui_sidebar.js` creates the browser shell, domain-aware header tabs (`[音色预设]`, `[音频库]`, `[工作流]`), domain-aware bottom-left tool suppression, and folder navigation. `ui_settings_hub.js` owns settings and model-card preferences; `ui_toolbox.js` owns the tool catalog, fixed shortcut bar, and tool dispatch; `ui_browser_navigation.js` owns shared panel hiding/cleanup and workspace return. diff --git a/api/__init__.py b/api/__init__.py index 113732b..fd0e758 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -88,3 +88,5 @@ def setup_routes(app): # Audio & Voice Studio Routes app.router.add_get('/anomalous/audio_voices', audio_catalog.api_get_audio_voices) app.router.add_get('/anomalous/audio_stream', audio_catalog.api_serve_audio) + app.router.add_get('/anomalous/audio_gallery', audio_catalog.api_get_audio_gallery) + app.router.add_post('/anomalous/delete_audio_gallery', audio_catalog.api_delete_audio_gallery) diff --git a/api/audio_catalog.py b/api/audio_catalog.py index 410ccde..701f315 100644 --- a/api/audio_catalog.py +++ b/api/audio_catalog.py @@ -118,14 +118,15 @@ async def api_get_audio_voices(request): async def api_serve_audio(request): - """GET /anomalous/audio_stream?path=... - Stream audio file securely.""" + """GET /anomalous/audio_stream?path=...&type=input|output - Stream audio file securely.""" rel_path = request.query.get("path", "").strip("/\\") + dir_type = request.query.get("type", "input") if not rel_path: return web.Response(status=400, text="Missing audio path") - input_dir = folder_paths.get_input_directory() + base_dir = folder_paths.get_output_directory() if dir_type == "output" else folder_paths.get_input_directory() try: - resolved_path = resolve_within(input_dir, rel_path) + resolved_path = resolve_within(base_dir, rel_path) except (ValueError, Exception): return web.Response(status=403, text="Forbidden path") @@ -141,8 +142,90 @@ async def api_serve_audio(request): '.m4a': 'audio/mp4' } content_type = content_types.get(ext, 'application/octet-stream') - return web.FileResponse( resolved_path, headers={"Content-Type": content_type, "Accept-Ranges": "bytes"} ) + + +def _collect_gallery_audios(output_dir): + """Scan output folder for generated audio files.""" + audios = [] + if not os.path.isdir(output_dir): + return audios + + for root, _, files in os.walk(output_dir): + for name in files: + ext = os.path.splitext(name)[1].lower() + if ext not in AUDIO_EXTENSIONS: + continue + full_path = os.path.join(root, name) + try: + stat = os.stat(full_path) + mtime = stat.st_mtime + size = stat.st_size + except OSError: + continue + + subfolder = os.path.relpath(root, output_dir) + clean_sub = "" if subfolder == "." else subfolder.replace(os.sep, '/') + rel_url_path = os.path.join(clean_sub, name).replace("\\", "/") + + audios.append({ + "filename": name, + "subfolder": clean_sub, + "size_bytes": size, + "mtime": mtime, + "audio_url": f"/anomalous/audio_stream?path={rel_url_path}&type=output" + }) + audios.sort(key=lambda a: a["mtime"], reverse=True) + return audios + + +async def api_get_audio_gallery(request): + """GET /anomalous/audio_gallery?page=1&limit=50 - List generated audio history.""" + output_dir = folder_paths.get_output_directory() + page = max(1, int(request.query.get("page", 1))) + limit = max(1, min(100, int(request.query.get("limit", 40)))) + + all_audios = _collect_gallery_audios(output_dir) + total = len(all_audios) + start = (page - 1) * limit + sliced = all_audios[start:start + limit] + + return web.json_response({ + "success": True, + "audios": sliced, + "total": total, + "page": page, + "has_more": start + limit < total + }) + + +async def api_delete_audio_gallery(request): + """POST /anomalous/delete_audio_gallery - Delete a generated audio file.""" + try: + data = await request.json() + except Exception: + return web.Response(status=400, text="Invalid JSON payload") + + filename = data.get("filename", "").strip() + subfolder = data.get("subfolder", "").strip() + if not filename: + return web.Response(status=400, text="Missing filename") + + output_dir = folder_paths.get_output_directory() + try: + rel = os.path.join(subfolder, filename).replace("\\", "/") if subfolder else filename + target = resolve_within(output_dir, rel) + except Exception: + return web.Response(status=403, text="Forbidden path") + + if os.path.isfile(target): + try: + os.remove(target) + return web.json_response({"success": True}) + except OSError as e: + return web.Response(status=500, text=f"Failed to delete: {e}") + return web.Response(status=404, text="File not found") + diff --git a/web/modules/browser.js b/web/modules/browser.js index 76081b8..15c6a0d 100644 --- a/web/modules/browser.js +++ b/web/modules/browser.js @@ -21,7 +21,8 @@ import { showImageWorkbench } from './ui_gallery_detail.js'; import { initDoctorPanel, diagnoseNode, renderGlobalDashboard, openLoraInsertionPicker, runGlobalDoctorScan } from './ui_doctor.js'; import { initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory } from './ui_node_assistant.js'; import { _openGalleryReplacer } from './ui_node_model_picker.js'; -import { renderAudioStudio } from './ui_audio_studio.js'; +import { renderAudioStudio, stopAudioStudioPlayback } from './ui_audio_studio.js'; +import { renderAudioGallery, stopGalleryAudio } from './ui_audio_gallery.js'; import { getActiveDomain } from './ui_domain_switcher.js'; export class AnomalousBrowser { @@ -50,6 +51,7 @@ export class AnomalousBrowser { } this.setTriggerVisible(false); this.modal.classList.add('visible'); + this.updateHeaderTabs?.(getActiveDomain()); if (getActiveDomain() === 'audio') { this.handleDomainChange('audio'); return; @@ -62,17 +64,22 @@ export class AnomalousBrowser { } handleDomainChange(domain) { + this.updateHeaderTabs?.(domain); if (domain === 'audio') { this.hideAllPanels(); + if (this.sidebarActions) this.sidebarActions.style.display = 'none'; + if (this.modelsBtn) this.setActiveHeaderTab?.(this.modelsBtn); if (this.audioStudioPanel) { this.audioStudioPanel.style.display = 'block'; renderAudioStudio(this.audioStudioPanel); } + this.renderSidebar(); } else { - if (this.audioStudioPanel) { - this.audioStudioPanel.style.display = 'none'; - } + if (this.sidebarActions) this.sidebarActions.style.display = 'flex'; + if (this.modelsBtn) this.setActiveHeaderTab?.(this.modelsBtn); + this.hideAllPanels(); this.grid.style.display = 'grid'; + this.renderSidebar(); if (!this.foldersData) { this.loadFolders(); } else { @@ -81,10 +88,29 @@ export class AnomalousBrowser { } } + switchAudioTab(tabName, filter = null) { + this.hideAllPanels(); + if (tabName === 'gallery') { + if (this.galleryBtn) this.setActiveHeaderTab?.(this.galleryBtn); + if (this.audioGalleryPanel) { + this.audioGalleryPanel.style.display = 'block'; + renderAudioGallery(this.audioGalleryPanel); + } + } else { + if (this.modelsBtn) this.setActiveHeaderTab?.(this.modelsBtn); + if (this.audioStudioPanel) { + this.audioStudioPanel.style.display = 'block'; + renderAudioStudio(this.audioStudioPanel, filter); + } + } + } + close() { closeUpdateGuide(this); this.modal.classList.remove('visible'); this.setTriggerVisible(true); + if (typeof stopAudioStudioPlayback === 'function') stopAudioStudioPlayback(); + if (typeof stopGalleryAudio === 'function') stopGalleryAudio(); const canvas = document.getElementById('graph-canvas'); if (canvas instanceof HTMLElement) canvas.focus({ preventScroll: true }); if (this._modelLoadController) this._modelLoadController.abort(); diff --git a/web/modules/locales.js b/web/modules/locales.js index 4f3556b..4b185b1 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -1255,7 +1255,26 @@ export const i18n = { audioCopied: "已复制语法", audioDragHint: "按住卡片可直接拖拽至画布的 F5TTS 节点", audioEmptyTitle: "暂无音频音色切片", - audioEmptyDesc: "将 5~10 秒的参考音频 (wav/mp3) 和同名文本 (txt) 放入 input/F5-TTS 目录,即可在此管理与试听。" + audioEmptyDesc: "将 5~10 秒的参考音频 (wav/mp3) 和同名文本 (txt) 放入 input/F5-TTS 目录,即可在此管理与试听。", + audioSidebarTitle: "音色分类", + audioRefresh: "刷新音色", + audioAllVoices: "全部音色", + audioSectionCharacters: "角色预设", + audioSectionHistory: "历史归档", + audioGalleryTab: "音频历史库", + audioGalleryTitle: "已生成音频归档", + audioGallerySubtitle: "浏览、试听与管理所有生成的配音文件", + audioGalleryEmpty: "暂无生成的音频文件", + audioDeleteConfirm: "确定要永久删除此音频文件吗?", + audioDeleteSuccess: "已删除音频文件", + audioDownload: "下载音频", + audioDelete: "删除", + audioFileSize: "文件大小", + audioDuration: "时长", + audioTime: "生成时间", + audioTabPresets: "音色预设", + audioTabGallery: "音频库", + audioTabWorkflow: "音频工作流" }, en: { actionTools: "Tools", @@ -2513,7 +2532,26 @@ export const i18n = { audioCopied: "Tag Copied!", audioDragHint: "Drag and drop directly onto F5TTS node on canvas", audioEmptyTitle: "No Voice Presets Found", - audioEmptyDesc: "Place 5-10s reference audios (.wav/.mp3) and matching .txt into input/F5-TTS folder to preview here." + audioEmptyDesc: "Place 5-10s reference audios (.wav/.mp3) and matching .txt into input/F5-TTS folder to preview here.", + audioSidebarTitle: "Voice Categories", + audioRefresh: "Refresh Voices", + audioAllVoices: "All Voices", + audioSectionCharacters: "Characters", + audioSectionHistory: "History", + audioGalleryTab: "Audio Gallery", + audioGalleryTitle: "Generated Audio Vault", + audioGallerySubtitle: "Browse, playback, and manage generated voice files", + audioGalleryEmpty: "No generated audio files found", + audioDeleteConfirm: "Permanently delete this audio file?", + audioDeleteSuccess: "Audio file deleted", + audioDownload: "Download", + audioDelete: "Delete", + audioFileSize: "Size", + audioDuration: "Duration", + audioTime: "Created At", + audioTabPresets: "Voice Presets", + audioTabGallery: "Audio Gallery", + audioTabWorkflow: "Audio Workflows" } }; diff --git a/web/modules/ui_audio_gallery.js b/web/modules/ui_audio_gallery.js new file mode 100644 index 0000000..733faee --- /dev/null +++ b/web/modules/ui_audio_gallery.js @@ -0,0 +1,359 @@ +import { t } from './interface_settings.js'; + +/** + * Audio Gallery Module (Output History & Management) + * Sleek, DAW-inspired player and track management for ComfyUI generated audio. + */ + +let activeAudio = null; +let activePlayBtn = null; +let activeProgressFill = null; +let activeTimeLabel = null; +let progressUpdateTimer = null; + +const SVG = { + PLAY: ``, + PAUSE: ``, + DOWNLOAD: ``, + TRASH: ``, + VAULT: ``, + AUDIO_WAVE: `` +}; + +export function stopGalleryAudio() { + if (progressUpdateTimer) { + clearInterval(progressUpdateTimer); + progressUpdateTimer = null; + } + if (activeAudio) { + activeAudio.pause(); + activeAudio = null; + } + if (activePlayBtn) { + activePlayBtn.innerHTML = SVG.PLAY; + activePlayBtn.classList.remove('is-playing'); + activePlayBtn = null; + } + if (activeProgressFill) { + activeProgressFill.style.width = '0%'; + activeProgressFill = null; + } + if (activeTimeLabel) { + activeTimeLabel.textContent = '--:--'; + activeTimeLabel = null; + } +} + +function formatDuration(sec) { + if (!sec || isNaN(sec)) return '00:00'; + const m = Math.floor(sec / 60); + const s = Math.floor(sec % 60); + return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; +} + +function formatBytes(bytes) { + if (!bytes || bytes <= 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +} + +function formatDate(timestampSec) { + if (!timestampSec) return ''; + const d = new Date(timestampSec * 1000); + const pad = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +function startPlayback(url, playBtn, fillEl, timeEl) { + if (activePlayBtn === playBtn) { + stopGalleryAudio(); + return; + } + stopGalleryAudio(); + + const audio = new Audio(url); + activeAudio = audio; + activePlayBtn = playBtn; + activeProgressFill = fillEl; + activeTimeLabel = timeEl; + + playBtn.innerHTML = SVG.PAUSE; + playBtn.classList.add('is-playing'); + + audio.onloadedmetadata = () => { + if (timeEl) timeEl.textContent = `00:00 / ${formatDuration(audio.duration)}`; + }; + + progressUpdateTimer = setInterval(() => { + if (!activeAudio || !activeAudio.duration) return; + const pct = (activeAudio.currentTime / activeAudio.duration) * 100; + if (activeProgressFill) activeProgressFill.style.width = `${pct}%`; + if (activeTimeLabel) { + activeTimeLabel.textContent = `${formatDuration(activeAudio.currentTime)} / ${formatDuration(activeAudio.duration)}`; + } + }, 100); + + audio.onended = () => stopGalleryAudio(); + audio.onerror = () => stopGalleryAudio(); + audio.play().catch(() => stopGalleryAudio()); +} + +async function requestDeleteTrack(item, rowEl, onDeleted) { + const ok = window.confirm(`${t('audioDeleteConfirm')}\n${item.filename}`); + if (!ok) return; + + try { + const resp = await fetch('/anomalous/delete_audio_gallery', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: item.filename, subfolder: item.subfolder }) + }); + const res = await resp.json(); + if (res.success) { + if (activeAudio && activeAudio.src.includes(item.filename)) { + stopGalleryAudio(); + } + rowEl.style.opacity = '0'; + rowEl.style.transform = 'translateX(20px)'; + setTimeout(() => { + rowEl.remove(); + if (typeof onDeleted === 'function') onDeleted(); + }, 200); + } else { + alert('Failed to delete: ' + (res.error || 'Unknown error')); + } + } catch (e) { + alert('Network error while deleting audio'); + } +} + +function createTrackRow(item, onDeleted) { + const row = document.createElement('div'); + row.className = 'anomalous-audio-track-item'; + row.draggable = true; + + row.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('text/plain', item.audio_url); + e.dataTransfer.setData('application/json', JSON.stringify(item)); + }); + + // Play/Pause button + const playBtn = document.createElement('button'); + playBtn.type = 'button'; + playBtn.className = 'anomalous-audio-play-btn'; + playBtn.innerHTML = SVG.PLAY; + + // Track Meta & Name + const infoCol = document.createElement('div'); + infoCol.style.display = 'flex'; + infoCol.style.flexDirection = 'column'; + infoCol.style.gap = '2px'; + infoCol.style.minWidth = '220px'; + infoCol.style.maxWidth = '320px'; + + const nameEl = document.createElement('span'); + nameEl.style.fontSize = '13px'; + nameEl.style.fontWeight = '500'; + nameEl.style.color = '#f1f5f9'; + nameEl.style.whiteSpace = 'nowrap'; + nameEl.style.overflow = 'hidden'; + nameEl.style.textOverflow = 'ellipsis'; + nameEl.textContent = item.filename; + nameEl.title = item.filename; + + const subMeta = document.createElement('div'); + subMeta.style.display = 'flex'; + subMeta.style.alignItems = 'center'; + subMeta.style.gap = '8px'; + subMeta.style.fontSize = '11px'; + subMeta.style.color = '#64748b'; + + if (item.subfolder) { + const folderTag = document.createElement('span'); + folderTag.style.background = 'rgba(99, 102, 241, 0.12)'; + folderTag.style.color = '#818cf8'; + folderTag.style.padding = '1px 5px'; + folderTag.style.borderRadius = '3px'; + folderTag.textContent = item.subfolder; + subMeta.appendChild(folderTag); + } + const sizeSpan = document.createElement('span'); + sizeSpan.textContent = formatBytes(item.size_bytes); + const dateSpan = document.createElement('span'); + dateSpan.textContent = formatDate(item.mtime); + subMeta.appendChild(sizeSpan); + subMeta.appendChild(dateSpan); + + infoCol.appendChild(nameEl); + infoCol.appendChild(subMeta); + + // Progress bar + const progressTrack = document.createElement('div'); + progressTrack.className = 'anomalous-track-progress-bar'; + const progressFill = document.createElement('div'); + progressFill.className = 'anomalous-track-progress-fill'; + progressTrack.appendChild(progressFill); + + progressTrack.onclick = (e) => { + if (!activeAudio || activePlayBtn !== playBtn) return; + const rect = progressTrack.getBoundingClientRect(); + const clickRatio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + if (activeAudio.duration) { + activeAudio.currentTime = clickRatio * activeAudio.duration; + } + }; + + // Time Label + const timeLabel = document.createElement('span'); + timeLabel.style.fontSize = '11px'; + timeLabel.style.color = '#94a3b8'; + timeLabel.style.fontFamily = 'monospace'; + timeLabel.style.minWidth = '90px'; + timeLabel.style.textAlign = 'right'; + timeLabel.textContent = '--:--'; + + playBtn.onclick = (e) => { + e.stopPropagation(); + startPlayback(item.audio_url, playBtn, progressFill, timeLabel); + }; + + // Actions + const actionGroup = document.createElement('div'); + actionGroup.style.display = 'flex'; + actionGroup.style.alignItems = 'center'; + actionGroup.style.gap = '6px'; + + const dlBtn = document.createElement('a'); + dlBtn.href = item.audio_url; + dlBtn.download = item.filename; + dlBtn.className = 'anomalous-audio-play-btn'; + dlBtn.innerHTML = SVG.DOWNLOAD; + dlBtn.title = t('audioDownload'); + dlBtn.style.textDecoration = 'none'; + + const delBtn = document.createElement('button'); + delBtn.type = 'button'; + delBtn.className = 'anomalous-audio-play-btn'; + delBtn.innerHTML = SVG.TRASH; + delBtn.title = t('audioDelete'); + delBtn.onmouseenter = () => { delBtn.style.color = '#f87171'; delBtn.style.borderColor = 'rgba(248, 113, 113, 0.4)'; }; + delBtn.onmouseleave = () => { delBtn.style.color = ''; delBtn.style.borderColor = ''; }; + delBtn.onclick = (e) => { + e.stopPropagation(); + requestDeleteTrack(item, row, onDeleted); + }; + + actionGroup.appendChild(dlBtn); + actionGroup.appendChild(delBtn); + + row.appendChild(playBtn); + row.appendChild(infoCol); + row.appendChild(progressTrack); + row.appendChild(timeLabel); + row.appendChild(actionGroup); + return row; +} + +function renderGalleryToolbar(totalCount, onRefresh) { + const bar = document.createElement('div'); + bar.className = 'anomalous-audio-toolbar'; + + const left = document.createElement('div'); + left.className = 'anomalous-audio-title-group'; + + const iconBox = document.createElement('div'); + iconBox.className = 'anomalous-audio-icon-box'; + iconBox.innerHTML = SVG.AUDIO_WAVE; + + const titleText = document.createElement('span'); + titleText.className = 'anomalous-audio-title-text'; + titleText.textContent = t('audioGalleryTitle'); + + const subText = document.createElement('span'); + subText.className = 'anomalous-audio-sub-text'; + subText.textContent = t('audioGallerySubtitle'); + + left.appendChild(iconBox); + left.appendChild(titleText); + left.appendChild(subText); + + const right = document.createElement('div'); + right.style.display = 'flex'; + right.style.alignItems = 'center'; + right.style.gap = '10px'; + + const countBadge = document.createElement('span'); + countBadge.style.fontSize = '11px'; + countBadge.style.color = '#94a3b8'; + countBadge.style.background = 'rgba(255, 255, 255, 0.05)'; + countBadge.style.padding = '3px 8px'; + countBadge.style.borderRadius = '12px'; + countBadge.textContent = `${totalCount} tracks`; + + const refreshBtn = document.createElement('button'); + refreshBtn.type = 'button'; + refreshBtn.className = 'anomalous-audio-play-btn'; + refreshBtn.innerHTML = '🔄'; + refreshBtn.title = t('audioRefresh'); + refreshBtn.onclick = onRefresh; + + right.appendChild(countBadge); + right.appendChild(refreshBtn); + + bar.appendChild(left); + bar.appendChild(right); + return bar; +} + +export async function renderAudioGallery(container) { + stopGalleryAudio(); + container.innerHTML = ''; + + const wrapper = document.createElement('div'); + wrapper.className = 'anomalous-audio-gallery-wrapper'; + + try { + const resp = await fetch('/anomalous/audio_gallery?page=1&limit=100'); + const data = await resp.json(); + const audios = data.audios || []; + + const toolbar = renderGalleryToolbar(audios.length, () => renderAudioGallery(container)); + wrapper.appendChild(toolbar); + + if (audios.length === 0) { + const emptyEl = document.createElement('div'); + emptyEl.style.padding = '80px 20px'; + emptyEl.style.textAlign = 'center'; + emptyEl.style.color = '#64748b'; + emptyEl.innerHTML = ` +
🎵
+
${t('audioGalleryEmpty')}
+
${t('audioStudioSubtitle')}
+ `; + wrapper.appendChild(emptyEl); + } else { + const trackList = document.createElement('div'); + trackList.style.display = 'flex'; + trackList.style.flexDirection = 'column'; + trackList.style.gap = '8px'; + + audios.forEach(item => { + const row = createTrackRow(item, () => { + const badge = toolbar.querySelector('span'); + if (badge) { + const cur = parseInt(badge.textContent) || 0; + badge.textContent = `${Math.max(0, cur - 1)} tracks`; + } + }); + trackList.appendChild(row); + }); + wrapper.appendChild(trackList); + } + } catch (e) { + console.error('Failed to load audio gallery:', e); + } + + container.appendChild(wrapper); +} diff --git a/web/modules/ui_audio_sidebar.js b/web/modules/ui_audio_sidebar.js new file mode 100644 index 0000000..8aeb21a --- /dev/null +++ b/web/modules/ui_audio_sidebar.js @@ -0,0 +1,163 @@ +import { t } from './interface_settings.js'; + +/** + * Audio Sidebar Navigation Module + * Renders structured category trees for voices, characters, and generated audio history. + */ + +let activeFilter = { type: 'all', value: null }; + +export function getActiveAudioFilter() { + return activeFilter; +} + +export function setActiveAudioFilter(filter) { + activeFilter = filter || { type: 'all', value: null }; +} + +const SIDEBAR_SVG = { + MIC: ``, + ALL_VOICES: ``, + USER: ``, + DISC: ``, + REFRESH: `` +}; + +function renderSidebarHeader(owner) { + const topBar = document.createElement('div'); + topBar.style.display = 'flex'; + topBar.style.justifyContent = 'space-between'; + topBar.style.alignItems = 'center'; + topBar.style.padding = '12px 14px 10px 14px'; + topBar.style.borderBottom = '1px solid rgba(255, 255, 255, 0.05)'; + + const title = document.createElement('h3'); + title.style.color = '#f1f5f9'; + title.style.margin = '0'; + title.style.fontSize = '12px'; + title.style.fontWeight = '600'; + title.style.letterSpacing = '0.5px'; + title.style.display = 'inline-flex'; + title.style.alignItems = 'center'; + title.style.gap = '8px'; + title.innerHTML = `${SIDEBAR_SVG.MIC}${t('audioSidebarTitle')}`; + + const refreshBtn = document.createElement('button'); + refreshBtn.type = 'button'; + refreshBtn.innerHTML = SIDEBAR_SVG.REFRESH; + refreshBtn.title = t('audioRefresh'); + refreshBtn.style.background = 'transparent'; + refreshBtn.style.border = 'none'; + refreshBtn.style.color = '#94a3b8'; + refreshBtn.style.cursor = 'pointer'; + refreshBtn.style.padding = '4px'; + refreshBtn.style.borderRadius = '4px'; + refreshBtn.style.display = 'inline-flex'; + refreshBtn.style.alignItems = 'center'; + refreshBtn.style.justifyContent = 'center'; + refreshBtn.onclick = () => renderAudioSidebar(owner); + + topBar.appendChild(title); + topBar.appendChild(refreshBtn); + return topBar; +} + +function createNavItem(label, iconSvg, count, isSelected, onClick) { + const item = document.createElement('div'); + item.className = `anomalous-audio-nav-item${isSelected ? ' active' : ''}`; + + const left = document.createElement('div'); + left.style.display = 'flex'; + left.style.alignItems = 'center'; + left.style.gap = '8px'; + left.innerHTML = `${iconSvg}${label}`; + + item.appendChild(left); + if (count !== undefined && count !== null) { + const badge = document.createElement('span'); + badge.style.fontSize = '10px'; + badge.style.color = isSelected ? '#818cf8' : '#64748b'; + badge.style.background = isSelected ? 'rgba(99, 102, 241, 0.2)' : 'rgba(255, 255, 255, 0.05)'; + badge.style.padding = '1px 6px'; + badge.style.borderRadius = '8px'; + badge.textContent = count; + item.appendChild(badge); + } + + item.onclick = onClick; + return item; +} + +function createSectionLabel(text) { + const sec = document.createElement('div'); + sec.style.padding = '12px 14px 4px 14px'; + sec.style.fontSize = '10px'; + sec.style.fontWeight = '700'; + sec.style.letterSpacing = '1px'; + sec.style.textTransform = 'uppercase'; + sec.style.color = '#64748b'; + sec.textContent = text; + return sec; +} + +export async function renderAudioSidebar(owner) { + if (!owner || !owner.sidebar) return; + owner.sidebar.innerHTML = ''; + owner.sidebar.appendChild(renderSidebarHeader(owner)); + + const listContainer = document.createElement('div'); + listContainer.style.overflowY = 'auto'; + listContainer.style.flex = '1'; + listContainer.style.padding = '6px 0'; + + try { + const resp = await fetch('/anomalous/audio_voices'); + const data = await resp.json(); + const chars = data.characters || []; + + // 1. All Voices Item + const allItem = createNavItem( + t('audioAllVoices'), SIDEBAR_SVG.ALL_VOICES, data.total_slices || 0, + activeFilter.type === 'all', + () => { + activeFilter = { type: 'all', value: null }; + renderAudioSidebar(owner); + owner.switchAudioTab?.('presets', activeFilter); + } + ); + listContainer.appendChild(allItem); + + // 2. Character Categories + listContainer.appendChild(createSectionLabel(t('audioSectionCharacters'))); + chars.forEach(c => { + const isSel = activeFilter.type === 'character' && activeFilter.value === c.character; + const cItem = createNavItem( + c.character, SIDEBAR_SVG.USER, c.total_slices, isSel, + () => { + activeFilter = { type: 'character', value: c.character }; + renderAudioSidebar(owner); + owner.switchAudioTab?.('presets', activeFilter); + } + ); + listContainer.appendChild(cItem); + }); + + // 3. Output History Entry + listContainer.appendChild(createSectionLabel(t('audioSectionHistory'))); + const histItem = createNavItem( + t('audioGalleryTab'), SIDEBAR_SVG.DISC, null, + activeFilter.type === 'gallery', + () => { + activeFilter = { type: 'gallery', value: null }; + renderAudioSidebar(owner); + owner.switchAudioTab?.('gallery', activeFilter); + } + ); + listContainer.appendChild(histItem); + + } catch (e) { + console.error('Failed to render audio sidebar:', e); + } + + owner.sidebar.appendChild(listContainer); +} diff --git a/web/modules/ui_audio_studio.js b/web/modules/ui_audio_studio.js index 5fe5ddf..a1e7625 100644 --- a/web/modules/ui_audio_studio.js +++ b/web/modules/ui_audio_studio.js @@ -1,4 +1,5 @@ import { t } from './interface_settings.js'; +import { stopGalleryAudio } from './ui_audio_gallery.js'; /** * Audio & Voice Studio Workspace @@ -14,10 +15,11 @@ const SVG = { PAUSE: ``, MIC: ``, COPY: ``, - CHECK: `` + CHECK: ``, + SEARCH: `` }; -function stopCurrentAudio() { +export function stopAudioStudioPlayback() { if (globalAudioPlayer) { globalAudioPlayer.pause(); globalAudioPlayer.currentTime = 0; @@ -25,8 +27,7 @@ function stopCurrentAudio() { } if (currentPlayingBtn) { currentPlayingBtn.innerHTML = SVG.PLAY; - currentPlayingBtn.style.color = '#94a3b8'; - currentPlayingBtn.style.borderColor = 'rgba(255,255,255,0.12)'; + currentPlayingBtn.classList.remove('is-playing'); currentPlayingBtn = null; } if (currentPlayingBar) { @@ -37,10 +38,11 @@ function stopCurrentAudio() { function playAudio(url, playBtn, eqBars) { if (currentPlayingBtn === playBtn) { - stopCurrentAudio(); + stopAudioStudioPlayback(); return; } - stopCurrentAudio(); + stopAudioStudioPlayback(); + if (typeof stopGalleryAudio === 'function') stopGalleryAudio(); const audio = new Audio(url); globalAudioPlayer = audio; @@ -48,13 +50,12 @@ function playAudio(url, playBtn, eqBars) { currentPlayingBar = eqBars; playBtn.innerHTML = SVG.PAUSE; - playBtn.style.color = '#38bdf8'; - playBtn.style.borderColor = 'rgba(56, 189, 248, 0.4)'; + playBtn.classList.add('is-playing'); if (eqBars) eqBars.style.display = 'inline-flex'; - audio.onended = () => stopCurrentAudio(); - audio.onerror = () => stopCurrentAudio(); - audio.play().catch(() => stopCurrentAudio()); + audio.onended = () => stopAudioStudioPlayback(); + audio.onerror = () => stopAudioStudioPlayback(); + audio.play().catch(() => stopAudioStudioPlayback()); } function copySyntax(tag, btn) { @@ -74,18 +75,18 @@ function copySyntax(tag, btn) { function getEmotionStyle(emotion) { const raw = String(emotion || '').toLowerCase(); if (raw.includes('happy') || raw.includes('joy')) { - return { color: '#fbbf24', bg: 'rgba(245, 158, 11, 0.1)', border: 'rgba(245, 158, 11, 0.25)' }; + return { color: '#fbbf24', bg: 'rgba(245, 158, 11, 0.12)', border: 'rgba(245, 158, 11, 0.3)' }; } if (raw.includes('sad') || raw.includes('cry')) { - return { color: '#60a5fa', bg: 'rgba(59, 130, 246, 0.1)', border: 'rgba(59, 130, 246, 0.25)' }; + return { color: '#60a5fa', bg: 'rgba(59, 130, 246, 0.12)', border: 'rgba(59, 130, 246, 0.3)' }; } if (raw.includes('surpris') || raw.includes('shock')) { - return { color: '#f472b6', bg: 'rgba(244, 114, 182, 0.1)', border: 'rgba(244, 114, 182, 0.25)' }; + return { color: '#f472b6', bg: 'rgba(244, 114, 182, 0.12)', border: 'rgba(244, 114, 182, 0.3)' }; } if (raw.includes('normal') || raw.includes('calm')) { - return { color: '#34d399', bg: 'rgba(16, 185, 129, 0.1)', border: 'rgba(16, 185, 129, 0.25)' }; + return { color: '#34d399', bg: 'rgba(16, 185, 129, 0.12)', border: 'rgba(16, 185, 129, 0.3)' }; } - return { color: '#a78bfa', bg: 'rgba(167, 139, 250, 0.1)', border: 'rgba(167, 139, 250, 0.25)' }; + return { color: '#a78bfa', bg: 'rgba(167, 139, 250, 0.12)', border: 'rgba(167, 139, 250, 0.3)' }; } function renderEqIndicator() { @@ -94,17 +95,30 @@ function renderEqIndicator() { barWrap.style.display = 'none'; barWrap.style.alignItems = 'flex-end'; barWrap.style.gap = '2px'; - barWrap.style.height = '12px'; - barWrap.style.marginRight = '4px'; + barWrap.style.height = '14px'; + barWrap.style.marginRight = '2px'; - for (let i = 0; i < 3; i++) { - const bar = document.createElement('span'); - bar.style.width = '2px'; - bar.style.height = `${6 + (i % 2) * 6}px`; - bar.style.background = '#38bdf8'; - bar.style.borderRadius = '1px'; - barWrap.appendChild(bar); - } + const b1 = document.createElement('span'); + b1.className = 'anomalous-eq-bar-1'; + b1.style.width = '2px'; + b1.style.background = '#38bdf8'; + b1.style.borderRadius = '1px'; + + const b2 = document.createElement('span'); + b2.className = 'anomalous-eq-bar-2'; + b2.style.width = '2px'; + b2.style.background = '#818cf8'; + b2.style.borderRadius = '1px'; + + const b3 = document.createElement('span'); + b3.className = 'anomalous-eq-bar-3'; + b3.style.width = '2px'; + b3.style.background = '#38bdf8'; + b3.style.borderRadius = '1px'; + + barWrap.appendChild(b1); + barWrap.appendChild(b2); + barWrap.appendChild(b3); return barWrap; } @@ -112,24 +126,6 @@ function renderSliceRow(slice) { const row = document.createElement('div'); row.className = 'anomalous-voice-slice-row'; row.draggable = true; - row.style.display = 'flex'; - row.style.alignItems = 'center'; - row.style.gap = '8px'; - row.style.padding = '6px 10px'; - row.style.borderRadius = '6px'; - row.style.background = 'rgba(255, 255, 255, 0.02)'; - row.style.border = '1px solid rgba(255, 255, 255, 0.05)'; - row.style.transition = 'all 0.18s cubic-bezier(0.16, 1, 0.3, 1)'; - row.style.cursor = 'grab'; - - row.addEventListener('mouseenter', () => { - row.style.background = 'rgba(255, 255, 255, 0.05)'; - row.style.borderColor = 'rgba(255, 255, 255, 0.12)'; - }); - row.addEventListener('mouseleave', () => { - row.style.background = 'rgba(255, 255, 255, 0.02)'; - row.style.borderColor = 'rgba(255, 255, 255, 0.05)'; - }); row.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', slice.syntax_tag); @@ -138,31 +134,20 @@ function renderSliceRow(slice) { const playBtn = document.createElement('button'); playBtn.type = 'button'; + playBtn.className = 'anomalous-audio-play-btn'; playBtn.innerHTML = SVG.PLAY; playBtn.title = t('audioPlay'); - playBtn.style.display = 'inline-flex'; - playBtn.style.alignItems = 'center'; - playBtn.style.justifyContent = 'center'; - playBtn.style.width = '24px'; - playBtn.style.height = '24px'; - playBtn.style.borderRadius = '50%'; - playBtn.style.border = '1px solid rgba(255, 255, 255, 0.12)'; - playBtn.style.background = 'rgba(255, 255, 255, 0.04)'; - playBtn.style.color = '#94a3b8'; - playBtn.style.cursor = 'pointer'; - playBtn.style.flexShrink = '0'; - playBtn.style.transition = 'all 0.15s ease'; const eqBars = renderEqIndicator(); - const emoStyle = getEmotionStyle(slice.emotion); + const emoTag = document.createElement('span'); emoTag.textContent = slice.emotion.toUpperCase(); emoTag.style.fontSize = '9px'; emoTag.style.fontWeight = '700'; emoTag.style.letterSpacing = '0.5px'; - emoTag.style.padding = '2px 5px'; - emoTag.style.borderRadius = '3px'; + emoTag.style.padding = '2px 6px'; + emoTag.style.borderRadius = '4px'; emoTag.style.color = emoStyle.color; emoTag.style.background = emoStyle.bg; emoTag.style.border = `1px solid ${emoStyle.border}`; @@ -185,10 +170,10 @@ function renderSliceRow(slice) { copyBtn.style.display = 'inline-flex'; copyBtn.style.alignItems = 'center'; copyBtn.style.gap = '4px'; - copyBtn.style.padding = '2px 7px'; + copyBtn.style.padding = '2px 8px'; copyBtn.style.borderRadius = '4px'; copyBtn.style.border = '1px solid rgba(255, 255, 255, 0.08)'; - copyBtn.style.background = 'rgba(0, 0, 0, 0.25)'; + copyBtn.style.background = 'rgba(0, 0, 0, 0.3)'; copyBtn.style.color = '#94a3b8'; copyBtn.style.fontSize = '11px'; copyBtn.style.fontFamily = 'monospace'; @@ -217,15 +202,6 @@ function renderSliceRow(slice) { function renderCharacterCard(charData) { const card = document.createElement('div'); card.className = 'anomalous-character-voice-card'; - card.style.background = 'rgba(22, 22, 27, 0.75)'; - card.style.border = '1px solid rgba(255, 255, 255, 0.08)'; - card.style.borderRadius = '10px'; - card.style.padding = '14px'; - card.style.display = 'flex'; - card.style.flexDirection = 'column'; - card.style.gap = '10px'; - card.style.backdropFilter = 'blur(16px)'; - card.style.boxShadow = '0 6px 20px rgba(0,0,0,0.3)'; const header = document.createElement('div'); header.style.display = 'flex'; @@ -235,40 +211,53 @@ function renderCharacterCard(charData) { const titleGroup = document.createElement('div'); titleGroup.style.display = 'flex'; titleGroup.style.alignItems = 'center'; - titleGroup.style.gap = '8px'; + titleGroup.style.gap = '10px'; + const isArona = charData.character.toLowerCase() === 'arona'; const avatar = document.createElement('div'); - avatar.style.width = '26px'; - avatar.style.height = '26px'; - avatar.style.borderRadius = '6px'; - avatar.style.background = charData.character.toLowerCase() === 'arona' - ? 'linear-gradient(135deg, rgba(56, 189, 248, 0.3), rgba(99, 102, 241, 0.3))' + avatar.style.width = '32px'; + avatar.style.height = '32px'; + avatar.style.borderRadius = '8px'; + avatar.style.background = isArona + ? 'linear-gradient(135deg, rgba(14, 165, 233, 0.35), rgba(99, 102, 241, 0.35))' : 'rgba(255, 255, 255, 0.06)'; - avatar.style.border = '1px solid rgba(255, 255, 255, 0.1)'; + avatar.style.border = isArona ? '1px solid rgba(56, 189, 248, 0.4)' : '1px solid rgba(255, 255, 255, 0.1)'; + avatar.style.boxShadow = isArona ? '0 0 12px rgba(14, 165, 233, 0.25)' : 'none'; avatar.style.display = 'flex'; avatar.style.alignItems = 'center'; avatar.style.justifyContent = 'center'; - avatar.style.color = charData.character.toLowerCase() === 'arona' ? '#38bdf8' : '#94a3b8'; + avatar.style.color = isArona ? '#38bdf8' : '#94a3b8'; avatar.innerHTML = SVG.MIC; + const nameBox = document.createElement('div'); + nameBox.style.display = 'flex'; + nameBox.style.flexDirection = 'column'; + const name = document.createElement('span'); - name.style.fontSize = '13px'; + name.style.fontSize = '14px'; name.style.fontWeight = '600'; - name.style.letterSpacing = '0.3px'; - name.style.color = '#f1f5f9'; + name.style.color = '#f8fafc'; name.textContent = charData.character; + const sub = document.createElement('span'); + sub.style.fontSize = '11px'; + sub.style.color = '#64748b'; + sub.textContent = t('audioDragHint'); + + nameBox.appendChild(name); + nameBox.appendChild(sub); + const countBadge = document.createElement('span'); - countBadge.style.fontSize = '10px'; - countBadge.style.padding = '2px 6px'; - countBadge.style.borderRadius = '10px'; + countBadge.style.fontSize = '11px'; + countBadge.style.padding = '3px 8px'; + countBadge.style.borderRadius = '12px'; countBadge.style.background = 'rgba(255, 255, 255, 0.05)'; - countBadge.style.border = '1px solid rgba(255, 255, 255, 0.06)'; - countBadge.style.color = '#64748b'; + countBadge.style.border = '1px solid rgba(255, 255, 255, 0.08)'; + countBadge.style.color = '#94a3b8'; countBadge.textContent = `${charData.total_slices} ${t('audioVoicePresets')}`; titleGroup.appendChild(avatar); - titleGroup.appendChild(name); + titleGroup.appendChild(nameBox); header.appendChild(titleGroup); header.appendChild(countBadge); card.appendChild(header); @@ -276,7 +265,7 @@ function renderCharacterCard(charData) { const sliceList = document.createElement('div'); sliceList.style.display = 'flex'; sliceList.style.flexDirection = 'column'; - sliceList.style.gap = '5px'; + sliceList.style.gap = '6px'; charData.slices.forEach(slice => { sliceList.appendChild(renderSliceRow(slice)); @@ -286,75 +275,112 @@ function renderCharacterCard(charData) { return card; } -export async function renderAudioStudio(container) { - stopCurrentAudio(); +function renderStudioToolbar(onSearch) { + const toolbar = document.createElement('div'); + toolbar.className = 'anomalous-audio-toolbar'; + + const titleGroup = document.createElement('div'); + titleGroup.className = 'anomalous-audio-title-group'; + + const iconBox = document.createElement('div'); + iconBox.className = 'anomalous-audio-icon-box'; + iconBox.innerHTML = SVG.MIC; + + const textGroup = document.createElement('div'); + textGroup.style.display = 'flex'; + textGroup.style.flexDirection = 'column'; + + const title = document.createElement('span'); + title.className = 'anomalous-audio-title-text'; + title.textContent = t('audioStudioTitle'); + + const desc = document.createElement('span'); + desc.className = 'anomalous-audio-sub-text'; + desc.style.marginLeft = '0'; + desc.textContent = t('audioStudioSubtitle'); + + textGroup.appendChild(title); + textGroup.appendChild(desc); + + titleGroup.appendChild(iconBox); + titleGroup.appendChild(textGroup); + + const searchWrap = document.createElement('div'); + searchWrap.style.display = 'flex'; + searchWrap.style.alignItems = 'center'; + searchWrap.style.gap = '6px'; + searchWrap.style.background = 'rgba(0, 0, 0, 0.25)'; + searchWrap.style.border = '1px solid rgba(255, 255, 255, 0.1)'; + searchWrap.style.borderRadius = '8px'; + searchWrap.style.padding = '4px 10px'; + + const searchIcon = document.createElement('span'); + searchIcon.style.color = '#64748b'; + searchIcon.innerHTML = SVG.SEARCH; + + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.placeholder = t('audioVoicePresets'); + searchInput.style.background = 'transparent'; + searchInput.style.border = 'none'; + searchInput.style.outline = 'none'; + searchInput.style.color = '#f1f5f9'; + searchInput.style.fontSize = '12px'; + searchInput.style.width = '140px'; + searchInput.oninput = (e) => onSearch(e.target.value.toLowerCase()); + + searchWrap.appendChild(searchIcon); + searchWrap.appendChild(searchInput); + + toolbar.appendChild(titleGroup); + toolbar.appendChild(searchWrap); + return toolbar; +} + +export async function renderAudioStudio(container, filter = null) { + stopAudioStudioPlayback(); container.innerHTML = ''; const studioWrapper = document.createElement('div'); studioWrapper.className = 'anomalous-audio-studio-wrapper'; - studioWrapper.style.padding = '16px'; - studioWrapper.style.height = '100%'; - studioWrapper.style.overflowY = 'auto'; - studioWrapper.style.boxSizing = 'border-box'; - - // Toolbar Header - const toolbar = document.createElement('div'); - toolbar.style.display = 'flex'; - toolbar.style.alignItems = 'center'; - toolbar.style.justifyContent = 'space-between'; - toolbar.style.marginBottom = '14px'; - toolbar.style.paddingBottom = '10px'; - toolbar.style.borderBottom = '1px solid rgba(255, 255, 255, 0.06)'; - - const titleGroup = document.createElement('div'); - titleGroup.style.display = 'flex'; - titleGroup.style.alignItems = 'center'; - titleGroup.style.gap = '8px'; - - const iconBox = document.createElement('span'); - iconBox.style.color = '#818cf8'; - iconBox.style.display = 'flex'; - iconBox.innerHTML = SVG.MIC; - - const title = document.createElement('span'); - title.style.fontSize = '14px'; - title.style.fontWeight = '600'; - title.style.color = '#f1f5f9'; - title.textContent = t('audioStudioTitle'); - - const desc = document.createElement('span'); - desc.style.fontSize = '11px'; - desc.style.color = '#64748b'; - desc.textContent = t('audioStudioSubtitle'); - - titleGroup.appendChild(iconBox); - titleGroup.appendChild(title); - titleGroup.appendChild(desc); - toolbar.appendChild(titleGroup); - studioWrapper.appendChild(toolbar); try { const resp = await fetch('/anomalous/audio_voices'); const data = await resp.json(); + let characters = data.characters || []; - if (!data.characters || data.characters.length === 0) { + // Apply character filter if selected from sidebar + if (filter && filter.type === 'character' && filter.value) { + characters = characters.filter(c => c.character.toLowerCase() === filter.value.toLowerCase()); + } + + const grid = document.createElement('div'); + grid.style.display = 'grid'; + grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(380px, 1fr))'; + grid.style.gap = '16px'; + + const toolbar = renderStudioToolbar((searchTerm) => { + const cards = grid.querySelectorAll('.anomalous-character-voice-card'); + cards.forEach(card => { + const text = card.textContent.toLowerCase(); + card.style.display = text.includes(searchTerm) ? 'flex' : 'none'; + }); + }); + studioWrapper.appendChild(toolbar); + + if (characters.length === 0) { const emptyGuide = document.createElement('div'); - emptyGuide.style.padding = '60px 20px'; + emptyGuide.style.padding = '70px 20px'; emptyGuide.style.textAlign = 'center'; emptyGuide.style.color = '#64748b'; emptyGuide.innerHTML = ` -
🎙️
-
${t('audioEmptyTitle')}
-
${t('audioEmptyDesc')}
+
🎙️
+
${t('audioEmptyTitle')}
+
${t('audioEmptyDesc')}
`; studioWrapper.appendChild(emptyGuide); } else { - const grid = document.createElement('div'); - grid.style.display = 'grid'; - grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(360px, 1fr))'; - grid.style.gap = '14px'; - - data.characters.forEach(charData => { + characters.forEach(charData => { grid.appendChild(renderCharacterCard(charData)); }); studioWrapper.appendChild(grid); diff --git a/web/modules/ui_browser_navigation.js b/web/modules/ui_browser_navigation.js index 2277de9..cf43acb 100644 --- a/web/modules/ui_browser_navigation.js +++ b/web/modules/ui_browser_navigation.js @@ -75,6 +75,7 @@ export function hideAllPanels() { if (this.assistantPanel) this.assistantPanel.style.display = 'none'; if (this.paramPanel) this.paramPanel.style.display = 'none'; if (this.audioStudioPanel) this.audioStudioPanel.style.display = 'none'; + if (this.audioGalleryPanel) this.audioGalleryPanel.style.display = 'none'; if (this.currentDetailObserver) { this.currentDetailObserver.disconnect(); this.currentDetailObserver = null; diff --git a/web/modules/ui_sidebar.js b/web/modules/ui_sidebar.js index 371274a..232b62f 100644 --- a/web/modules/ui_sidebar.js +++ b/web/modules/ui_sidebar.js @@ -10,7 +10,10 @@ import { showUpdateGuide } from './ui_update_guide.js'; import { setScanButtonState } from './ui_scan_wizard.js'; import { createSettingsHub } from './ui_settings_hub.js'; import { createToolbox } from './ui_toolbox.js'; -import { createDomainSwitcher } from './ui_domain_switcher.js'; +import { createDomainSwitcher, getActiveDomain } from './ui_domain_switcher.js'; +import { renderAudioSidebar } from './ui_audio_sidebar.js'; +import { renderAudioStudio } from './ui_audio_studio.js'; +import { renderAudioGallery } from './ui_audio_gallery.js'; const t = (key, params) => translate(key, params); @@ -18,6 +21,8 @@ const SIDEBAR_ICONS = { MODELS: ``, GALLERY: ``, RECIPES: ``, + AUDIO_STUDIO: ``, + AUDIO_VAULT: ``, DOCK: ``, HELP: ``, TOOLBOX: ``, @@ -257,6 +262,33 @@ export function createDOM() { galleryBtn.id = 'anomalous-gallery-btn'; galleryBtn.innerHTML = `${SIDEBAR_ICONS.GALLERY}${t('gallery') || '图库'}`; + const nbBtn = document.createElement('button'); + nbBtn.id = 'anomalous-notebook-btn'; + nbBtn.title = t('recipeTitle'); + nbBtn.innerHTML = `${SIDEBAR_ICONS.RECIPES}${t('recipeTitle')}`; + + this.modelsBtn = modelsBtn; + this.galleryBtn = galleryBtn; + this.nbBtn = nbBtn; + + const updateHeaderTabs = (domain) => { + const isAudio = domain === 'audio'; + if (isAudio) { + modelsBtn.innerHTML = `${SIDEBAR_ICONS.AUDIO_STUDIO}${t('audioTabPresets')}`; + galleryBtn.innerHTML = `${SIDEBAR_ICONS.AUDIO_VAULT}${t('audioTabGallery')}`; + const nbText = nbBtn.querySelector('.anomalous-btn-text'); + if (nbText) nbText.textContent = t('audioTabWorkflow'); + if (this.sidebarActions) this.sidebarActions.style.display = 'none'; + } else { + modelsBtn.innerHTML = `${SIDEBAR_ICONS.MODELS}${t('models')}`; + galleryBtn.innerHTML = `${SIDEBAR_ICONS.GALLERY}${t('gallery') || '图库'}`; + const nbText = nbBtn.querySelector('.anomalous-btn-text'); + if (nbText) nbText.textContent = t('recipeTitle'); + if (this.sidebarActions) this.sidebarActions.style.display = 'flex'; + } + }; + this.updateHeaderTabs = updateHeaderTabs; + const setActiveHeaderTab = (btn) => { modelsBtn.classList.remove('active'); galleryBtn.classList.remove('active'); @@ -268,6 +300,17 @@ export function createDOM() { modelsBtn.onclick = () => { this.hideAllPanels(); setActiveHeaderTab(modelsBtn); + if (getActiveDomain() === 'audio') { + showSidebar(); + menuBtn.disabled = false; + menuBtn.style.opacity = '1'; + menuBtn.style.cursor = 'pointer'; + if (this.audioStudioPanel) { + this.audioStudioPanel.style.display = 'block'; + renderAudioStudio(this.audioStudioPanel); + } + return; + } if (localStorage.getItem('anomalous_user_sidebar_closed') === 'true') { container.classList.add('anomalous-sidebar-closed'); } else { @@ -287,6 +330,17 @@ export function createDOM() { galleryBtn.onclick = () => { this.hideAllPanels(); setActiveHeaderTab(galleryBtn); + if (getActiveDomain() === 'audio') { + showSidebar(); + menuBtn.disabled = false; + menuBtn.style.opacity = '1'; + menuBtn.style.cursor = 'pointer'; + if (this.audioGalleryPanel) { + this.audioGalleryPanel.style.display = 'block'; + renderAudioGallery(this.audioGalleryPanel); + } + return; + } this.gallerySelectModel = null; this.galleryPanel.classList.remove('is-cover-selecting'); const selectBanner = document.getElementById('anomalous-gallery-select-banner'); @@ -320,11 +374,6 @@ export function createDOM() { container.classList.add('anomalous-docked'); } - const nbBtn = document.createElement('button'); - nbBtn.id = 'anomalous-notebook-btn'; - nbBtn.title = t('recipeTitle'); - nbBtn.innerHTML = `${SIDEBAR_ICONS.RECIPES}${t('recipeTitle')}`; - const dBtn = document.getElementById('anomalous-doctor-btn'); if (dBtn) { dBtn.removeAttribute('title'); dBtn.setAttribute('aria-label', t('sidebarDoctor')); } const aBtn = document.getElementById('anomalous-assistant-btn'); @@ -503,6 +552,13 @@ export function createDOM() { this.audioStudioPanel.style.height = '100%'; this.audioStudioPanel.style.overflow = 'hidden'; + this.audioGalleryPanel = document.createElement('div'); + this.audioGalleryPanel.id = 'anomalous-audio-gallery-panel'; + this.audioGalleryPanel.style.display = 'none'; + this.audioGalleryPanel.style.flex = '1'; + this.audioGalleryPanel.style.height = '100%'; + this.audioGalleryPanel.style.overflow = 'hidden'; + this.galleryGrid = document.createElement('div'); this.galleryGrid.className = 'anomalous-gallery-grid'; this.galleryPanel.appendChild(this.galleryGrid); @@ -544,6 +600,7 @@ export function createDOM() { content.appendChild(this.doctorPanel); content.appendChild(this.assistantPanel); content.appendChild(this.audioStudioPanel); + content.appendChild(this.audioGalleryPanel); container.appendChild(this.sidebarWrapper); container.appendChild(content); @@ -588,6 +645,10 @@ export function createDOM() { export function renderSidebar() { + if (getActiveDomain() === 'audio') { + renderAudioSidebar(this); + return; + } this.sidebar.innerHTML = ''; const topBar = document.createElement('div'); diff --git a/web/styles.css b/web/styles.css index 8ad0920..81cdc3a 100644 --- a/web/styles.css +++ b/web/styles.css @@ -9,3 +9,4 @@ @import url("./styles/08-theme-gallery-overrides.css?v=20260918-model-sources-1"); @import url("./styles/09-prompt-drawers.css?v=20260918-model-sources-1"); @import url("./styles/10-model-sources.css?v=20260918-model-sources-1"); +@import url("./styles/11-audio-studio.css?v=20260920-audio-1"); diff --git a/web/styles/11-audio-studio.css b/web/styles/11-audio-studio.css new file mode 100644 index 0000000..b4f2ffe --- /dev/null +++ b/web/styles/11-audio-studio.css @@ -0,0 +1,217 @@ +/** + * 11-audio-studio.css + * Dark Glassmorphism Studio / DAW Design for Audio & Voice Ecosystem + */ + +/* Equalizer bar keyframes */ +@keyframes eq-pulse-1 { + 0%, 100% { height: 4px; } + 50% { height: 12px; } +} + +@keyframes eq-pulse-2 { + 0%, 100% { height: 12px; } + 50% { height: 5px; } +} + +@keyframes eq-pulse-3 { + 0%, 100% { height: 7px; } + 50% { height: 14px; } +} + +.anomalous-eq-bar-1 { animation: eq-pulse-1 0.65s ease-in-out infinite; } +.anomalous-eq-bar-2 { animation: eq-pulse-2 0.5s ease-in-out infinite; } +.anomalous-eq-bar-3 { animation: eq-pulse-3 0.8s ease-in-out infinite; } + +/* Workspace Containers */ +.anomalous-audio-studio-wrapper, +.anomalous-audio-gallery-wrapper { + height: 100%; + overflow-y: auto; + padding: 20px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 16px; + background: radial-gradient(circle at top right, rgba(99, 102, 241, 0.05), transparent 40%), + radial-gradient(circle at bottom left, rgba(56, 189, 248, 0.04), transparent 40%); +} + +/* Header & Toolbars */ +.anomalous-audio-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + flex-shrink: 0; +} + +.anomalous-audio-title-group { + display: flex; + align-items: center; + gap: 10px; +} + +.anomalous-audio-icon-box { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(56, 189, 248, 0.15)); + border: 1px solid rgba(129, 140, 248, 0.3); + color: #818cf8; + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.2); +} + +.anomalous-audio-title-text { + font-size: 15px; + font-weight: 600; + color: #f8fafc; + letter-spacing: 0.3px; +} + +.anomalous-audio-sub-text { + font-size: 12px; + color: #64748b; + margin-left: 6px; +} + +/* Character Voice Cards */ +.anomalous-character-voice-card { + background: rgba(18, 18, 23, 0.75); + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + backdrop-filter: blur(20px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); + transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.2s ease, box-shadow 0.2s ease; +} + +.anomalous-character-voice-card:hover { + border-color: rgba(99, 102, 241, 0.25); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), 0 0 15px rgba(99, 102, 241, 0.08); +} + +.anomalous-voice-slice-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.04); + transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1); + cursor: grab; + user-select: none; +} + +.anomalous-voice-slice-row:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.12); + transform: translateX(2px); +} + +.anomalous-voice-slice-row:active { + cursor: grabbing; +} + +/* Play Buttons */ +.anomalous-audio-play-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); + color: #cbd5e1; + cursor: pointer; + flex-shrink: 0; + transition: all 0.15s ease; +} + +.anomalous-audio-play-btn:hover { + background: rgba(56, 189, 248, 0.15); + border-color: rgba(56, 189, 248, 0.4); + color: #38bdf8; + transform: scale(1.06); +} + +.anomalous-audio-play-btn.is-playing { + background: rgba(56, 189, 248, 0.2); + border-color: #38bdf8; + color: #38bdf8; + box-shadow: 0 0 10px rgba(56, 189, 248, 0.3); +} + +/* Audio Gallery Tracks */ +.anomalous-audio-track-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-radius: 10px; + background: rgba(22, 22, 28, 0.7); + border: 1px solid rgba(255, 255, 255, 0.06); + backdrop-filter: blur(14px); + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.anomalous-audio-track-item:hover { + background: rgba(28, 28, 36, 0.85); + border-color: rgba(129, 140, 248, 0.3); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35); +} + +.anomalous-track-progress-bar { + flex: 1; + height: 4px; + border-radius: 2px; + background: rgba(255, 255, 255, 0.08); + position: relative; + overflow: hidden; + cursor: pointer; +} + +.anomalous-track-progress-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, #38bdf8, #818cf8); + border-radius: 2px; + transition: width 0.1s linear; +} + +/* Audio Sidebar Nav */ +.anomalous-audio-nav-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + margin: 2px 8px; + border-radius: 8px; + cursor: pointer; + font-size: 12px; + color: #cbd5e1; + transition: all 0.15s ease; + border-left: 2px solid transparent; +} + +.anomalous-audio-nav-item:hover { + background: rgba(255, 255, 255, 0.04); + color: #f1f5f9; +} + +.anomalous-audio-nav-item.active { + background: rgba(99, 102, 241, 0.14); + color: #818cf8; + border-left-color: #818cf8; + font-weight: 600; +}