feat: 字数限制2000 + MiniMax emotion参数 + 前端情绪全局选择模式
This commit is contained in:
+47
-97
@@ -5,18 +5,21 @@
|
||||
const CONFIG = {
|
||||
API_BASE_URL: '', // 空字符串表示使用相对路径,通过Nginx代理
|
||||
MAX_CHARS: 2000,
|
||||
EMOTIONS: {
|
||||
'happy': { name: '开心', color: '#fbbf24' },
|
||||
'sad': { name: '悲伤', color: '#60a5fa' },
|
||||
'excited': { name: '激动', color: '#f97316' },
|
||||
'surprised': { name: '惊讶', color: '#a855f7' },
|
||||
'fearful': { name: '害怕', color: '#ec4899' },
|
||||
'disgusted': { name: '厌恶', color: '#14b8a6' }
|
||||
}
|
||||
};
|
||||
|
||||
// 情绪按钮与 API emotion 值的映射
|
||||
const EMOTION_MAP = {
|
||||
'happy': { emoji: '😊', label: '开心', apiValue: 'happy' },
|
||||
'sad': { emoji: '😢', label: '悲伤', apiValue: 'sad' },
|
||||
'angry': { emoji: '🔥', label: '激昂', apiValue: 'angry' },
|
||||
'surprised': { emoji: '❗', label: '惊讶', apiValue: 'surprised' },
|
||||
'fearful': { emoji: '😨', label: '紧张', apiValue: 'fearful' },
|
||||
'disgusted': { emoji: '😒', label: '厌恶', apiValue: 'disgusted' },
|
||||
};
|
||||
|
||||
const state = {
|
||||
currentEngine: 'minimax',
|
||||
currentEmotion: 'neutral',
|
||||
selectedEmotion: null,
|
||||
generatedAudio: null,
|
||||
isPlaying: false,
|
||||
@@ -90,85 +93,40 @@ function getSelectedText() {
|
||||
return window.getSelection().toString().trim();
|
||||
}
|
||||
|
||||
function applyEmotionTag(emotion, emotionName) {
|
||||
const selection = window.getSelection();
|
||||
if (!selection.rangeCount) {
|
||||
showToast('请先选中要添加情绪的文字', 'error');
|
||||
return;
|
||||
/**
|
||||
* 切换情绪状态(全局选择模式)
|
||||
* 点击一个 emoji 按钮设置当前情绪,再点取消;同时只能选一个
|
||||
*/
|
||||
function applyEmotionTag(emotion) {
|
||||
const emotionInfo = EMOTION_MAP[emotion];
|
||||
if (!emotionInfo) return;
|
||||
|
||||
if (state.currentEmotion === emotionInfo.apiValue) {
|
||||
// 再次点击同一情绪,取消选择
|
||||
state.currentEmotion = 'neutral';
|
||||
state.selectedEmotion = null;
|
||||
elements.emotionBtns.forEach(btn => btn.classList.remove('active'));
|
||||
showToast('已取消情绪选择');
|
||||
} else {
|
||||
// 选择新情绪
|
||||
state.currentEmotion = emotionInfo.apiValue;
|
||||
state.selectedEmotion = emotion;
|
||||
elements.emotionBtns.forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.emotion === emotion);
|
||||
});
|
||||
showToast(`已选择「${emotionInfo.label}」情绪风格`, 'success');
|
||||
}
|
||||
const selectedText = selection.toString().trim();
|
||||
if (!selectedText) {
|
||||
showToast('请先选中要添加情绪的文字', 'error');
|
||||
return;
|
||||
}
|
||||
const tagHtml = `<span class="emotion-tag" data-emotion="${emotion}" contenteditable="false">[${emotionName}]</span>`;
|
||||
const range = selection.getRangeAt(0);
|
||||
const selectedContent = range.extractContents();
|
||||
const wrapper = document.createElement('span');
|
||||
wrapper.innerHTML = tagHtml;
|
||||
const textNode = document.createTextNode(selectedText);
|
||||
wrapper.appendChild(textNode);
|
||||
range.insertNode(wrapper);
|
||||
selection.removeAllRanges();
|
||||
const space = document.createTextNode(' ');
|
||||
wrapper.after(space);
|
||||
elements.editor.focus();
|
||||
showToast(`已添加「${emotionName}」情绪`, 'success');
|
||||
clearEmotionSelection();
|
||||
}
|
||||
|
||||
function clearEmotionSelection() {
|
||||
state.selectedEmotion = null;
|
||||
state.currentEmotion = 'neutral';
|
||||
elements.emotionBtns.forEach(btn => btn.classList.remove('active'));
|
||||
}
|
||||
|
||||
function getPlainText() {
|
||||
// Remove emotion tags completely - don't include any emotion labels in the text
|
||||
const clone = elements.editor.cloneNode(true);
|
||||
|
||||
// Completely remove emotion tags
|
||||
const tags = clone.querySelectorAll('.emotion-tag');
|
||||
tags.forEach(tag => tag.remove());
|
||||
|
||||
return clone.innerText.trim();
|
||||
}
|
||||
|
||||
// 情绪标签映射:前端显示 → MiniMax标签 / CosyVoice指令
|
||||
const EMOTION_LABELS = {
|
||||
'happy': { miniMax: '[开心]', cosyvoice: '请用开心的语气' },
|
||||
'excited': { miniMax: '[激昂]', cosyvoice: '请用激昂的语气' },
|
||||
'surprised': { miniMax: '[惊讶]', cosyvoice: '请用惊讶的语气' },
|
||||
'sad': { miniMax: '[悲伤]', cosyvoice: '请用悲伤的语气' },
|
||||
'fearful': { miniMax: '[恐惧]', cosyvoice: '请用恐惧的语气' },
|
||||
'disgusted': { miniMax: '[厌恶]', cosyvoice: '请用厌恶的语气' }
|
||||
};
|
||||
|
||||
function getTextWithEmotionTags(engine) {
|
||||
const clone = elements.editor.cloneNode(true);
|
||||
|
||||
if (engine === 'minimax') {
|
||||
const tags = clone.querySelectorAll('.emotion-tag');
|
||||
tags.forEach(tag => {
|
||||
const emotion = tag.getAttribute('data-emotion');
|
||||
const label = EMOTION_LABELS[emotion]?.miniMax || `[${emotion}]`;
|
||||
const textNode = document.createTextNode(label);
|
||||
tag.parentNode.replaceChild(textNode, tag);
|
||||
});
|
||||
return clone.innerText.trim();
|
||||
} else {
|
||||
const tags = clone.querySelectorAll('.emotion-tag');
|
||||
tags.forEach(tag => tag.remove());
|
||||
return clone.innerText.trim();
|
||||
}
|
||||
}
|
||||
|
||||
function getFirstEmotionInstruction() {
|
||||
const firstEmotionTag = elements.editor.querySelector('.emotion-tag');
|
||||
if (firstEmotionTag) {
|
||||
const emotion = firstEmotionTag.getAttribute('data-emotion');
|
||||
return EMOTION_LABELS[emotion]?.cosyvoice || null;
|
||||
}
|
||||
return null;
|
||||
// 直接返回纯文本,不再处理情绪标签
|
||||
return elements.editor.innerText.trim();
|
||||
}
|
||||
|
||||
function clearEditor() {
|
||||
@@ -202,23 +160,23 @@ async function generateAudio() {
|
||||
showLoading('正在生成音频...');
|
||||
elements.generateBtn.disabled = true;
|
||||
try {
|
||||
// 根据引擎获取带情绪标签的文本
|
||||
const textForApi = getTextWithEmotionTags(state.currentEngine);
|
||||
// 获取CosyVoice的情绪指令
|
||||
const emotionInstruction = getFirstEmotionInstruction();
|
||||
|
||||
const requestData = {
|
||||
text: textForApi,
|
||||
text: text,
|
||||
engine: state.currentEngine,
|
||||
speed: parseFloat(elements.speedSlider.value),
|
||||
pitch: parseInt(elements.pitchSlider.value),
|
||||
volume: parseFloat(elements.volumeSlider.value)
|
||||
volume: parseFloat(elements.volumeSlider.value),
|
||||
emotion: state.currentEmotion
|
||||
};
|
||||
|
||||
|
||||
// CosyVoice使用instruction参数传递情绪
|
||||
if (state.currentEngine === 'cosyvoice' && emotionInstruction) {
|
||||
requestData.instruction = emotionInstruction;
|
||||
if (state.currentEngine === 'cosyvoice' && state.currentEmotion !== 'neutral') {
|
||||
const emotionInfo = Object.values(EMOTION_MAP).find(e => e.apiValue === state.currentEmotion);
|
||||
if (emotionInfo) {
|
||||
requestData.instruction = `请用${emotionInfo.label}的语气`;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch('/api/synthesize', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -338,15 +296,7 @@ elements.switchBtns.forEach(btn => {
|
||||
elements.emotionBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const emotion = btn.dataset.emotion;
|
||||
const emotionName = CONFIG.EMOTIONS[emotion]?.name || emotion;
|
||||
if (state.selectedEmotion === emotion) {
|
||||
clearEmotionSelection();
|
||||
} else {
|
||||
elements.emotionBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
state.selectedEmotion = emotion;
|
||||
applyEmotionTag(emotion, emotionName);
|
||||
}
|
||||
applyEmotionTag(emotion);
|
||||
});
|
||||
});
|
||||
elements.speedSlider.addEventListener('input', updateSliderDisplay);
|
||||
|
||||
Reference in New Issue
Block a user