394 lines
15 KiB
JavaScript
394 lines
15 KiB
JavaScript
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));
|
|
});
|
|
|
|
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 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';
|
|
|
|
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);
|
|
}
|