Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<link rel="icon" href="../static/logo_whisper.png" type="image/x-icon"> | |
<title>Funsound语音识别</title> | |
<style> | |
body { | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
height: 100vh; | |
margin: 0; | |
background-color: #1c1c1c; | |
color: #eaeaea; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
.container { | |
display: flex; | |
flex-direction: column; | |
width: 80%; | |
max-width: 1800px; /* 添加最大宽度以避免过宽 */ | |
min-width: 300px; /* 确保最小宽度以保持可读性 */ | |
height: auto; /* 高度自适应内容 */ | |
border: 1px solid #444; | |
border-radius: 8px; | |
padding: 20px; | |
box-sizing: border-box; | |
background-color: #282828; | |
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3); | |
overflow: hidden; | |
} | |
.title { | |
text-align: center; | |
font-size: 2.5rem; | |
margin-bottom: 20px; | |
color: #ffffff; | |
border-bottom: 2px solid #444; | |
padding-bottom: 15px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.title img { | |
width: 40px; | |
height: 40px; | |
margin-right: 15px; | |
} | |
.content { | |
display: flex; | |
flex-grow: 1; | |
overflow: hidden; | |
} | |
.video-container, .asr-container { | |
margin: 10px; | |
border: 1px solid #444; | |
border-radius: 8px; | |
padding: 15px; | |
box-sizing: border-box; | |
background-color: #333; | |
overflow: hidden; | |
} | |
.video-container { | |
flex: 0 0 30%; | |
} | |
.asr-container { | |
flex: 0 0 70%; | |
overflow-y: auto; | |
position: relative; | |
display: flex; | |
flex-direction: column; | |
} | |
video { | |
width: 100%; | |
height: 30%; /* 自适应视频高度 */ | |
background-color: #000; | |
border-radius: 8px; | |
margin-bottom: 0px; | |
} | |
label { | |
margin-bottom: 5px; | |
font-size: 1rem; | |
color: #aaa; | |
display: block; | |
} | |
.asr-list { | |
flex-grow: 1; | |
overflow-y: auto; | |
background-color: #1c1c1c; | |
padding: 10px; | |
border-radius: 8px; | |
border: 1px solid #444; | |
color: #eaeaea; | |
margin-bottom: 10px; | |
} | |
.asr-item { | |
display: flex; | |
align-items: center; | |
padding: 10px; | |
border-bottom: 1px solid #444; | |
box-sizing: border-box; | |
color: #eaeaea; | |
justify-content: space-between; | |
} | |
.asr-item label, | |
.asr-item input, | |
.asr-item select, | |
.asr-item button { | |
margin: 0 5px; | |
} | |
.asr-item .timestamp { | |
display: flex; | |
align-items: center; | |
flex: 0 0 200px; | |
text-align: center; | |
} | |
.asr-item .timestamp input { | |
width: 60px; | |
text-align: center; | |
background-color: #444; | |
color: #eaeaea; | |
border: 1px solid #555; | |
border-radius: 4px; | |
} | |
.asr-item input[type="text"], | |
.asr-item select { | |
padding: 5px; | |
border: 1px solid #555; | |
background-color: #444; | |
color: #eaeaea; | |
width: 100%; | |
border-radius: 4px; | |
flex: 1; | |
} | |
.asr-item input.role-field { | |
width: 70px; | |
padding: 5px; | |
border: 1px solid #555; | |
background-color: #444; | |
color: #eaeaea; | |
border-radius: 4px; | |
flex: 0 0 70px; /* 保持宽度固定为 100px */ | |
text-align: center; | |
} | |
.asr-item input[type="checkbox"] { | |
margin-right: 5px; | |
transform: scale(0.8); | |
} | |
.play-button { | |
margin: 0 5px; | |
padding: 3px 8px; | |
background-color: #ff8c00; /* 使用橙色 */ | |
color: #000; | |
border: none; | |
cursor: pointer; | |
font-size: 0.8rem; | |
border-radius: 4px; | |
transition: background-color 0.3s; | |
white-space: nowrap; | |
} | |
.play-button:hover { | |
background-color: #e67e00; | |
} | |
.upload-button { | |
padding: 10px; | |
background-color: #007bff; /* 使用蓝色 */ | |
color: #fff; | |
border: none; | |
cursor: pointer; | |
font-size: 1rem; | |
margin-right: 10px; | |
border-radius: 4px; | |
transition: background-color 0.3s; | |
} | |
.upload-button:hover { | |
background-color: #0056b3; | |
} | |
.file-input-wrapper { | |
position: relative; | |
overflow: hidden; | |
display: inline-block; | |
} | |
.file-input { | |
font-size: 1rem; | |
font-weight: bold; | |
color: white; | |
background-color: #17a2b8; /* 使用青色 */ | |
border: none; | |
padding: 10px 20px; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: background-color 0.3s; | |
} | |
.file-input:hover { | |
background-color: #138496; | |
} | |
.file-input-wrapper input[type="file"] { | |
font-size: 100px; | |
position: absolute; | |
left: 0; | |
top: 0; | |
opacity: 0; | |
cursor: pointer; | |
} | |
.export-button { | |
padding: 10px; | |
background-color: #28a745; /* 使用绿色 */ | |
color: #fff; | |
border: none; | |
cursor: pointer; | |
font-size: 1rem; | |
border-radius: 4px; | |
transition: background-color 0.3s; | |
} | |
.export-button:hover { | |
background-color: #218838; | |
} | |
.buttons-container { | |
display: flex; | |
justify-content: center; | |
margin-bottom: 10px; | |
} | |
/* 新增合成按钮样式,使用橙色 */ | |
.subtitle-button { | |
padding: 10px; | |
background-color: #ff8c00; /* 使用橙色 */ | |
color: #000; | |
border: none; | |
cursor: pointer; | |
font-size: 1rem; | |
border-radius: 4px; | |
transition: background-color 0.3s; | |
} | |
.subtitle-button:hover { | |
background-color: #e67e00; | |
} | |
.progress-bar { | |
width: 100%; | |
background-color: #444; | |
margin-top: 0px; | |
border-radius: 4px; | |
} | |
.progress-bar div { | |
width: 0%; | |
background-color: #00ff84; /* 使用绿色 */ | |
color: #000; | |
text-align: center; | |
padding: 2px 0; | |
border-radius: 4px; | |
transition: width 0.3s ease; | |
} | |
.center-buttons { | |
display: flex; | |
justify-content: center; | |
gap: 10px; | |
margin-top: 20px; | |
position: sticky; | |
bottom: 0; | |
background-color: #333; | |
padding: 10px 0; | |
border-top: 1px solid #444; | |
} | |
footer { | |
text-align: center; | |
padding: 10px; | |
background-color: #1c1c1c; | |
color: #888; | |
font-size: 0.9rem; | |
margin-top: 20px; | |
border-top: 1px solid #444; | |
width: 100%; | |
} | |
footer a { | |
color: #00ff84; | |
text-decoration: none; | |
transition: color 0.3s; | |
} | |
footer a:hover { | |
color: #00d473; | |
} | |
@media (max-width: 768px) { | |
.container { | |
width: 95%; | |
padding: 10px; /* 减小内边距以增加可用空间 */ | |
} | |
.content { | |
flex-direction: column; | |
} | |
.video-container, .asr-container { | |
flex: 0 0 100%; | |
} | |
.title { | |
font-size: 1.5rem; /* 缩小标题字体以适应小屏幕 */ | |
} | |
.asr-item input[type="text"], | |
.asr-item select, | |
.asr-item input.role-field { | |
width: auto; /* 使输入框在小屏幕上更灵活 */ | |
} | |
} | |
.server-url-input { | |
width: 70%; | |
padding: 10px; | |
font-size: 1rem; | |
color: #eaeaea; | |
background-color: #333; | |
border: 1px solid #555; | |
border-radius: 4px; | |
outline: none; | |
transition: all 0.3s ease; | |
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); | |
} | |
.server-url-input:focus { | |
border-color: #00ff84; /* 聚焦时边框颜色 */ | |
box-shadow: 0 0 8px rgba(0, 255, 132, 0.8); /* 添加绿色光晕效果 */ | |
} | |
.options-container { | |
display: flex; | |
justify-content: space-around; | |
align-items: center; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="title"> | |
<img src="../static/logo_whisper.png" alt="Funsound Logo"> | |
Funsound 语音识别 | |
</div> | |
<div class="content"> | |
<div class="video-container"> | |
<div class="file-input-wrapper"> | |
<button class="file-input">选择文件</button> | |
<input type="file" id="videoInput" accept=".wav, .mp3, .m4a, .mp4, .aac"> | |
</div> | |
<video id="videoPlayer" controls> | |
您的浏览器不支持 video 标签。 | |
</video> | |
<!-- Added serverUrl input field --> | |
<!-- <div style="margin: 10px 0;"> | |
<label for="serverUrlInput">服务器地址:</label> | |
<input type="text" id="serverUrlInput" value="https://www.funsound.cn/" class="server-url-input"> | |
</div> --> | |
<div class="options-container"> | |
<!-- Recognition Model --> | |
<div style="margin: 10px;"> | |
<label>识别模型(单选):</label> | |
<label><input type="radio" name="pipeline" value="whisper" checked> Whisper</label> | |
<label><input type="radio" name="pipeline" value="funasr"> FunASR</label> | |
</div> | |
<!-- Additional Tasks --> | |
<div style="margin: 10px;"> | |
<label>附加任务(多选):</label> | |
<label><input type="checkbox" id="speakerRecognition"> 说话人识别</label> | |
<label><input type="checkbox" id="englishTranslation"> 中英翻译</label> | |
</div> | |
</div> | |
<div class="buttons-container"> | |
<button id="uploadBtn" class="upload-button" onclick="uploadFile()">上传并识别</button> | |
</div> | |
<label>上传进度:</label> | |
<div id="uploadProgress" class="progress-bar"> | |
<div>0%</div> | |
</div> | |
<label>识别进度:</label> | |
<div id="recognitionProgress" class="progress-bar"> | |
<div>0%</div> | |
</div> | |
<label>合成进度:</label> | |
<div id="subtitleProgress" class="progress-bar"> | |
<div>0%</div> | |
</div> | |
<div id="logContent" style="margin-top: 10px; color: #fff;"></div> | |
</div> | |
<div class="asr-container"> | |
<label>识别结果:</label> | |
<div id="asrList" class="asr-list"></div> | |
<div class="center-buttons"> | |
<button id="exportJsonBtn" class="export-button" onclick="exportAsrData('json')">导出 JSON</button> | |
<button id="exportSrtBtn" class="export-button" onclick="exportAsrData('srt')">导出 SRT</button> | |
<!-- 修改合成按钮颜色 --> | |
<button id="generateSubtitleBtn" class="subtitle-button" onclick="generateSubtitle()">合成字幕</button> | |
<button id="downloadVideoBtn" style="display: none;" onclick="downloadVideo()">下载字幕视频</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<footer> | |
联系邮箱: <a href="mailto:[email protected]">[email protected]</a> | | |
CSDN: <a href="https://blog.csdn.net/Ephemeroptera" target="_blank">Pika在线</a> | | |
Modelscope: <a href="https://modelscope.cn/studios/QuadraV/FunSound">Funsound</a> | | |
私有化部署 | |
</footer> | |
<script> | |
let currentTaskId = null; // 当前任务ID | |
let asrData = []; // 存储识别结果数据 | |
let serverUrl = "https://www.funsound.cn"; // 服务器地址 | |
const chunkSize = 1 * 1024 * 1024; // 分块大小,5MB | |
// 监听文件输入更改,预览视频 | |
document.getElementById('videoInput').addEventListener('change', function (event) { | |
const file = event.target.files[0]; | |
log('Selected file: ' + file.name); | |
document.getElementById('uploadBtn').disabled = file.size > 300 * 1024 * 1024; | |
const videoPlayer = document.getElementById('videoPlayer'); | |
videoPlayer.src = URL.createObjectURL(file); | |
log('Video URL: ' + videoPlayer.src); | |
}); | |
// 上传文件并提交任务 | |
function uploadFile() { | |
const fileInput = document.getElementById('videoInput'); | |
const file = fileInput.files[0]; | |
if (!file) { | |
alert('请先选择一个文件'); | |
return; | |
} | |
const speakerRecognition = document.getElementById('speakerRecognition').checked; | |
const englishTranslation = document.getElementById('englishTranslation').checked; | |
const selectedPipeline = document.querySelector('input[name="pipeline"]:checked').value; | |
log('Speaker Recognition: ' + speakerRecognition + ', English Translation: ' + englishTranslation); | |
log('Selected pipeline: ' + selectedPipeline); | |
document.getElementById('uploadBtn').disabled = true; | |
document.getElementById('uploadBtn').innerText = '正在转写...'; | |
resetProgress(); | |
// serverUrl = document.getElementById('serverUrlInput').value.trim(); | |
log('Server URL: ' + serverUrl); | |
initializeTask(file, speakerRecognition, englishTranslation, selectedPipeline); | |
} | |
// 初始化任务 | |
function initializeTask(file, speakerRecognition, englishTranslation, pipeline) { | |
log("Initializing task with filename: " + file.name); | |
const formData = new FormData(); | |
formData.append('status', 'init'); | |
formData.append('filename', file.name); | |
formData.append('totalChunks', Math.ceil(file.size / chunkSize)); | |
formData.append('speakerRecognition', speakerRecognition); | |
formData.append('englishTranslation', englishTranslation); | |
formData.append('pipeline', pipeline); | |
const xhr = new XMLHttpRequest(); | |
xhr.open('POST', `${serverUrl}/submit`, true); | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
log('Initialization response: ' + JSON.stringify(response)); | |
if (response.code === 0) { | |
currentTaskId = response.content; | |
log('Task ID: ' + currentTaskId); | |
uploadChunks(file); // 开始分块上传 | |
} else { | |
alert('初始化失败,请重试'); | |
resetUploadButton(); | |
} | |
} else { | |
alert('初始化失败,请重试'); | |
resetUploadButton(); | |
} | |
}; | |
xhr.onerror = handleUploadError; | |
xhr.send(formData); | |
} | |
// 上传文件块 | |
function uploadChunks(file) { | |
log("正在上传文件.."); | |
const totalChunks = Math.ceil(file.size / chunkSize); | |
let chunkIndex = 0; | |
function uploadNextChunk() { | |
if (chunkIndex >= totalChunks) { | |
log("上传完毕,开始转写.."); | |
submitASRTask(); | |
return; | |
} | |
const start = chunkIndex * chunkSize; | |
const end = Math.min(file.size, start + chunkSize); | |
const chunk = file.slice(start, end); | |
const formData = new FormData(); | |
formData.append('status', 'upload'); | |
formData.append('task_id', currentTaskId); | |
formData.append('ChunkId', chunkIndex); | |
formData.append('file', chunk); | |
const xhr = new XMLHttpRequest(); | |
xhr.open('POST', `${serverUrl}/submit`, true); | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
log(`Chunk ${chunkIndex} upload response: ` + JSON.stringify(response)); | |
if (response.code === 0) { | |
updateUploadProgress(chunkIndex, totalChunks); | |
chunkIndex++; | |
uploadNextChunk(); // 上传下一个块 | |
} else { | |
alert(`第 ${chunkIndex + 1} 块上传失败,请重试`); | |
resetUploadButton(); | |
} | |
} else { | |
alert(`第 ${chunkIndex + 1} 块上传失败,请重试`); | |
resetUploadButton(); | |
} | |
}; | |
xhr.onerror = handleUploadError; | |
xhr.send(formData); | |
} | |
uploadNextChunk(); // 开始上传第一个块 | |
} | |
// 更新上传进度 | |
function updateUploadProgress(chunkIndex, totalChunks) { | |
const totalProgress = ((chunkIndex + 1) / totalChunks) * 100; | |
log('Upload progress: ' + totalProgress.toFixed(2) + '%'); | |
document.getElementById('uploadProgress').firstElementChild.style.width = `${totalProgress}%`; | |
document.getElementById('uploadProgress').firstElementChild.innerText = `${totalProgress.toFixed(2)}%`; | |
} | |
// 提交 ASR 任务 | |
function submitASRTask() { | |
const formData = new FormData(); | |
formData.append('status', 'asr'); | |
formData.append('task_id', currentTaskId); | |
const xhr = new XMLHttpRequest(); | |
xhr.open('POST', `${serverUrl}/submit`, true); | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
log('ASR submission response: ' + JSON.stringify(response)); | |
if (response.code === 0) { | |
monitorTaskProgress(currentTaskId); | |
} else { | |
alert('ASR 提交失败,请重试'); | |
resetUploadButton(); | |
} | |
} else { | |
alert('ASR 提交失败,请重试'); | |
resetUploadButton(); | |
} | |
}; | |
xhr.onerror = handleUploadError; | |
xhr.send(formData); | |
} | |
// 监控任务进度 | |
function monitorTaskProgress(taskId) { | |
log('Monitoring task progress for Task ID: ' + taskId); | |
let failedRequests = 0; | |
const maxFailedRequests = 10; | |
const intervalId = setInterval(function () { | |
const xhr = new XMLHttpRequest(); | |
xhr.open('GET', `${serverUrl}/task_asr_prgs/${taskId}`, true); | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
log('Progress response: ' + JSON.stringify(response)); | |
const status = response.content.status; | |
const progress = response.content.prgs; | |
failedRequests = 0; | |
if (progress) { | |
updateRecognitionProgress((progress.cur / progress.total) * 100, progress.msg); | |
} | |
if (status === "SUCCESS") { | |
clearInterval(intervalId); | |
asrData = response.content.result; | |
log('Recognition successful, ASR data: ' + JSON.stringify(asrData)); | |
displayResults(asrData); | |
resetUploadButton(); | |
} else if (status === "FAIL") { | |
clearInterval(intervalId); | |
alert('识别任务失败'); | |
resetUploadButton(); | |
} | |
} else { | |
handleProgressError(); | |
} | |
}; | |
xhr.onerror = handleProgressError; | |
xhr.send(); | |
}, 2000); | |
} | |
// 处理进度请求错误 | |
function handleProgressError() { | |
failedRequests++; | |
log('请求失败,当前重连次数: ' + failedRequests); | |
if (failedRequests >= maxFailedRequests) { | |
clearInterval(intervalId); | |
alert('连续请求失败,任务未完成'); | |
resetUploadButton(); | |
} | |
} | |
// 更新识别进度 | |
function updateRecognitionProgress(progress, msg) { | |
log('Recognition progress: ' + progress + '%, Message: ' + msg); | |
document.getElementById('recognitionProgress').firstElementChild.style.width = `${progress}%`; | |
document.getElementById('recognitionProgress').firstElementChild.innerText = `${progress.toFixed(2)}%`; | |
document.getElementById('logContent').innerText = `进度: ${progress.toFixed(2)}%, 状态: ${msg}`; | |
} | |
// 显示识别结果 | |
function displayResults(results) { | |
log('Displaying ASR results'); | |
const asrList = document.getElementById('asrList'); | |
asrList.innerHTML = ""; | |
results.forEach((entry) => { | |
const div = document.createElement('div'); | |
div.className = 'asr-item'; | |
div.innerHTML = ` | |
<div class="timestamp"> | |
<button class="play-button">播放</button> | |
<input type="number" value="${entry.start.toFixed(1)}" step="0.1" min="0" class="start-time"> | |
- | |
<input type="number" value="${entry.end.toFixed(1)}" step="0.1" min="0" class="end-time"> | |
</div> | |
<input type="text" value="${entry.role}" placeholder="角色" class="role-field"> | |
<input type="text" value="${entry.text}" placeholder="文本" class="text-field"> | |
<input type="text" value="${entry.trans}" placeholder="翻译" class="trans-field"> | |
<label> | |
<input type="checkbox" ${entry.drop ? 'checked' : ''}> 丢弃 | |
</label> | |
`; | |
setupASREventHandlers(div, entry); | |
asrList.appendChild(div); | |
}); | |
} | |
// 导出识别结果为 JSON 或 SRT 格式 | |
function exportAsrData(format) { | |
if (asrData.length === 0) { | |
alert('没有数据可以导出'); | |
return; | |
} | |
// 过滤掉被标记为丢弃的条目 | |
const filteredData = asrData.filter(entry => !entry.drop); | |
let content = ''; | |
if (format === 'json') { | |
content = JSON.stringify(filteredData, null, 2); | |
} else if (format === 'srt') { | |
filteredData.forEach((entry, index) => { | |
content += `${index + 1}\n`; | |
const start = formatTime(entry.start); | |
const end = formatTime(entry.end); | |
content += `${start} --> ${end}\n`; | |
content += `${entry.text}\n${entry.trans}\n\n`; | |
}); | |
} | |
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); | |
const url = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = `result.${format}`; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} | |
// 设置 ASR 事件处理 | |
function setupASREventHandlers(div, entry) { | |
const startInput = div.querySelector('.start-time'); | |
const endInput = div.querySelector('.end-time'); | |
const roleInput = div.querySelector('input.role-field'); | |
const textInput = div.querySelector('input.text-field'); | |
const transInput = div.querySelector('input.trans-field'); | |
const dropCheckbox = div.querySelector('input[type="checkbox"]'); | |
const playButton = div.querySelector('.play-button'); | |
startInput.addEventListener('input', () => entry.start = parseFloat(startInput.value)); | |
endInput.addEventListener('input', () => entry.end = parseFloat(endInput.value)); | |
roleInput.addEventListener('input', () => entry.role = roleInput.value); | |
textInput.addEventListener('input', () => entry.text = textInput.value); | |
transInput.addEventListener('input', () => entry.trans = transInput.value); | |
dropCheckbox.addEventListener('change', () => entry.drop = dropCheckbox.checked); | |
playButton.addEventListener('click', () => { | |
const video = document.getElementById('videoPlayer'); | |
video.currentTime = entry.start; | |
video.play(); | |
const interval = setInterval(() => { | |
if (video.currentTime >= entry.end) { | |
video.pause(); | |
clearInterval(interval); | |
} | |
}, 100); | |
}); | |
} | |
// 生成字幕 | |
function generateSubtitle() { | |
if (!currentTaskId || asrData.length === 0) { | |
alert('请确保已选择视频文件并进行了识别'); | |
return; | |
} | |
const formData = new FormData(); | |
formData.append('status', "subtitle"); | |
formData.append('task_id', currentTaskId); | |
formData.append('asr_results', JSON.stringify(asrData)); // 将 ASR 数据转为字符串 | |
document.getElementById('generateSubtitleBtn').disabled = true; | |
document.getElementById('generateSubtitleBtn').innerText = '生成中...'; | |
const xhr = new XMLHttpRequest(); | |
xhr.open('POST', `${serverUrl}/submit`, true); | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
log('Subtitle generation response: ' + JSON.stringify(response)); | |
if (response.code === 0) { | |
monitorSubtitleGeneration(response.content); | |
} else { | |
alert('字幕生成失败,请重试'); | |
resetGenerateButton(); | |
} | |
} else { | |
alert('字幕生成失败,请重试'); | |
resetGenerateButton(); | |
} | |
}; | |
xhr.onerror = function () { | |
alert('字幕生成失败,请重试'); | |
resetGenerateButton(); | |
}; | |
xhr.send(formData); | |
} | |
// 监控字幕生成进度 | |
function monitorSubtitleGeneration(taskId) { | |
log('Monitoring subtitle generation for Task ID: ' + taskId); | |
let failedRequests = 0; | |
const maxFailedRequests = 10; | |
const intervalId = setInterval(function () { | |
const xhr = new XMLHttpRequest(); | |
xhr.open('GET', `${serverUrl}/task_subtitle_prgs/${taskId}`, true); | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
const progress = response.content.progress; | |
failedRequests = 0; | |
if (progress !== undefined) { | |
updateSubtitleProgress(progress * 100, '正在生成字幕'); | |
} | |
if (progress >= 1.0) { | |
clearInterval(intervalId); | |
document.getElementById('videoPlayer').src = `${serverUrl}/video/${taskId}`; | |
document.getElementById('downloadVideoBtn').style.display = 'inline'; // Show the download button | |
resetGenerateButton(); | |
log("生成完毕,点击上方播放器进行预览"); | |
} | |
} else { | |
handleProgressError(); | |
} | |
}; | |
xhr.onerror = function () { | |
handleProgressError(); | |
}; | |
xhr.send(); | |
}, 2000); | |
} | |
// 更新字幕生成进度 | |
function updateSubtitleProgress(progress, msg) { | |
log('Subtitle progress: ' + progress + '%, Message: ' + msg); | |
document.getElementById('subtitleProgress').firstElementChild.style.width = `${progress}%`; | |
document.getElementById('subtitleProgress').firstElementChild.innerText = `${progress.toFixed(2)}%`; | |
document.getElementById('logContent').innerText = `进度: ${progress.toFixed(2)}%, 状态: ${msg}`; | |
} | |
// 重置上传按钮状态 | |
function resetUploadButton() { | |
log('Resetting upload button state'); | |
document.getElementById('uploadBtn').disabled = false; | |
document.getElementById('uploadBtn').innerText = '上传并识别'; | |
} | |
// 重置字幕生成按钮状态 | |
function resetGenerateButton() { | |
document.getElementById('generateSubtitleBtn').disabled = false; | |
document.getElementById('generateSubtitleBtn').innerText = '生成字幕'; | |
} | |
// 重置进度条和界面 | |
function resetProgress() { | |
log('Resetting progress bars and UI elements'); | |
document.getElementById('uploadProgress').firstElementChild.style.width = '0%'; | |
document.getElementById('uploadProgress').firstElementChild.innerText = ''; | |
document.getElementById('recognitionProgress').firstElementChild.style.width = '0%'; | |
document.getElementById('recognitionProgress').firstElementChild.innerText = ''; | |
document.getElementById('subtitleProgress').firstElementChild.style.width = '0%'; | |
document.getElementById('subtitleProgress').firstElementChild.innerText = ''; | |
document.getElementById('asrList').innerHTML = ""; | |
document.getElementById('logContent').innerText = ""; | |
document.getElementById('downloadVideoBtn').style.display = 'none'; | |
} | |
// 下载视频 | |
function downloadVideo() { | |
if (!currentTaskId) { | |
alert('请确保已生成字幕视频'); | |
return; | |
} | |
const xhr = new XMLHttpRequest(); | |
xhr.open('GET', `${serverUrl}/url/${currentTaskId}`, true); | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
if (response.code === 0) { | |
const videoUrl = response.content.url; | |
const a = document.createElement('a'); | |
a.href = `${serverUrl}/${videoUrl}`; | |
a.download = `subtitle_video_${currentTaskId}.mp4`; // Specify the filename | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
} else { | |
alert('下载链接生成失败,请重试'); | |
} | |
} else { | |
alert('下载失败,请重试'); | |
} | |
}; | |
xhr.onerror = function () { | |
alert('下载请求出错,请重试'); | |
}; | |
xhr.send(); | |
} | |
// 格式化时间,用于字幕格式化 | |
function formatTime(seconds) { | |
const hours = Math.floor(seconds / 3600); | |
const minutes = Math.floor((seconds % 3600) / 60); | |
const secs = Math.floor(seconds % 60); | |
const millis = Math.floor((seconds - Math.floor(seconds)) * 1000); | |
return `${pad(hours)}:${pad(minutes)}:${pad(secs)},${padMillis(millis)}`; | |
} | |
function pad(value) { | |
return value.toString().padStart(2, '0'); | |
} | |
function padMillis(value) { | |
return value.toString().padStart(3, '0'); | |
} | |
// 错误处理 | |
function handleUploadError() { | |
log('Upload error occurred'); | |
alert('上传失败,请重试'); | |
resetUploadButton(); | |
} | |
function log(msg) { | |
document.getElementById('logContent').innerText = msg; | |
console.log(msg); | |
} | |
</script> | |
</body> | |
</html> | |