Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_audio_studio.js
T

489 lines
19 KiB
JavaScript

import { app } from '../../../scripts/app.js';
import { t } from './interface_settings.js';
import { stopGalleryAudio } from './ui_audio_gallery.js';
/**
* Audio & Voice Studio Workspace
* Professional, dark-themed DAW/Studio aesthetic for character voice presets.
*/
let globalAudioPlayer = null;
let currentPlayingBtn = null;
let currentPlayingBar = null;
const SVG = {
PLAY: `<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></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>`,
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>`
};
export function stopAudioStudioPlayback() {
if (globalAudioPlayer) {
globalAudioPlayer.pause();
globalAudioPlayer.currentTime = 0;
globalAudioPlayer = null;
}
if (currentPlayingBtn) {
currentPlayingBtn.innerHTML = SVG.PLAY;
currentPlayingBtn.classList.remove('is-playing');
currentPlayingBtn = null;
}
if (currentPlayingBar) {
currentPlayingBar.style.display = 'none';
currentPlayingBar = null;
}
}
function playAudio(url, playBtn, eqBars) {
if (currentPlayingBtn === playBtn) {
stopAudioStudioPlayback();
return;
}
stopAudioStudioPlayback();
if (typeof stopGalleryAudio === 'function') stopGalleryAudio();
const audio = new Audio(url);
globalAudioPlayer = audio;
currentPlayingBtn = playBtn;
currentPlayingBar = eqBars;
playBtn.innerHTML = SVG.PAUSE;
playBtn.classList.add('is-playing');
if (eqBars) eqBars.style.display = 'inline-flex';
audio.onended = () => stopAudioStudioPlayback();
audio.onerror = () => stopAudioStudioPlayback();
audio.play().catch(() => stopAudioStudioPlayback());
}
function copySyntax(tag, btn) {
navigator.clipboard.writeText(tag).then(() => {
const originalHtml = btn.innerHTML;
btn.innerHTML = `${SVG.CHECK} <span>${t('audioCopied')}</span>`;
btn.style.color = '#34d399';
btn.style.borderColor = 'rgba(52, 211, 153, 0.4)';
setTimeout(() => {
btn.innerHTML = originalHtml;
btn.style.color = '';
btn.style.borderColor = '';
}, 1500);
});
}
function getEmotionStyle(emotion) {
const raw = String(emotion || '').toLowerCase();
if (raw.includes('happy') || raw.includes('joy')) {
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.12)', border: 'rgba(59, 130, 246, 0.3)' };
}
if (raw.includes('surpris') || raw.includes('shock')) {
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.12)', border: 'rgba(16, 185, 129, 0.3)' };
}
return { color: '#a78bfa', bg: 'rgba(167, 139, 250, 0.12)', border: 'rgba(167, 139, 250, 0.3)' };
}
function renderEqIndicator() {
const barWrap = document.createElement('span');
barWrap.className = 'anomalous-audio-eq-bars';
barWrap.style.display = 'none';
barWrap.style.alignItems = 'flex-end';
barWrap.style.gap = '2px';
barWrap.style.height = '14px';
barWrap.style.marginRight = '2px';
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;
}
function renderSliceRow(slice) {
const row = document.createElement('div');
row.className = 'anomalous-voice-slice-row';
row.draggable = true;
row.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', slice.syntax_tag);
e.dataTransfer.setData('application/json', JSON.stringify(slice));
window.__anomalous_active_audio_slice = slice;
setupCanvasAudioDrop();
});
row.addEventListener('dragend', () => {
window.__anomalous_active_audio_slice = null;
});
const playBtn = document.createElement('button');
playBtn.type = 'button';
playBtn.className = 'anomalous-audio-play-btn';
playBtn.innerHTML = SVG.PLAY;
playBtn.title = t('audioPlay');
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 6px';
emoTag.style.borderRadius = '4px';
emoTag.style.color = emoStyle.color;
emoTag.style.background = emoStyle.bg;
emoTag.style.border = `1px solid ${emoStyle.border}`;
emoTag.style.flexShrink = '0';
const textSpan = document.createElement('div');
textSpan.style.flex = '1';
textSpan.style.fontSize = '12px';
textSpan.style.color = '#cbd5e1';
textSpan.style.whiteSpace = 'nowrap';
textSpan.style.overflow = 'hidden';
textSpan.style.textOverflow = 'ellipsis';
textSpan.textContent = slice.text ? `“${slice.text}”` : slice.filename;
textSpan.title = slice.text || slice.filename;
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.innerHTML = `${SVG.COPY} <span>${slice.syntax_tag}</span>`;
copyBtn.title = t('audioCopyTag');
copyBtn.style.display = 'inline-flex';
copyBtn.style.alignItems = 'center';
copyBtn.style.gap = '4px';
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.3)';
copyBtn.style.color = '#94a3b8';
copyBtn.style.fontSize = '11px';
copyBtn.style.fontFamily = 'monospace';
copyBtn.style.cursor = 'pointer';
copyBtn.style.flexShrink = '0';
copyBtn.style.transition = 'all 0.15s ease';
copyBtn.onclick = (e) => {
e.stopPropagation();
copySyntax(slice.syntax_tag, copyBtn);
};
playBtn.onclick = (e) => {
e.stopPropagation();
playAudio(slice.audio_url, playBtn, eqBars);
};
row.appendChild(playBtn);
row.appendChild(eqBars);
row.appendChild(emoTag);
row.appendChild(textSpan);
row.appendChild(copyBtn);
return row;
}
function renderCharacterCard(charData) {
const card = document.createElement('div');
card.className = 'anomalous-character-voice-card';
const header = document.createElement('div');
header.style.display = 'flex';
header.style.alignItems = 'center';
header.style.justifyContent = 'space-between';
const titleGroup = document.createElement('div');
titleGroup.style.display = 'flex';
titleGroup.style.alignItems = 'center';
titleGroup.style.gap = '10px';
const isArona = charData.character.toLowerCase() === 'arona';
const avatar = document.createElement('div');
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 = 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 = 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 = '14px';
name.style.fontWeight = '600';
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 = '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.08)';
countBadge.style.color = '#94a3b8';
countBadge.textContent = `${charData.total_slices} ${t('audioVoicePresets')}`;
titleGroup.appendChild(avatar);
titleGroup.appendChild(nameBox);
header.appendChild(titleGroup);
header.appendChild(countBadge);
card.appendChild(header);
const sliceList = document.createElement('div');
sliceList.style.display = 'flex';
sliceList.style.flexDirection = 'column';
sliceList.style.gap = '6px';
charData.slices.forEach(slice => {
sliceList.appendChild(renderSliceRow(slice));
});
card.appendChild(sliceList);
return card;
}
function setupCanvasAudioDrop() {
if (window.__anomalous_canvas_audio_drop_bound) return;
window.__anomalous_canvas_audio_drop_bound = true;
window.addEventListener('drop', (e) => {
const slice = window.__anomalous_active_audio_slice;
if (!slice || !app?.graph || !app?.canvas) return;
const surface = app.canvas.canvas;
if (!surface) return;
const rect = surface.getBoundingClientRect();
if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
let pos = null;
if (app.canvas.convertEventToCanvasOffset) pos = app.canvas.convertEventToCanvasOffset(e);
else if (app.canvas.adjustMouseEvent) { app.canvas.adjustMouseEvent(e); pos = [e.canvasX, e.canvasY]; }
if (!pos) return;
const node = app.graph.getNodeOnPos?.(pos[0], pos[1]);
if (!node) return;
const sampleWidget = node.widgets?.find(w => w.name === 'sample' || w.name === 'audio' || w.name === 'prompt_audio');
if (sampleWidget) {
sampleWidget.value = `F5-TTS/${slice.filename}`;
node.setDirtyCanvas(true, true);
}
}, true);
}
function createLoadWorkflowButton() {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'anomalous-audio-load-wf-btn';
btn.innerHTML = `<span>⚡</span> <span>${t('audioLoadAronaWorkflow')}</span>`;
btn.style.display = 'inline-flex';
btn.style.alignItems = 'center';
btn.style.gap = '6px';
btn.style.padding = '5px 12px';
btn.style.borderRadius = '8px';
btn.style.border = '1px solid rgba(129, 140, 248, 0.4)';
btn.style.background = 'linear-gradient(135deg, rgba(99, 102, 241, 0.25), rgba(56, 189, 248, 0.2))';
btn.style.color = '#e0e7ff';
btn.style.fontSize = '12px';
btn.style.fontWeight = '500';
btn.style.cursor = 'pointer';
btn.style.transition = 'all 0.2s cubic-bezier(0.16, 1, 0.3, 1)';
btn.style.boxShadow = '0 2px 10px rgba(99, 102, 241, 0.2)';
btn.onmouseenter = () => {
btn.style.background = 'linear-gradient(135deg, rgba(99, 102, 241, 0.4), rgba(56, 189, 248, 0.35))';
btn.style.borderColor = 'rgba(129, 140, 248, 0.7)';
btn.style.transform = 'translateY(-1px)';
};
btn.onmouseleave = () => {
btn.style.background = 'linear-gradient(135deg, rgba(99, 102, 241, 0.25), rgba(56, 189, 248, 0.2))';
btn.style.borderColor = 'rgba(129, 140, 248, 0.4)';
btn.style.transform = 'translateY(0)';
};
btn.onclick = async () => {
try {
btn.style.opacity = '0.6';
const resp = await fetch('/anomalous/audio_template_workflow?name=arona');
const data = await resp.json();
if (data.workflow && app.loadGraphData) {
await app.loadGraphData(data.workflow);
btn.innerHTML = `<span>✅</span> <span>${t('audioWorkflowLoaded')}</span>`;
setTimeout(() => {
btn.innerHTML = `<span>⚡</span> <span>${t('audioLoadAronaWorkflow')}</span>`;
btn.style.opacity = '1';
}, 2000);
}
} catch (e) {
console.error('Failed to load Arona workflow:', e);
btn.style.opacity = '1';
}
};
return btn;
}
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 rightActions = document.createElement('div');
rightActions.style.display = 'flex';
rightActions.style.alignItems = 'center';
rightActions.style.gap = '10px';
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);
const loadWfBtn = createLoadWorkflowButton();
rightActions.appendChild(loadWfBtn);
rightActions.appendChild(searchWrap);
toolbar.appendChild(titleGroup);
toolbar.appendChild(rightActions);
return toolbar;
}
export async function renderAudioStudio(container, filter = null) {
stopAudioStudioPlayback();
container.innerHTML = '';
const studioWrapper = document.createElement('div');
studioWrapper.className = 'anomalous-audio-studio-wrapper';
try {
const resp = await fetch('/anomalous/audio_voices');
const data = await resp.json();
let characters = data.characters || [];
// 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 = '70px 20px';
emptyGuide.style.textAlign = 'center';
emptyGuide.style.color = '#64748b';
emptyGuide.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('audioEmptyTitle')}</div>
<div style="font-size:12px;max-width:420px;margin:0 auto;line-height:1.6;">${t('audioEmptyDesc')}</div>
`;
studioWrapper.appendChild(emptyGuide);
} else {
characters.forEach(charData => {
grid.appendChild(renderCharacterCard(charData));
});
studioWrapper.appendChild(grid);
}
} catch (e) {
console.error('Failed to load audio voices:', e);
}
container.appendChild(studioWrapper);
}