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:605686962@qq.com">605686962@qq.com</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> | |