feat(frontend): engine dropdown + emotion dual mode

- Replace engine buttons with <select> dropdown
- Add ENGINE_CONFIG for centralized engine management
- Add global/per-sentence emotion UI mode switching
- Add #engine-select dark theme styles
- Remove old .switch-btn and .engine-icon styles
- Reset emotion state on engine switch
This commit is contained in:
lanhao
2026-07-01 00:48:05 +08:00
parent 81460f06d7
commit a0346d05ba
3 changed files with 88 additions and 42 deletions
+47 -10
View File
@@ -3,10 +3,23 @@
*/ */
const CONFIG = { const CONFIG = {
API_BASE_URL: '', // 空字符串表示使用相对路径,通过Nginx代理 API_BASE_URL: '',
MAX_CHARS: 2000, MAX_CHARS: 2000,
}; };
const ENGINE_CONFIG = {
minimax: {
name: 'MiniMax',
emotionMode: 'global',
color: '#00d4aa'
},
cosyvoice: {
name: 'CosyVoice',
emotionMode: 'global',
color: '#ff6b6b'
}
};
// 情绪按钮与 API emotion 值的映射 // 情绪按钮与 API emotion 值的映射
const EMOTION_MAP = { const EMOTION_MAP = {
'happy': { emoji: '😊', label: '开心', apiValue: 'happy' }, 'happy': { emoji: '😊', label: '开心', apiValue: 'happy' },
@@ -30,8 +43,10 @@ const elements = {
editor: document.getElementById('editor'), editor: document.getElementById('editor'),
editorPlaceholder: document.getElementById('editorPlaceholder'), editorPlaceholder: document.getElementById('editorPlaceholder'),
charCount: document.getElementById('charCount'), charCount: document.getElementById('charCount'),
switchBtns: document.querySelectorAll('.switch-btn'), engineSelect: document.getElementById('engine-select'),
emotionBtns: document.querySelectorAll('.emotion-btn'), emotionBtns: document.querySelectorAll('.emotion-btn'),
emotionGlobal: document.getElementById('emotion-global'),
emotionPerSentence: document.getElementById('emotion-per-sentence'),
speedSlider: document.getElementById('speedSlider'), speedSlider: document.getElementById('speedSlider'),
speedValue: document.getElementById('speedValue'), speedValue: document.getElementById('speedValue'),
pitchSlider: document.getElementById('pitchSlider'), pitchSlider: document.getElementById('pitchSlider'),
@@ -143,12 +158,34 @@ function updateSliderDisplay() {
function switchEngine(engine) { function switchEngine(engine) {
state.currentEngine = engine; state.currentEngine = engine;
elements.switchBtns.forEach(btn => { const config = ENGINE_CONFIG[engine];
btn.classList.toggle('active', btn.dataset.engine === engine); if (!config) return;
});
const engineName = engine === 'minimax' ? 'MiniMax' : 'CosyVoice'; // Update engine badge
document.getElementById('engineBadge').textContent = `蓝皓 · ${engineName}`; const badge = document.getElementById('engineBadge');
showToast(`已切换到${engineName}引擎`); badge.textContent = `蓝皓 · ${config.name}`;
badge.style.background = config.color;
// Update engine select value
elements.engineSelect.value = engine;
// Switch emotion UI mode
updateEmotionMode(config.emotionMode);
// Reset emotion state
clearEmotionSelection();
showToast(`已切换到${config.name}引擎`);
}
function updateEmotionMode(mode) {
if (mode === 'global') {
elements.emotionGlobal.style.display = 'flex';
elements.emotionPerSentence.style.display = 'none';
} else if (mode === 'perSentence') {
elements.emotionGlobal.style.display = 'none';
elements.emotionPerSentence.style.display = 'block';
}
} }
async function generateAudio() { async function generateAudio() {
@@ -290,8 +327,8 @@ function downloadAudio() {
elements.editor.addEventListener('input', updateCharCount); elements.editor.addEventListener('input', updateCharCount);
elements.editor.addEventListener('paste', () => { setTimeout(updateCharCount, 0); }); elements.editor.addEventListener('paste', () => { setTimeout(updateCharCount, 0); });
elements.switchBtns.forEach(btn => { elements.engineSelect.addEventListener('change', (e) => {
btn.addEventListener('click', () => switchEngine(btn.dataset.engine)); switchEngine(e.target.value);
}); });
elements.emotionBtns.forEach(btn => { elements.emotionBtns.forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
+16 -15
View File
@@ -37,24 +37,25 @@
<div class="toolbar"> <div class="toolbar">
<div class="toolbar-group"> <div class="toolbar-group">
<span class="toolbar-label">引擎:</span> <span class="toolbar-label">引擎:</span>
<button class="switch-btn active" data-engine="minimax"> <select id="engine-select">
<span class="engine-icon minimax-icon">M</span> <option value="minimax">MiniMax</option>
MiniMax <option value="cosyvoice">CosyVoice</option>
</button> </select>
<button class="switch-btn" data-engine="cosyvoice">
<span class="engine-icon cosyvoice-icon">C</span>
CosyVoice
</button>
</div> </div>
<div class="toolbar-group emotion-group"> <div class="toolbar-group emotion-group">
<span class="toolbar-label">情绪:</span> <span class="toolbar-label">情绪:</span>
<button class="emotion-btn" data-emotion="happy" title="开心愉快">😊</button> <div id="emotion-global">
<button class="emotion-btn" data-emotion="sad" title="悲伤">😢</button> <button class="emotion-btn" data-emotion="happy" title="开心愉快">😊</button>
<button class="emotion-btn" data-emotion="angry" title="激昂">🔥</button> <button class="emotion-btn" data-emotion="sad" title="悲伤">😢</button>
<button class="emotion-btn" data-emotion="surprised" title="惊讶"></button> <button class="emotion-btn" data-emotion="angry" title="激昂">🔥</button>
<button class="emotion-btn" data-emotion="fearful" title="害怕">😨</button> <button class="emotion-btn" data-emotion="surprised" title="惊讶"></button>
<button class="emotion-btn" data-emotion="disgusted" title="厌恶">😒</button> <button class="emotion-btn" data-emotion="fearful" title="害怕">😨</button>
<span class="toolbar-hint">← 选择整段语音的情绪风格</span> <button class="emotion-btn" data-emotion="disgusted" title="厌恶">😒</button>
<span class="toolbar-hint">← 选择整段语音的情绪风格</span>
</div>
<div id="emotion-per-sentence" style="display:none">
逐句标注模式:选中句子后点击情绪标签(即将支持)
</div>
</div> </div>
</div> </div>
+25 -17
View File
@@ -121,30 +121,38 @@ body {
.toolbar-label { font-size: 0.85rem; color: var(--text-secondary); } .toolbar-label { font-size: 0.85rem; color: var(--text-secondary); }
.toolbar-hint { font-size: 0.7rem; color: var(--text-muted); margin-left: 8px; } .toolbar-hint { font-size: 0.7rem; color: var(--text-muted); margin-left: 8px; }
.switch-btn { #engine-select {
display: flex; padding: 6px 12px;
align-items: center; background: #1a1a25;
gap: 4px; border: 1px solid #2a2a3a;
padding: 4px 12px;
background: transparent;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
color: var(--text-secondary); color: #ffffff;
font-size: 0.85rem; font-size: 0.85rem;
cursor: pointer; cursor: pointer;
transition: var(--transition-fast); transition: var(--transition-fast);
outline: none;
-webkit-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2371717a' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
padding-right: 28px;
} }
.switch-btn:hover { border-color: var(--border-hover); color: var(--text-primary); } #engine-select:hover { border-color: var(--border-hover); }
.switch-btn.active { background: var(--bg-card); border-color: var(--accent-primary); color: var(--text-primary); } #engine-select:focus { border-color: var(--accent-primary); }
#engine-select option { background: #1a1a25; color: #ffffff; }
.engine-icon { #emotion-global {
width: 18px; height: 18px; display: flex;
border-radius: var(--radius-sm); align-items: center;
display: flex; align-items: center; justify-content: center; gap: 4px;
font-size: 0.65rem; font-weight: 700; }
#emotion-per-sentence {
color: #71717a;
padding: 8px;
font-size: 0.85rem;
} }
.minimax-icon { background: var(--minimax-color); color: #0a0a0f; }
.cosyvoice-icon { background: var(--cosyvoice-color); color: white; }
.emotion-btn { .emotion-btn {
width: 32px; height: 32px; width: 32px; height: 32px;