feat(audio): complete audio sidebar, audio vault gallery, and domain navigation
This commit is contained in:
+5
-3
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
+87
-4
@@ -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")
|
||||
|
||||
|
||||
+30
-4
@@ -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();
|
||||
|
||||
+40
-2
@@ -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"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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: `<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>`,
|
||||
PAUSE: `<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>`,
|
||||
DOWNLOAD: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
|
||||
TRASH: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
|
||||
VAULT: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="20" x="2" y="2" rx="2.18" ry="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="17" x2="22" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/></svg>`,
|
||||
AUDIO_WAVE: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 10v4"/><path d="M6 7v10"/><path d="M10 4v16"/><path d="M14 8v8"/><path d="M18 5v14"/><path d="M22 10v4"/></svg>`
|
||||
};
|
||||
|
||||
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 = `
|
||||
<div style="font-size:32px;margin-bottom:12px;opacity:0.6;">🎵</div>
|
||||
<div style="color:#cbd5e1;font-size:14px;font-weight:600;margin-bottom:6px;">${t('audioGalleryEmpty')}</div>
|
||||
<div style="font-size:12px;max-width:400px;margin:0 auto;line-height:1.6;">${t('audioStudioSubtitle')}</div>
|
||||
`;
|
||||
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);
|
||||
}
|
||||
@@ -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: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg>`,
|
||||
ALL_VOICES: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`,
|
||||
USER: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
|
||||
DISC: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>`,
|
||||
REFRESH: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 21h5v-5"/></svg>`
|
||||
};
|
||||
|
||||
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 = `<span style="color:#818cf8;">${SIDEBAR_SVG.MIC}</span><span>${t('audioSidebarTitle')}</span>`;
|
||||
|
||||
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 = `<span style="opacity:0.85;display:inline-flex;align-items:center;">${iconSvg}</span><span>${label}</span>`;
|
||||
|
||||
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);
|
||||
}
|
||||
+167
-141
@@ -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: `<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>`,
|
||||
MIC: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg>`,
|
||||
COPY: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`,
|
||||
CHECK: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`
|
||||
CHECK: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
|
||||
SEARCH: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`
|
||||
};
|
||||
|
||||
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 = `
|
||||
<div style="font-size:28px;margin-bottom:8px;opacity:0.6;">🎙️</div>
|
||||
<div style="color:#cbd5e1;font-size:13px;font-weight:600;margin-bottom:4px;">${t('audioEmptyTitle')}</div>
|
||||
<div style="font-size:11px;max-width:380px;margin:0 auto;line-height:1.5;">${t('audioEmptyDesc')}</div>
|
||||
<div style="font-size:32px;margin-bottom:12px;opacity:0.6;">🎙️</div>
|
||||
<div style="color:#cbd5e1;font-size:14px;font-weight:600;margin-bottom:6px;">${t('audioEmptyTitle')}</div>
|
||||
<div style="font-size:12px;max-width:420px;margin:0 auto;line-height:1.6;">${t('audioEmptyDesc')}</div>
|
||||
`;
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`,
|
||||
GALLERY: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
|
||||
RECIPES: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v6"/><path d="M9 6h6"/><path d="M7.8 7.8l8.4 8.4"/></svg>`,
|
||||
AUDIO_STUDIO: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/></svg>`,
|
||||
AUDIO_VAULT: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="anomalous-btn-icon"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>`,
|
||||
DOCK: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>`,
|
||||
HELP: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:-1px;margin-right:7px;flex-shrink:0;"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
||||
TOOLBOX: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><rect width="20" height="14" x="2" y="6" rx="2"/><path d="M2 12h20"/><path d="M10 12v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-2"/></svg>`,
|
||||
@@ -257,6 +262,33 @@ export function createDOM() {
|
||||
galleryBtn.id = 'anomalous-gallery-btn';
|
||||
galleryBtn.innerHTML = `${SIDEBAR_ICONS.GALLERY}<span class="anomalous-btn-text">${t('gallery') || '图库'}</span>`;
|
||||
|
||||
const nbBtn = document.createElement('button');
|
||||
nbBtn.id = 'anomalous-notebook-btn';
|
||||
nbBtn.title = t('recipeTitle');
|
||||
nbBtn.innerHTML = `${SIDEBAR_ICONS.RECIPES}<span class="anomalous-btn-text">${t('recipeTitle')}</span>`;
|
||||
|
||||
this.modelsBtn = modelsBtn;
|
||||
this.galleryBtn = galleryBtn;
|
||||
this.nbBtn = nbBtn;
|
||||
|
||||
const updateHeaderTabs = (domain) => {
|
||||
const isAudio = domain === 'audio';
|
||||
if (isAudio) {
|
||||
modelsBtn.innerHTML = `${SIDEBAR_ICONS.AUDIO_STUDIO}<span class="anomalous-btn-text">${t('audioTabPresets')}</span>`;
|
||||
galleryBtn.innerHTML = `${SIDEBAR_ICONS.AUDIO_VAULT}<span class="anomalous-btn-text">${t('audioTabGallery')}</span>`;
|
||||
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}<span class="anomalous-btn-text">${t('models')}</span>`;
|
||||
galleryBtn.innerHTML = `${SIDEBAR_ICONS.GALLERY}<span class="anomalous-btn-text">${t('gallery') || '图库'}</span>`;
|
||||
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}<span class="anomalous-btn-text">${t('recipeTitle')}</span>`;
|
||||
|
||||
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');
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user