test / index.html
AntheaLaffey's picture
如果转写过程中有哪个地方失败了即使报错。模拟功能不能混入正常的语音转写中,需要另外设置一个按钮,试听功能也是 - Initial Deployment
f356955 verified
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时直播音频转写系统</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>
.waveform {
height: 100px;
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
position: relative;
overflow: hidden;
}
.waveform::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 50%, rgba(255,255,255,0.3) 100%);
animation: wave 2s linear infinite;
opacity: 0.8;
}
@keyframes wave {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.subtitle-display {
min-height: 120px;
transition: all 0.3s ease;
}
.subtitle-line {
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.language-flag {
width: 24px;
height: 16px;
display: inline-block;
margin-right: 8px;
background-size: cover;
border-radius: 2px;
}
.cn { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 24"><rect width="36" height="24" fill="%23de2910"/><path fill="%23ffde00" d="M9.6,4.8l1.2,3.6H15L12,9.6l1.2,3.6L9.6,9.6L6,13.2L7.2,9.6L4.2,8.4h4.2Z"/></svg>'); }
.jp { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 24"><rect width="36" height="24" fill="%23fff"/><circle cx="18" cy="12" r="6.4" fill="%23bc002d"/></svg>'); }
.en { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30"><clipPath id="a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="b"><path d="M30 15h30v15zv15H0zH0V0zV0h60z"/></clipPath><g clip-path="url(#a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>'); }
.api-selector.active {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
}
.recording-indicator {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/* 响应式调整 */
@media (max-width: 768px) {
.controls-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.api-selectors {
flex-direction: column;
}
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<header class="mb-8 text-center">
<h1 class="text-3xl md:text-4xl font-bold text-gray-800 mb-2">
<i class="fas fa-broadcast-tower text-blue-500 mr-2"></i>
实时直播音频转写系统
</h1>
<p class="text-gray-600">选择直播音频流,实时转写为中日英文字幕</p>
</header>
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
<!-- 音频波形显示 -->
<div class="waveform" id="waveform">
<div class="absolute inset-0 flex items-center justify-center" id="noAudioIndicator">
<div class="text-white bg-black bg-opacity-40 px-4 py-2 rounded-full">
<i class="fas fa-microphone-slash mr-2"></i>
未检测到音频输入
</div>
</div>
</div>
<!-- 控制面板 -->
<div class="p-6">
<div class="grid controls-grid md:grid-cols-3 gap-6 mb-6">
<!-- 音频源选择 -->
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">
<i class="fas fa-signal mr-2"></i>音频源选择
</label>
<div class="flex space-x-2">
<select id="audioSource" class="flex-1 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
<option value="">-- 选择音频源 --</option>
<option value="mic">麦克风输入</option>
<option value="system">系统音频</option>
<option value="custom">自定义流URL</option>
</select>
<button id="testAudioBtn" class="mt-1 px-3 py-2 bg-gray-200 hover:bg-gray-300 rounded-md text-gray-700" title="测试音频">
<i class="fas fa-volume-up"></i>
</button>
</div>
<div id="customUrlContainer" class="mt-2 hidden">
<input type="text" id="streamUrl" placeholder="输入音频流URL" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
</div>
</div>
<!-- 语言选择 -->
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">
<i class="fas fa-language mr-2"></i>转写语言
</label>
<select id="language" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
<option value="zh">中文</option>
<option value="ja">日本語</option>
<option value="en">English</option>
</select>
</div>
<!-- 操作按钮 -->
<div class="flex items-end space-x-3">
<button id="startBtn" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md flex items-center justify-center">
<i class="fas fa-play mr-2"></i> 开始转写
</button>
<button id="stopBtn" class="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-md flex items-center justify-center" disabled>
<i class="fas fa-stop mr-2"></i> 停止
</button>
</div>
</div>
<!-- API选择器 -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-cloud mr-2"></i>选择语音转写API
</label>
<div class="api-selectors flex flex-wrap gap-3">
<div class="api-selector flex-1 min-w-[200px] bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-300 active" data-api="azure">
<div class="flex items-center">
<img src="https://upload.wikimedia.org/wikipedia/commons/a/a8/Microsoft_Azure_Logo.svg" alt="Azure" class="h-8 mr-3">
<div>
<h3 class="font-medium">Azure Speech</h3>
<p class="text-xs text-gray-500">高精度,支持多语言</p>
</div>
</div>
</div>
<div class="api-selector flex-1 min-w-[200px] bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-300" data-api="google">
<div class="flex items-center">
<img src="https://upload.wikimedia.org/wikipedia/commons/2/2f/Google_2015_logo.svg" alt="Google" class="h-8 mr-3">
<div>
<h3 class="font-medium">Google Cloud</h3>
<p class="text-xs text-gray-500">快速响应,准确率高</p>
</div>
</div>
</div>
<div class="api-selector flex-1 min-w-[200px] bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-300" data-api="aws">
<div class="flex items-center">
<img src="https://upload.wikimedia.org/wikipedia/commons/9/93/Amazon_Web_Services_Logo.svg" alt="AWS" class="h-8 mr-3">
<div>
<h3 class="font-medium">AWS Transcribe</h3>
<p class="text-xs text-gray-500">实时流式处理</p>
</div>
</div>
</div>
<div class="api-selector flex-1 min-w-[200px] bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-blue-300" data-api="iflytek">
<div class="flex items-center">
<img src="https://www.iflytek.com/favicon.ico" alt="iFlytek" class="h-8 mr-3">
<div>
<h3 class="font-medium">讯飞语音</h3>
<p class="text-xs text-gray-500">中文识别准确率高</p>
</div>
</div>
</div>
</div>
</div>
<!-- API密钥输入 -->
<div class="mb-6" id="apiKeyContainer">
<label class="block text-sm font-medium text-gray-700 mb-1">
<i class="fas fa-key mr-2"></i>API密钥
</label>
<div class="flex space-x-2">
<input type="password" id="apiKey" placeholder="输入API密钥" class="flex-1 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md border">
<button id="saveKeyBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
<i class="fas fa-save mr-1"></i>保存
</button>
</div>
<div class="mt-2 text-xs text-gray-500 space-y-1">
<p><i class="fas fa-info-circle mr-1"></i>密钥仅保存在本地浏览器中</p>
<p><i class="fas fa-exclamation-triangle mr-1 text-yellow-500"></i>请确保使用正确的API服务密钥</p>
</div>
</div>
</div>
</div>
<!-- 字幕显示区域 -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-800">
<i class="fas fa-closed-captioning text-blue-500 mr-2"></i>
实时字幕
</h2>
<div class="flex items-center space-x-3">
<div class="flex items-center">
<span class="text-sm text-gray-500 mr-2">字幕大小:</span>
<select id="fontSize" class="text-sm border-gray-300 rounded">
<option value="sm"></option>
<option value="md" selected></option>
<option value="lg"></option>
<option value="xl">特大</option>
</select>
</div>
<button id="clearSubsBtn" class="text-sm text-gray-500 hover:text-gray-700">
<i class="fas fa-trash-alt mr-1"></i>清空
</button>
<button id="copySubsBtn" class="text-sm text-blue-500 hover:text-blue-700">
<i class="fas fa-copy mr-1"></i>复制
</button>
</div>
</div>
<div class="subtitle-display p-6" id="subtitleDisplay">
<div class="text-center text-gray-400 py-10" id="emptySubtitleMessage">
<i class="fas fa-comment-dots text-3xl mb-3"></i>
<p>字幕将显示在这里</p>
</div>
<div class="space-y-4 hidden" id="subtitleContent">
<!-- 字幕内容将在这里动态添加 -->
</div>
</div>
</div>
<!-- 转写历史 -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-800">
<i class="fas fa-history text-blue-500 mr-2"></i>
转写历史
</h2>
</div>
<div class="p-4">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">语言</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">API</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时长</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200" id="historyTableBody">
<tr>
<td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">暂无历史记录</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 状态提示弹窗 -->
<div id="statusToast" class="fixed bottom-4 right-4 bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg hidden flex items-center">
<i class="fas fa-info-circle mr-2"></i>
<span id="toastMessage"></span>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 元素引用
const audioSource = document.getElementById('audioSource');
const customUrlContainer = document.getElementById('customUrlContainer');
const streamUrl = document.getElementById('streamUrl');
const language = document.getElementById('language');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const apiSelectors = document.querySelectorAll('.api-selector');
const apiKeyContainer = document.getElementById('apiKeyContainer');
const apiKey = document.getElementById('apiKey');
const saveKeyBtn = document.getElementById('saveKeyBtn');
const subtitleDisplay = document.getElementById('subtitleDisplay');
const subtitleContent = document.getElementById('subtitleContent');
const emptySubtitleMessage = document.getElementById('emptySubtitleMessage');
const fontSize = document.getElementById('fontSize');
const clearSubsBtn = document.getElementById('clearSubsBtn');
const copySubsBtn = document.getElementById('copySubsBtn');
const historyTableBody = document.getElementById('historyTableBody');
const statusToast = document.getElementById('statusToast');
const toastMessage = document.getElementById('toastMessage');
const noAudioIndicator = document.getElementById('noAudioIndicator');
// 状态变量
let selectedApi = 'azure';
let isTranscribing = false;
let audioContext;
let analyser;
let microphone;
let mediaStream;
let subtitles = [];
let currentApiKey = '';
// 初始化
init();
// Test audio sample
const testAudio = new Audio('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3');
function init() {
// 从本地存储加载API密钥
// Test audio button
document.getElementById('testAudioBtn').addEventListener('click', function() {
if (testAudio.paused) {
testAudio.currentTime = 0;
testAudio.play();
this.innerHTML = '<i class="fas fa-volume-mute"></i>';
showToast('正在播放测试音频');
} else {
testAudio.pause();
this.innerHTML = '<i class="fas fa-volume-up"></i>';
showToast('测试音频已停止');
}
});
const savedKey = localStorage.getItem(`${selectedApi}_api_key`);
if (savedKey) {
apiKey.value = savedKey;
currentApiKey = savedKey;
}
// 从本地存储加载历史记录
loadHistory();
// 事件监听器
audioSource.addEventListener('change', function() {
if (this.value === 'custom') {
customUrlContainer.classList.remove('hidden');
} else {
customUrlContainer.classList.add('hidden');
}
});
// API选择器
apiSelectors.forEach(selector => {
selector.addEventListener('click', function() {
apiSelectors.forEach(s => s.classList.remove('active'));
this.classList.add('active');
selectedApi = this.dataset.api;
// 确保API密钥输入可见
apiKeyContainer.classList.remove('hidden');
// 加载保存的密钥
const savedKey = localStorage.getItem(`${selectedApi}_api_key`);
apiKey.value = savedKey || '';
currentApiKey = savedKey || '';
});
});
// 保存API密钥
saveKeyBtn.addEventListener('click', function() {
const key = apiKey.value.trim();
if (key) {
localStorage.setItem(`${selectedApi}_api_key`, key);
currentApiKey = key;
showToast('API密钥已保存');
} else {
showToast('请输入有效的API密钥', 'error');
}
});
// 开始转写
startBtn.addEventListener('click', startTranscription);
// 停止转写
stopBtn.addEventListener('click', stopTranscription);
// 字幕大小调整
fontSize.addEventListener('change', function() {
const size = this.value;
let sizeClass = '';
switch(size) {
case 'sm': sizeClass = 'text-sm'; break;
case 'md': sizeClass = 'text-base'; break;
case 'lg': sizeClass = 'text-lg'; break;
case 'xl': sizeClass = 'text-xl'; break;
}
subtitleContent.className = `space-y-4 ${sizeClass}`;
});
// 清空字幕
clearSubsBtn.addEventListener('click', function() {
subtitleContent.innerHTML = '';
subtitles = [];
emptySubtitleMessage.classList.remove('hidden');
subtitleContent.classList.add('hidden');
});
// 复制字幕
copySubsBtn.addEventListener('click', function() {
if (subtitles.length === 0) {
showToast('没有可复制的字幕内容', 'error');
return;
}
const textToCopy = subtitles.map(sub => sub.text).join('\n');
navigator.clipboard.writeText(textToCopy)
.then(() => showToast('字幕已复制到剪贴板'))
.catch(err => showToast('复制失败: ' + err, 'error'));
});
}
// 开始转写
async function startTranscription() {
if (isTranscribing) return;
// 验证API密钥
if (!currentApiKey) {
showToast('请先输入并保存API密钥', 'error');
return;
}
// 验证音频源
if (audioSource.value === '') {
showToast('请选择音频源', 'error');
return;
}
if (audioSource.value === 'custom' && !streamUrl.value.trim()) {
showToast('请输入有效的音频流URL', 'error');
return;
}
try {
// 初始化音频上下文
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
// 根据选择的音频源获取音频流
if (audioSource.value === 'mic') {
mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
microphone = audioContext.createMediaStreamSource(mediaStream);
microphone.connect(analyser);
noAudioIndicator.classList.add('hidden');
} else if (audioSource.value === 'system') {
// 注意: 系统音频捕获通常需要浏览器扩展或特定API
// 这里只是模拟
showToast('系统音频捕获需要特定权限或扩展', 'warning');
simulateAudio();
} else if (audioSource.value === 'custom') {
// 自定义音频流处理
processCustomStream();
}
// 开始可视化
visualizeAudio();
// 开始API调用
startAPICall();
// 更新UI状态
isTranscribing = true;
startBtn.disabled = true;
stopBtn.disabled = false;
startBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
startBtn.classList.add('bg-green-500', 'hover:bg-green-600');
startBtn.innerHTML = '<i class="fas fa-microphone recording-indicator mr-2"></i> 转写中...';
showToast('转写已开始');
// 隐藏空字幕消息
emptySubtitleMessage.classList.add('hidden');
subtitleContent.classList.remove('hidden');
} catch (error) {
console.error('Error starting transcription:', error);
showToast('启动转写失败: ' + error.message, 'error');
stopTranscription();
}
}
// 停止转写
function stopTranscription() {
if (!isTranscribing) return;
// 停止所有音频流
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
}
if (audioContext) {
audioContext.close();
}
// 关闭所有WebSocket连接
if (this.ws) {
this.ws.close();
this.ws = null;
}
// 清除可视化
cancelAnimationFrame(animationId);
// 更新UI状态
isTranscribing = false;
startBtn.disabled = false;
stopBtn.disabled = true;
startBtn.classList.remove('bg-green-500', 'hover:bg-green-600');
startBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
startBtn.innerHTML = '<i class="fas fa-play mr-2"></i> 开始转写';
showToast('转写已停止');
// 保存到历史记录
if (subtitles.length > 0) {
saveToHistory();
}
}
// 音频可视化
let animationId;
function visualizeAudio() {
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const waveform = document.getElementById('waveform');
function draw() {
animationId = requestAnimationFrame(draw);
analyser.getByteTimeDomainData(dataArray);
// 创建波形效果
let waveformHTML = '';
for (let i = 0; i < bufferLength; i++) {
const value = dataArray[i] / 128.0;
const height = value * 50;
waveformHTML += `<div class="absolute bottom-0 bg-white bg-opacity-70" style="left: ${i * (100 / bufferLength)}%; width: ${100 / bufferLength}%; height: ${height}%"></div>`;
}
waveform.innerHTML = waveformHTML;
}
draw();
}
// 模拟音频输入 (用于演示)
function simulateAudio() {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = 'sine';
oscillator.frequency.value = 440;
gainNode.gain.value = 0.1;
oscillator.connect(gainNode);
gainNode.connect(analyser);
oscillator.start();
// 随机添加字幕
const languages = {
zh: ["大家好,欢迎来到我的直播间", "今天我们要讨论人工智能", "语音识别技术非常有趣", "感谢大家的观看"],
ja: ["こんにちは、ライブストリームへようこそ", "今日はAIについて話します", "音声認識技術はとても面白いです", "ご視聴ありがとうございました"],
en: ["Hello everyone, welcome to my live stream", "Today we'll discuss AI", "Speech recognition is fascinating", "Thank you for watching"]
};
let count = 0;
const interval = setInterval(() => {
if (!isTranscribing) {
clearInterval(interval);
oscillator.stop();
return;
}
const lang = language.value;
const texts = languages[lang];
const text = texts[count % texts.length];
addSubtitle(text, lang);
count++;
}, 5000);
}
// 处理自定义音频流 (模拟)
function processCustomStream() {
// 实际应用中这里应该处理真正的音频流
// 这里只是模拟
simulateAudio();
}
// API调用
function startAPICall() {
if (selectedApi === 'iflytek') {
connectIflytekWebSocket();
} else {
// 其他API的模拟调用
simulateAPICall();
}
}
// 连接讯飞WebSocket
function connectIflytekWebSocket() {
if (!currentApiKey) {
showToast('请先输入并保存讯飞API密钥', 'error');
return;
}
// 生成请求参数
const appId = currentApiKey.split('.')[0]; // 假设API key格式是 appid.key
const ts = Math.floor(Date.now() / 1000);
const signa = generateIflytekSignature(appId, ts);
const wsUrl = `wss://rtasr.xfyun.cn/v1/ws?appid=${appId}&ts=${ts}&signa=${encodeURIComponent(signa)}`;
const ws = new WebSocket(wsUrl);
ws.onopen = function() {
console.log('讯飞WebSocket连接已建立');
// 开始发送音频数据
startSendingAudio(ws);
};
ws.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.action === 'result') {
// 处理识别结果
const text = data.data.result;
if (text) {
addSubtitle(text, language.value);
}
}
};
ws.onerror = function(e) {
console.error('讯飞WebSocket错误:', e);
showToast('讯飞连接错误', 'error');
stopTranscription();
};
ws.onclose = function() {
console.log('讯飞WebSocket连接已关闭');
};
return ws;
}
// 生成讯飞签名
function generateIflytekSignature(appId, ts) {
// 这里需要实现讯飞的签名算法
// 实际应用中应该使用更安全的服务器端生成
const key = currentApiKey.split('.')[1] || '';
const baseString = `${appId}${ts}`;
// 简单示例,实际应该使用HMAC-SHA1
return btoa(baseString + key).slice(0, 20);
}
// 开始发送音频数据
function startSendingAudio(ws) {
const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
analyser.connect(scriptProcessor);
scriptProcessor.onaudioprocess = function(e) {
if (!isTranscribing) return;
const inputData = e.inputBuffer.getChannelData(0);
// 将音频数据发送到WebSocket
ws.send(inputData);
};
scriptProcessor.connect(audioContext.destination);
}
// 添加字幕
function addSubtitle(text, lang) {
if (!text) return;
const timestamp = new Date().toLocaleTimeString();
const langClass = {
zh: 'cn',
ja: 'jp',
en: 'en'
}[lang] || 'en';
const subtitle = {
text,
lang,
timestamp
};
subtitles.push(subtitle);
// 创建字幕元素
const subtitleElement = document.createElement('div');
subtitleElement.className = 'subtitle-line bg-gray-50 p-3 rounded-lg';
subtitleElement.innerHTML = `
<div class="flex items-center mb-1">
<span class="language-flag ${langClass}"></span>
<span class="text-xs text-gray-500">${timestamp}</span>
</div>
<p>${text}</p>
`;
subtitleContent.appendChild(subtitleElement);
// 自动滚动到底部
subtitleDisplay.scrollTop = subtitleDisplay.scrollHeight;
}
// 保存到历史记录
function saveToHistory() {
const history = JSON.parse(localStorage.getItem('transcription_history') || '[]');
const newEntry = {
id: Date.now(),
date: new Date().toLocaleString(),
language: language.options[language.selectedIndex].text,
api: selectedApi,
duration: Math.floor(subtitles.length * 5) + '秒', // 模拟时长
subtitles: [...subtitles]
};
history.unshift(newEntry);
localStorage.setItem('transcription_history', JSON.stringify(history));
// 重新加载历史记录
loadHistory();
}
// 加载历史记录
function loadHistory() {
const history = JSON.parse(localStorage.getItem('transcription_history') || '[]');
if (history.length === 0) {
historyTableBody.innerHTML = `
<tr>
<td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">暂无历史记录</td>
</tr>
`;
return;
}
let html = '';
history.forEach(entry => {
html += `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${entry.date}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span class="language-flag ${entry.language === '中文' ? 'cn' : entry.language === '日本語' ? 'jp' : 'en'}"></span>
${entry.language}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${entry.api}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${entry.duration}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<button class="text-blue-500 hover:text-blue-700 mr-3 view-history" data-id="${entry.id}">
<i class="fas fa-eye mr-1"></i>查看
</button>
<button class="text-red-500 hover:text-red-700 delete-history" data-id="${entry.id}">
<i class="fas fa-trash-alt mr-1"></i>删除
</button>
</td>
</tr>
`;
});
historyTableBody.innerHTML = html;
// 添加历史记录按钮事件
document.querySelectorAll('.view-history').forEach(btn => {
btn.addEventListener('click', function() {
const id = parseInt(this.dataset.id);
viewHistory(id);
});
});
document.querySelectorAll('.delete-history').forEach(btn => {
btn.addEventListener('click', function() {
const id = parseInt(this.dataset.id);
deleteHistory(id);
});
});
}
// 查看历史记录
function viewHistory(id) {
const history = JSON.parse(localStorage.getItem('transcription_history') || '[]');
const entry = history.find(e => e.id === id);
if (!entry) {
showToast('未找到历史记录', 'error');
return;
}
// 清空当前字幕
subtitleContent.innerHTML = '';
subtitles = [];
// 添加历史字幕
entry.subtitles.forEach(sub => {
addSubtitle(sub.text, sub.lang);
});
// 更新语言选择
language.value = entry.language === '中文' ? 'zh' : entry.language === '日本語' ? 'ja' : 'en';
showToast('已加载历史记录');
// 隐藏空字幕消息
emptySubtitleMessage.classList.add('hidden');
subtitleContent.classList.remove('hidden');
}
// 删除历史记录
function deleteHistory(id) {
if (!confirm('确定要删除这条历史记录吗?')) return;
let history = JSON.parse(localStorage.getItem('transcription_history') || '[]');
history = history.filter(e => e.id !== id);
localStorage.setItem('transcription_history', JSON.stringify(history));
loadHistory();
showToast('历史记录已删除');
}
// 显示状态提示
function showToast(message, type = 'info') {
toastMessage.textContent = message;
// 设置颜色
statusToast.className = 'fixed bottom-4 right-4 text-white px-4 py-2 rounded-lg shadow-lg hidden flex items-center';
switch(type) {
case 'error':
statusToast.classList.add('bg-red-500');
break;
case 'warning':
statusToast.classList.add('bg-yellow-500');
break;
case 'success':
statusToast.classList.add('bg-green-500');
break;
default:
statusToast.classList.add('bg-gray-800');
}
statusToast.classList.remove('hidden');
// 3秒后自动隐藏
setTimeout(() => {
statusToast.classList.add('hidden');
}, 3000);
}
});
</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=AntheaLaffey/test" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>