Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_audio_studio.js
T
DemonGatanjieuandClaude Opus 5.5 3ec4b1de61 chore(checkpoint): save uncommitted Script Director work from previous session
未完成/验证待完成:剧本台(ui_script_director.js、13-script-director.css)、
音频面板返回状态与 locales 文案来自上一轮会话,尚未验证。仅作为恢复点,
后续修复另行提交。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W411Z69FC3em9CzpeqEub5
2026-09-23 07:42:41 +00:00

577 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from '../../../scripts/app.js';
import { t } from './interface_settings.js';
import { stopGalleryAudio } from './ui_audio_gallery.js';
import { openAudioUploaderModal } from './ui_audio_uploader.js';
import {
openScriptDirector,
closeScriptDirector,
handleVoiceSelectionForScript,
isScriptDirectorActive
} from './ui_script_director.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 freshUrl = url.includes('?') ? `${url}&_t=${Date.now()}` : `${url}?_t=${Date.now()}`;
const audio = new Audio(freshUrl);
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, characterName) {
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.onclick = () => {
if (isScriptDirectorActive()) {
handleVoiceSelectionForScript({
character: characterName,
audio_path: slice.syntax_tag
});
}
};
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, charData.character));
});
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 = '6px';
btn.style.border = '1px solid rgba(255, 255, 255, 0.1)';
btn.style.background = 'rgba(255, 255, 255, 0.05)';
btn.style.color = '#d1d5db';
btn.style.fontSize = '12px';
btn.style.fontWeight = '500';
btn.style.cursor = 'pointer';
btn.style.transition = 'all 0.2s ease';
btn.style.boxShadow = 'none';
btn.style.whiteSpace = 'nowrap';
btn.onmouseenter = () => {
btn.style.background = 'rgba(255, 255, 255, 0.1)';
btn.style.borderColor = 'rgba(255, 255, 255, 0.2)';
};
btn.onmouseleave = () => {
btn.style.background = 'rgba(255, 255, 255, 0.05)';
btn.style.borderColor = 'rgba(255, 255, 255, 0.1)';
};
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 createAddVoiceButton(onVoiceAdded) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'anomalous-audio-add-voice-btn';
btn.innerHTML = `<span>➕</span> <span>${t('audioAddVoice')}</span>`;
btn.style.display = 'inline-flex';
btn.style.alignItems = 'center';
btn.style.gap = '6px';
btn.style.padding = '5px 12px';
btn.style.borderRadius = '6px';
btn.style.border = '1px solid rgba(255, 255, 255, 0.1)';
btn.style.background = 'rgba(255, 255, 255, 0.05)';
btn.style.color = '#d1d5db';
btn.style.fontSize = '12px';
btn.style.fontWeight = '500';
btn.style.cursor = 'pointer';
btn.style.transition = 'all 0.2s ease';
btn.style.boxShadow = 'none';
btn.style.whiteSpace = 'nowrap';
btn.onmouseenter = () => {
btn.style.background = 'rgba(255, 255, 255, 0.1)';
btn.style.borderColor = 'rgba(255, 255, 255, 0.2)';
};
btn.onmouseleave = () => {
btn.style.background = 'rgba(255, 255, 255, 0.05)';
btn.style.borderColor = 'rgba(255, 255, 255, 0.1)';
};
btn.onclick = () => {
openAudioUploaderModal({
onSaved: (slice) => {
if (typeof onVoiceAdded === 'function') {
onVoiceAdded(slice);
}
}
});
};
return btn;
}
function createScriptDirectorButton(container) {
const btn = document.createElement('button');
btn.className = 'anomalous-audio-action-btn';
// An icon representing a script or director's clapperboard
btn.innerHTML = `<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="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>`;
const textSpan = document.createElement('span');
textSpan.style.whiteSpace = 'nowrap';
textSpan.textContent = t('scriptDirectorOpen') || 'Script Director';
btn.appendChild(textSpan);
btn.onclick = () => {
if (isScriptDirectorActive()) {
closeScriptDirector();
btn.classList.remove('active');
} else {
openScriptDirector(container);
btn.classList.add('active');
}
};
return btn;
}
function renderStudioToolbar(onSearch, onVoiceAdded, container) {
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.className = 'anomalous-audio-right-actions';
rightActions.style.flexWrap = 'wrap';
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 addVoiceBtn = createAddVoiceButton(onVoiceAdded);
const loadWfBtn = createLoadWorkflowButton();
const scriptDirBtn = createScriptDirectorButton(container);
rightActions.appendChild(scriptDirBtn);
rightActions.appendChild(addVoiceBtn);
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';
});
}, () => {
renderAudioStudio(container, filter);
}, container);
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);
}