ai / index.html
wwqg's picture
你这api设置里面只有api站点和一个密钥,数量对不上,而且也没有具体调用模型的设置 - Follow Up Deployment
4d8d1c7 verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI语音播客工作室</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* 自定义CSS补充Tailwind */
.waveform {
background: linear-gradient(90deg, #3b82f6 0%, #3b82f6 var(--progress, 0%), #e5e7eb var(--progress, 0%), #e5e7eb 100%);
}
.voice-option:hover .play-sample {
display: block;
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body class="bg-gray-50 text-gray-800 min-h-screen flex flex-col">
<!-- 导航栏 -->
<nav class="bg-indigo-600 text-white shadow-lg">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fas fa-microphone-alt text-2xl"></i>
<h1 class="text-xl font-bold">AI语音播客工作室</h1>
</div>
<div class="hidden md:flex space-x-6">
<a href="#" class="hover:text-indigo-200 transition">首页</a>
<a href="#" class="hover:text-indigo-200 transition">教程</a>
<a href="#" class="hover:text-indigo-200 transition">示例</a>
<a href="#" class="hover:text-indigo-200 transition">关于</a>
</div>
<button class="md:hidden text-xl">
<i class="fas fa-bars"></i>
</button>
</div>
</nav>
<!-- 主内容区 -->
<main class="flex-grow container mx-auto px-4 py-8">
<div class="max-w-6xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- 左侧输入和设置面板 -->
<div class="lg:col-span-2 space-y-6">
<!-- 文本输入区 -->
<div class="bg-white rounded-xl shadow-md overflow-hidden">
<div class="bg-indigo-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center">
<h2 class="font-semibold text-indigo-700">输入文本</h2>
<div class="flex space-x-2">
<button id="clear-text" class="text-gray-500 hover:text-gray-700 transition">
<i class="fas fa-trash-alt"></i> 清空
</button>
<button id="sample-text" class="text-indigo-600 hover:text-indigo-800 transition">
<i class="fas fa-lightbulb"></i> 示例
</button>
</div>
</div>
<div class="p-4">
<textarea id="input-text" rows="10" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" placeholder="在这里输入您想要转换为语音的文本..."></textarea>
</div>
</div>
<!-- 语音设置 -->
<div class="bg-white rounded-xl shadow-md overflow-hidden">
<div class="bg-indigo-50 px-4 py-3 border-b border-gray-200">
<h2 class="font-semibold text-indigo-700">语音设置</h2>
</div>
<div class="p-4 space-y-4">
<!-- 语音选择 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">选择语音</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3" id="voice-options">
<!-- 语音选项将通过JS动态生成 -->
</div>
</div>
<!-- 语音参数 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="speed" class="block text-sm font-medium text-gray-700">语速 <span id="speed-value" class="text-indigo-600">1.0</span></label>
<input type="range" id="speed" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div>
<label for="pitch" class="block text-sm font-medium text-gray-700">音高 <span id="pitch-value" class="text-indigo-600">1.0</span></label>
<input type="range" id="pitch" min="0.5" max="1.5" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div>
<label for="volume" class="block text-sm font-medium text-gray-700">音量 <span id="volume-value" class="text-indigo-600">1.0</span></label>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
</div>
<!-- 情感设置 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">情感风格</label>
<div class="flex flex-wrap gap-2">
<button class="emotion-btn px-3 py-1 bg-gray-100 hover:bg-indigo-100 text-gray-800 rounded-full border border-gray-300 transition" data-emotion="neutral">中性</button>
<button class="emotion-btn px-3 py-1 bg-gray-100 hover:bg-indigo-100 text-gray-800 rounded-full border border-gray-300 transition" data-emotion="happy">快乐</button>
<button class="emotion-btn px-3 py-1 bg-gray-100 hover:bg-indigo-100 text-gray-800 rounded-full border border-gray-300 transition" data-emotion="sad">悲伤</button>
<button class="emotion-btn px-3 py-1 bg-gray-100 hover:bg-indigo-100 text-gray-800 rounded-full border border-gray-300 transition" data-emotion="angry">愤怒</button>
<button class="emotion-btn px-3 py-1 bg-gray-100 hover:bg-indigo-100 text-gray-800 rounded-full border border-gray-300 transition" data-emotion="excited">兴奋</button>
<button class="emotion-btn px-3 py-1 bg-gray-100 hover:bg-indigo-100 text-gray-800 rounded-full border border-gray-300 transition" data-emotion="calm">平静</button>
</div>
</div>
</div>
</div>
<!-- API设置 -->
<div class="bg-white rounded-xl shadow-md overflow-hidden">
<div class="bg-indigo-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center">
<h2 class="font-semibold text-indigo-700">API设置</h2>
<button id="toggle-api" class="text-indigo-600 hover:text-indigo-800 transition">
<i class="fas fa-cog"></i> 高级设置
</button>
</div>
<div id="api-settings" class="p-4 hidden space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="tts-api" class="block text-sm font-medium text-gray-700 mb-1">TTS API端点</label>
<input type="text" id="tts-api" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" placeholder="https://api.example.com/tts">
</div>
<div>
<label for="tts-model" class="block text-sm font-medium text-gray-700 mb-1">TTS模型</label>
<select id="tts-model" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
<option value="vits">VITS (默认)</option>
<option value="fastspeech2">FastSpeech2</option>
<option value="tacotron2">Tacotron2</option>
<option value="glowtts">Glow-TTS</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="nlp-api" class="block text-sm font-medium text-gray-700 mb-1">NLP API端点</label>
<input type="text" id="nlp-api" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" placeholder="https://api.example.com/nlp">
</div>
<div>
<label for="nlp-model" class="block text-sm font-medium text-gray-700 mb-1">NLP模型</label>
<select id="nlp-model" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
<option value="bert">BERT (默认)</option>
<option value="gpt">GPT</option>
<option value="roberta">RoBERTa</option>
<option value="t5">T5</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="api-key" class="block text-sm font-medium text-gray-700 mb-1">API密钥</label>
<input type="password" id="api-key" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" placeholder="输入您的API密钥">
</div>
<div>
<label for="api-region" class="block text-sm font-medium text-gray-700 mb-1">服务区域</label>
<select id="api-region" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition">
<option value="global">全球 (默认)</option>
<option value="cn-east-1">华东1</option>
<option value="cn-north-1">华北1</option>
<option value="cn-south-1">华南1</option>
<option value="us-east-1">美东1</option>
<option value="eu-west-1">欧洲西部1</option>
</select>
</div>
</div>
<div class="flex justify-end">
<button id="save-api" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition">
保存设置
</button>
</div>
</div>
</div>
</div>
<!-- 右侧输出和控制面板 -->
<div class="space-y-6">
<!-- 播放控制 -->
<div class="bg-white rounded-xl shadow-md overflow-hidden">
<div class="bg-indigo-50 px-4 py-3 border-b border-gray-200">
<h2 class="font-semibold text-indigo-700">播放控制</h2>
</div>
<div class="p-4 space-y-4">
<!-- 波形图占位 -->
<div class="waveform h-12 rounded-lg" id="waveform" style="--progress: 0%"></div>
<!-- 控制按钮 -->
<div class="flex justify-center space-x-4">
<button id="generate-btn" class="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition flex items-center space-x-2">
<i class="fas fa-magic"></i>
<span>生成语音</span>
</button>
<button id="play-btn" disabled class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition flex items-center space-x-2">
<i class="fas fa-play"></i>
<span>播放</span>
</button>
<button id="pause-btn" disabled class="px-6 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition flex items-center space-x-2">
<i class="fas fa-pause"></i>
<span>暂停</span>
</button>
</div>
<!-- 进度和时间 -->
<div class="flex justify-between items-center text-sm text-gray-600">
<span id="current-time">00:00</span>
<span id="total-time">00:00</span>
</div>
<!-- 下载选项 -->
<div class="pt-4 border-t border-gray-200">
<label class="block text-sm font-medium text-gray-700 mb-2">下载格式</label>
<div class="flex space-x-3">
<button id="download-mp3" disabled class="px-4 py-2 bg-gray-100 hover:bg-indigo-100 text-gray-800 rounded-lg border border-gray-300 transition flex items-center space-x-2">
<i class="fas fa-file-audio"></i>
<span>MP3</span>
</button>
<button id="download-wav" disabled class="px-4 py-2 bg-gray-100 hover:bg-indigo-100 text-gray-800 rounded-lg border border-gray-300 transition flex items-center space-x-2">
<i class="fas fa-file-waveform"></i>
<span>WAV</span>
</button>
</div>
</div>
</div>
</div>
<!-- 历史记录 -->
<div class="bg-white rounded-xl shadow-md overflow-hidden">
<div class="bg-indigo-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center">
<h2 class="font-semibold text-indigo-700">历史记录</h2>
<button id="clear-history" class="text-gray-500 hover:text-gray-700 transition text-sm">
<i class="fas fa-trash-alt"></i> 清空
</button>
</div>
<div class="p-4">
<div id="history-list" class="space-y-2 max-h-60 overflow-y-auto">
<!-- 历史记录项将通过JS动态生成 -->
<div class="text-center text-gray-500 py-4">
<i class="fas fa-history text-2xl mb-2"></i>
<p>暂无历史记录</p>
</div>
</div>
</div>
</div>
<!-- 提示和建议 -->
<div class="bg-indigo-50 rounded-xl p-4 border border-indigo-100">
<h3 class="font-medium text-indigo-700 mb-2 flex items-center">
<i class="fas fa-lightbulb mr-2"></i> 使用小贴士
</h3>
<ul class="text-sm text-indigo-800 space-y-1">
<li>• 使用不同的情感风格来匹配文本内容</li>
<li>• 调整语速和音高以获得最佳效果</li>
<li>• 长文本会自动分段处理</li>
<li>• 点击示例按钮获取灵感</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="bg-gray-800 text-white py-6">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<div class="flex items-center space-x-2">
<i class="fas fa-microphone-alt text-2xl text-indigo-400"></i>
<span class="text-xl font-bold">AI语音播客工作室</span>
</div>
<p class="text-gray-400 text-sm mt-1">让文字生动起来</p>
</div>
<div class="flex space-x-6">
<a href="#" class="hover:text-indigo-400 transition">隐私政策</a>
<a href="#" class="hover:text-indigo-400 transition">服务条款</a>
<a href="#" class="hover:text-indigo-400 transition">联系我们</a>
</div>
</div>
<div class="border-t border-gray-700 mt-6 pt-6 text-center text-gray-400 text-sm">
<p>© 2023 AI语音播客工作室. 保留所有权利.</p>
</div>
</div>
</footer>
<!-- 音频元素 -->
<audio id="audio-player" class="hidden"></audio>
<!-- 脚本 -->
<script>
// 模块化代码结构
const App = (() => {
// 状态管理
const state = {
voices: [
{ id: 'voice-1', name: '标准女声', gender: 'female', sampleText: '您好,欢迎使用AI语音播客工作室。', selected: true },
{ id: 'voice-2', name: '标准男声', gender: 'male', sampleText: '科技改变生活,语音连接未来。' },
{ id: 'voice-3', name: '甜美女声', gender: 'female', sampleText: '今天的天气真好,阳光明媚,适合外出散步。' },
{ id: 'voice-4', name: '磁性男声', gender: 'male', sampleText: '深度思考是人类最宝贵的财富之一。' },
{ id: 'voice-5', name: '儿童声音', gender: 'child', sampleText: '我最喜欢听故事了,能给我讲一个吗?' },
{ id: 'voice-6', name: '老年声音', gender: 'senior', sampleText: '岁月如歌,人生如梦,珍惜当下每一刻。' }
],
currentVoice: null,
emotion: 'neutral',
audioBlob: null,
history: JSON.parse(localStorage.getItem('ttsHistory')) || [],
apiSettings: JSON.parse(localStorage.getItem('apiSettings')) || {
ttsApi: '',
ttsModel: 'vits',
nlpApi: '',
nlpModel: 'bert',
apiKey: '',
apiRegion: 'global'
},
isPlaying: false,
audioDuration: 0,
currentAudioTime: 0,
audioContext: null,
analyser: null,
audioSource: null,
animationFrameId: null
};
// DOM元素
const elements = {
inputText: document.getElementById('input-text'),
voiceOptions: document.getElementById('voice-options'),
speedSlider: document.getElementById('speed'),
speedValue: document.getElementById('speed-value'),
pitchSlider: document.getElementById('pitch'),
pitchValue: document.getElementById('pitch-value'),
volumeSlider: document.getElementById('volume'),
volumeValue: document.getElementById('volume-value'),
emotionBtns: document.querySelectorAll('.emotion-btn'),
generateBtn: document.getElementById('generate-btn'),
playBtn: document.getElementById('play-btn'),
pauseBtn: document.getElementById('pause-btn'),
audioPlayer: document.getElementById('audio-player'),
waveform: document.getElementById('waveform'),
currentTime: document.getElementById('current-time'),
totalTime: document.getElementById('total-time'),
downloadMp3: document.getElementById('download-mp3'),
downloadWav: document.getElementById('download-wav'),
clearText: document.getElementById('clear-text'),
sampleText: document.getElementById('sample-text'),
historyList: document.getElementById('history-list'),
clearHistory: document.getElementById('clear-history'),
toggleApi: document.getElementById('toggle-api'),
apiSettings: document.getElementById('api-settings'),
ttsApi: document.getElementById('tts-api'),
nlpApi: document.getElementById('nlp-api'),
apiKey: document.getElementById('api-key'),
saveApi: document.getElementById('save-api')
};
// 初始化函数
const init = () => {
renderVoiceOptions();
bindEvents();
loadApiSettings();
renderHistory();
setupAudioContext();
// 设置默认语音
state.currentVoice = state.voices.find(v => v.selected);
};
// 渲染语音选项
const renderVoiceOptions = () => {
elements.voiceOptions.innerHTML = state.voices.map(voice => `
<div class="voice-option relative p-3 border rounded-lg cursor-pointer transition ${voice.selected ? 'border-indigo-500 bg-indigo-50' : 'border-gray-200 hover:border-indigo-300'}" data-voice-id="${voice.id}">
<div class="flex items-center">
<div class="mr-3">
<i class="fas fa-${voice.gender === 'male' ? 'male' : voice.gender === 'female' ? 'female' : voice.gender === 'child' ? 'child' : 'user'} text-xl ${voice.gender === 'male' ? 'text-blue-500' : voice.gender === 'female' ? 'text-pink-500' : 'text-purple-500'}"></i>
</div>
<div>
<h3 class="font-medium">${voice.name}</h3>
<p class="text-sm text-gray-600 truncate">${voice.sampleText}</p>
</div>
</div>
<button class="play-sample absolute right-3 top-3 hidden text-indigo-600 hover:text-indigo-800" data-sample-text="${voice.sampleText}">
<i class="fas fa-play-circle text-lg"></i>
</button>
</div>
`).join('');
};
// 绑定事件
const bindEvents = () => {
// 语音选择
document.querySelectorAll('.voice-option').forEach(el => {
el.addEventListener('click', (e) => {
const voiceId = el.getAttribute('data-voice-id');
selectVoice(voiceId);
// 阻止事件冒泡,避免触发播放样本
if (e.target.closest('.play-sample')) {
e.stopPropagation();
}
});
});
// 播放样本
document.querySelectorAll('.play-sample').forEach(btn => {
btn.addEventListener('click', (e) => {
const sampleText = btn.getAttribute('data-sample-text');
playSample(sampleText);
});
});
// 滑块控制
elements.speedSlider.addEventListener('input', () => {
const value = elements.speedSlider.value;
elements.speedValue.textContent = value;
});
elements.pitchSlider.addEventListener('input', () => {
const value = elements.pitchSlider.value;
elements.pitchValue.textContent = value;
});
elements.volumeSlider.addEventListener('input', () => {
const value = elements.volumeSlider.value;
elements.volumeValue.textContent = value;
if (elements.audioPlayer) {
elements.audioPlayer.volume = value;
}
});
// 情感按钮
elements.emotionBtns.forEach(btn => {
btn.addEventListener('click', () => {
state.emotion = btn.getAttribute('data-emotion');
updateEmotionButtons();
});
});
// 生成按钮
elements.generateBtn.addEventListener('click', generateAudio);
// 播放控制
elements.playBtn.addEventListener('click', playAudio);
elements.pauseBtn.addEventListener('click', pauseAudio);
// 音频事件
elements.audioPlayer.addEventListener('timeupdate', updateAudioProgress);
elements.audioPlayer.addEventListener('loadedmetadata', () => {
state.audioDuration = elements.audioPlayer.duration;
updateTimeDisplay();
});
elements.audioPlayer.addEventListener('ended', () => {
state.isPlaying = false;
updatePlayButtons();
cancelAnimationFrame(state.animationFrameId);
});
elements.audioPlayer.addEventListener('play', () => {
state.isPlaying = true;
updatePlayButtons();
visualizeAudio();
});
elements.audioPlayer.addEventListener('pause', () => {
state.isPlaying = false;
updatePlayButtons();
cancelAnimationFrame(state.animationFrameId);
});
// 下载按钮
elements.downloadMp3.addEventListener('click', () => downloadAudio('mp3'));
elements.downloadWav.addEventListener('click', () => downloadAudio('wav'));
// 文本操作
elements.clearText.addEventListener('click', () => {
elements.inputText.value = '';
});
elements.sampleText.addEventListener('click', () => {
elements.inputText.value = `欢迎来到AI语音播客工作室!
这是一个示例文本,展示了系统如何处理不同情感的表达。
[快乐] 今天真是美好的一天!阳光明媚,心情愉悦!
[悲伤] 听到这个消息,我感到非常难过...
[愤怒] 这种行为简直不可接受!必须立即停止!
[兴奋] 哇!我简直不敢相信自己的眼睛!太棒了!
[平静] 深呼吸,放松身心,感受当下的宁静。
您可以根据需要调整语音参数,创造出符合场景的语音效果。`;
});
// 历史记录
elements.clearHistory.addEventListener('click', clearHistory);
// API设置
elements.toggleApi.addEventListener('click', toggleApiSettings);
elements.saveApi.addEventListener('click', saveApiSettings);
};
// 选择语音
const selectVoice = (voiceId) => {
state.voices.forEach(voice => {
voice.selected = voice.id === voiceId;
});
state.currentVoice = state.voices.find(v => v.id === voiceId);
renderVoiceOptions();
};
// 播放样本
const playSample = (text) => {
// 在实际应用中,这里会调用TTS API生成样本语音
// 这里使用Web Speech API作为演示
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
const voices = window.speechSynthesis.getVoices();
const voice = voices.find(v => v.name.includes(state.currentVoice.gender === 'male' ? 'Male' : 'Female')) || voices[0];
utterance.voice = voice;
utterance.rate = parseFloat(elements.speedSlider.value);
utterance.pitch = parseFloat(elements.pitchSlider.value);
window.speechSynthesis.speak(utterance);
} else {
alert('您的浏览器不支持语音合成API,请使用Chrome或Edge浏览器。');
}
};
// 更新情感按钮状态
const updateEmotionButtons = () => {
elements.emotionBtns.forEach(btn => {
if (btn.getAttribute('data-emotion') === state.emotion) {
btn.classList.add('bg-indigo-100', 'border-indigo-400', 'text-indigo-800');
btn.classList.remove('bg-gray-100', 'text-gray-800');
} else {
btn.classList.remove('bg-indigo-100', 'border-indigo-400', 'text-indigo-800');
btn.classList.add('bg-gray-100', 'text-gray-800');
}
});
};
// 生成音频
const generateAudio = async () => {
const text = elements.inputText.value.trim();
if (!text) {
alert('请输入要转换的文本');
return;
}
// 显示加载状态
elements.generateBtn.disabled = true;
elements.generateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>生成中...</span>';
try {
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1500));
// 在实际应用中,这里会调用TTS API
// 这里使用模拟的音频数据作为演示
simulateTTSGeneration(text);
// 添加到历史记录
addToHistory(text);
} catch (error) {
console.error('生成语音出错:', error);
alert('生成语音时出错: ' + error.message);
} finally {
elements.generateBtn.disabled = false;
elements.generateBtn.innerHTML = '<i class="fas fa-magic"></i><span>生成语音</span>';
}
};
// 模拟TTS生成
const simulateTTSGeneration = (text) => {
// 创建一个模拟的音频Blob
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = 'sine';
oscillator.frequency.value = state.currentVoice.gender === 'male' ? 120 : 220;
gainNode.gain.value = 0.5;
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// 根据情感调整声音
switch (state.emotion) {
case 'happy':
oscillator.frequency.setValueAtTime(state.currentVoice.gender === 'male' ? 140 : 260, audioContext.currentTime);
break;
case 'sad':
oscillator.frequency.setValueAtTime(state.currentVoice.gender === 'male' ? 100 : 180, audioContext.currentTime);
break;
case 'angry':
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(state.currentVoice.gender === 'male' ? 130 : 230, audioContext.currentTime);
break;
case 'excited':
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(state.currentVoice.gender === 'male' ? 150 : 280, audioContext.currentTime);
break;
case 'calm':
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(state.currentVoice.gender === 'male' ? 110 : 200, audioContext.currentTime);
break;
}
// 模拟音频持续时间 (0.1秒每字符)
const duration = Math.min(text.length * 0.1, 30); // 最长30秒
// 创建音频缓冲区
const sampleRate = 44100;
const numChannels = 1;
const numFrames = sampleRate * duration;
const buffer = audioContext.createBuffer(numChannels, numFrames, sampleRate);
// 填充缓冲区
for (let channel = 0; channel < numChannels; channel++) {
const nowBuffering = buffer.getChannelData(channel);
for (let i = 0; i < numFrames; i++) {
const time = i / sampleRate;
const frequency = state.currentVoice.gender === 'male' ? 120 : 220;
nowBuffering[i] = Math.sin(2 * Math.PI * frequency * time) * 0.5;
}
}
// 转换为Blob
const audioBlob = bufferToWav(buffer);
state.audioBlob = audioBlob;
// 设置音频播放器
const audioUrl = URL.createObjectURL(audioBlob);
elements.audioPlayer.src = audioUrl;
// 启用播放和下载按钮
elements.playBtn.disabled = false;
elements.downloadMp3.disabled = false;
elements.downloadWav.disabled = false;
// 显示成功消息
showNotification('语音生成成功!', 'success');
};
// 将AudioBuffer转换为WAV Blob
const bufferToWav = (buffer) => {
const numChannels = buffer.numberOfChannels;
const sampleRate = buffer.sampleRate;
const length = buffer.length;
const interleaved = new Float32Array(length * numChannels);
for (let channel = 0; channel < numChannels; channel++) {
const channelData = buffer.getChannelData(channel);
for (let i = 0; i < length; i++) {
interleaved[i * numChannels + channel] = channelData[i];
}
}
const bufferLength = length * numChannels * 2;
const arrayBuffer = new ArrayBuffer(44 + bufferLength);
const view = new DataView(arrayBuffer);
// WAV头部
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + bufferLength, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true); // PCM格式
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * numChannels * 2, true);
view.setUint16(32, numChannels * 2, true);
view.setUint16(34, 16, true);
writeString(view, 36, 'data');
view.setUint32(40, bufferLength, true);
// 音频数据
const offset = 44;
for (let i = 0; i < interleaved.length; i++) {
const sample = Math.max(-1, Math.min(1, interleaved[i]));
view.setInt16(offset + i * 2, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);
}
return new Blob([view], { type: 'audio/wav' });
};
const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
// 播放音频
const playAudio = () => {
if (state.audioBlob) {
elements.audioPlayer.play();
}
};
// 暂停音频
const pauseAudio = () => {
elements.audioPlayer.pause();
};
// 更新播放按钮状态
const updatePlayButtons = () => {
elements.playBtn.disabled = !state.audioBlob || state.isPlaying;
elements.pauseBtn.disabled = !state.audioBlob || !state.isPlaying;
};
// 更新音频进度
const updateAudioProgress = () => {
state.currentAudioTime = elements.audioPlayer.currentTime;
const progress = (state.currentAudioTime / state.audioDuration) * 100;
elements.waveform.style.setProperty('--progress', `${progress}%`);
updateTimeDisplay();
};
// 更新时间显示
const updateTimeDisplay = () => {
elements.currentTime.textContent = formatTime(state.currentAudioTime);
elements.totalTime.textContent = formatTime(state.audioDuration);
};
// 格式化时间
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 音频可视化
const visualizeAudio = () => {
if (!state.audioContext) {
setupAudioContext();
}
if (state.audioSource) {
state.audioSource.disconnect();
}
state.audioSource = state.audioContext.createMediaElementSource(elements.audioPlayer);
state.audioSource.connect(state.analyser);
state.analyser.connect(state.audioContext.destination);
state.analyser.fftSize = 64;
const bufferLength = state.analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const draw = () => {
state.animationFrameId = requestAnimationFrame(draw);
state.analyser.getByteFrequencyData(dataArray);
// 简化波形显示 - 在实际应用中可以实现更复杂的可视化
const average = dataArray.reduce((a, b) => a + b) / bufferLength;
const progress = (elements.audioPlayer.currentTime / elements.audioPlayer.duration) * 100 || 0;
elements.waveform.style.setProperty('--progress', `${progress}%`);
};
draw();
};
// 设置音频上下文
const setupAudioContext = () => {
state.audioContext = new (window.AudioContext || window.webkitAudioContext)();
state.analyser = state.audioContext.createAnalyser();
};
// 下载音频
const downloadAudio = (format) => {
if (!state.audioBlob) return;
let blob = state.audioBlob;
let extension = 'wav';
let mimeType = 'audio/wav';
if (format === 'mp3') {
// 在实际应用中,这里会将WAV转换为MP3
// 这里我们只是模拟转换
mimeType = 'audio/mpeg';
extension = 'mp3';
}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `ai-voice-${new Date().toISOString().slice(0, 10)}.${extension}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// 添加到历史记录
const addToHistory = (text) => {
const historyItem = {
id: Date.now(),
text: text.length > 50 ? text.substring(0, 50) + '...' : text,
fullText: text,
voice: state.currentVoice.name,
emotion: state.emotion,
date: new Date().toLocaleString(),
audioBlob: state.audioBlob
};
state.history.unshift(historyItem);
if (state.history.length > 10) {
state.history.pop();
}
saveHistory();
renderHistory();
};
// 渲染历史记录
const renderHistory = () => {
if (state.history.length === 0) {
elements.historyList.innerHTML = `
<div class="text-center text-gray-500 py-4">
<i class="fas fa-history text-2xl mb-2"></i>
<p>暂无历史记录</p>
</div>
`;
return;
}
elements.historyList.innerHTML = state.history.map(item => `
<div class="history-item p-3 border border-gray-200 rounded-lg hover:bg-gray-50 transition cursor-pointer fade-in" data-id="${item.id}">
<div class="flex justify-between items-start">
<div class="flex-1 min-w-0">
<h3 class="font-medium truncate">${item.text}</h3>
<div class="flex items-center text-xs text-gray-500 mt-1 space-x-2">
<span><i class="fas fa-${item.voice.includes('男') ? 'male' : 'female'} mr-1"></i> ${item.voice}</span>
<span><i class="fas fa-heart mr-1"></i> ${getEmotionName(item.emotion)}</span>
<span>${item.date}</span>
</div>
</div>
<button class="play-history ml-2 text-indigo-600 hover:text-indigo-800" data-id="${item.id}">
<i class="fas fa-play"></i>
</button>
</div>
</div>
`).join('');
// 绑定历史项目点击事件
document.querySelectorAll('.history-item').forEach(el => {
el.addEventListener('click', (e) => {
const id = parseInt(el.getAttribute('data-id'));
const item = state.history.find(i => i.id === id);
if (e.target.closest('.play-history')) {
e.stopPropagation();
playHistoryItem(id);
} else {
loadHistoryItem(id);
}
});
});
};
// 获取情感名称
const getEmotionName = (emotion) => {
const emotions = {
neutral: '中性',
happy: '快乐',
sad: '悲伤',
angry: '愤怒',
excited: '兴奋',
calm: '平静'
};
return emotions[emotion] || emotion;
};
// 播放历史项目
const playHistoryItem = (id) => {
const item = state.history.find(i => i.id === id);
if (item && item.audioBlob) {
state.audioBlob = item.audioBlob;
const audioUrl = URL.createObjectURL(item.audioBlob);
elements.audioPlayer.src = audioUrl;
elements.audioPlayer.play();
// 启用播放和下载按钮
elements.playBtn.disabled = false;
elements.downloadMp3.disabled = false;
elements.downloadWav.disabled = false;
}
};
// 加载历史项目
const loadHistoryItem = (id) => {
const item = state.history.find(i => i.id === id);
if (item) {
elements.inputText.value = item.fullText;
// 设置对应的语音和情感
const voice = state.voices.find(v => v.name === item.voice);
if (voice) {
selectVoice(voice.id);
}
state.emotion = item.emotion;
updateEmotionButtons();
// 滚动到顶部
window.scrollTo({ top: 0, behavior: 'smooth' });
}
};
// 清空历史记录
const clearHistory = () => {
if (confirm('确定要清空所有历史记录吗?此操作不可撤销。')) {
state.history = [];
saveHistory();
renderHistory();
}
};
// 保存历史记录
const saveHistory = () => {
// 只保存必要信息,Blob太大不保存
const historyToSave = state.history.map(item => ({
id: item.id,
text: item.text,
fullText: item.fullText,
voice: item.voice,
emotion: item.emotion,
date: item.date
}));
localStorage.setItem('ttsHistory', JSON.stringify(historyToSave));
};
// 加载API设置
const loadApiSettings = () => {
elements.ttsApi.value = state.apiSettings.ttsApi || '';
elements.ttsModel.value = state.apiSettings.ttsModel || 'vits';
elements.nlpApi.value = state.apiSettings.nlpApi || '';
elements.nlpModel.value = state.apiSettings.nlpModel || 'bert';
elements.apiKey.value = state.apiSettings.apiKey || '';
elements.apiRegion.value = state.apiSettings.apiRegion || 'global';
};
// 切换API设置显示
const toggleApiSettings = () => {
elements.apiSettings.classList.toggle('hidden');
const isHidden = elements.apiSettings.classList.contains('hidden');
elements.toggleApi.innerHTML = isHidden
? '<i class="fas fa-cog"></i> 高级设置'
: '<i class="fas fa-times"></i> 关闭设置';
};
// 保存API设置
const saveApiSettings = () => {
state.apiSettings = {
ttsApi: elements.ttsApi.value.trim(),
ttsModel: elements.ttsModel.value,
nlpApi: elements.nlpApi.value.trim(),
nlpModel: elements.nlpModel.value,
apiKey: elements.apiKey.value.trim(),
apiRegion: elements.apiRegion.value
};
localStorage.setItem('apiSettings', JSON.stringify(state.apiSettings));
showNotification('API设置已保存', 'success');
};
// 显示通知
const showNotification = (message, type) => {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-4 py-2 rounded-lg shadow-lg text-white ${
type === 'success' ? 'bg-green-500' : 'bg-red-500'
} z-50 fade-in`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.remove('fade-in');
notification.classList.add('fade-out');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
};
// 公共方法
return {
init
};
})();
// 初始化应用
document.addEventListener('DOMContentLoaded', App.init);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=wwqg/ai" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>