QuadraV's picture
Update index.html
29cab7d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="https://www.funsound.cn/static/images/logo_funsound.png" type="image/png">
<title>Funsound Cross-Language Speech Translator</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="https://www.funsound.cn/static/images/logo_funsound.png" alt="Funsound Logo">
Funsound Cross-Language Speech Translator
</div>
<div class="content">
<!-- 左列 -->
<div class="video-container">
<div class="file-input-wrapper">
<button class="file-input">Select File</button>
<input type="file" id="videoInput" accept=".wav, .mp3, .m4a, .mp4, .aac, .webm">
</div>
<video id="videoPlayer" controls>
您的浏览器不支持 video 标签。
</video>
<div class="options-container">
<!-- Input Language Selection -->
<div style="margin: 10px;">
<label for="language_src">Select language (source):</label>
<select id="language_src" name="language_src">
<!-- Options will be populated by JavaScript -->
</select>
</div>
<!-- Output Language Selection -->
<div style="margin: 10px;">
<label for="language_dst">Select language (target):</label>
<select id="language_dst" name="language_dst">
<!-- Options will be populated by JavaScript -->
</select>
</div>
<!-- Speaker Recognition -->
<div style="margin: 10px;">
<label><input type="checkbox" id="speakerRecognition">Speaker Identification</label>
</div>
</div>
<div class="buttons-container">
<button id="uploadBtn" class="upload-button" onclick="uploadFile()">Recognize</button>
</div>
<label>Upload:</label>
<div id="uploadProgress" class="progress-bar">
<div>0%</div>
</div>
<label>Decoding:</label>
<div id="recognitionProgress" class="progress-bar">
<div>0%</div>
</div>
<label>Subtitles:</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>Results:</label>
<div id="asrList" class="asr-list"></div>
<div class="center-buttons">
<button id="exportJsonBtn" class="export-button" onclick="exportAsrData('json')">Export JSON</button>
<button id="exportSrtBtn" class="export-button" onclick="exportAsrData('srt')">Export SRT</button>
<button id="generateSubtitleBtn" class="subtitle-button" onclick="generateSubtitle()">Synthetic Subtitles</button>
<button id="downloadVideoBtn" style="display: none;" onclick="downloadVideo()">Download Video</button>
</div>
</div>
</div>
</div>
<footer>
EMAIL: <a href="mailto:[email protected]">[email protected]</a> |
HuggingFace: <a href="https://huggingface.co/spaces/QuadraV/Funsound_Multiligual_Speech_Translator">Funsound</a> |
Modelscope: <a href="https://modelscope.cn/studios/QuadraV/Funsound_Multiligual_translator">Funsound</a>
</footer>
<script>
let currentTaskId = null; // 当前任务ID
let asrData = []; // 存储识别结果数据
let serverUrl = "https://www.funsound.cn/st"; // 服务器地址
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);
});
// 更新支持的语言
document.addEventListener("DOMContentLoaded", function() {
// Initialize language dropdowns
fetch(`${serverUrl}/get_languages`)
.then(response => response.json())
.then(data => {
if (data.code === 0) {
const languageSrcSelect = document.getElementById('language_src');
const languageDstSelect = document.getElementById('language_dst');
// Populate source languages
data.content.input_languages.forEach(lang => {
const option = document.createElement('option');
option.value = lang;
option.textContent = lang;
languageSrcSelect.appendChild(option);
});
// Populate destination languages
data.content.dst_languages.forEach(lang => {
const option = document.createElement('option');
option.value = lang;
option.textContent = lang;
languageDstSelect.appendChild(option);
});
} else {
console.error(data.message);
}
})
.catch(error => console.error('Error fetching languages:', error));
// Initialize other components if necessary
initOtherComponents();
});
// 上传文件并提交任务
function uploadFile() {
const fileInput = document.getElementById('videoInput');
const file = fileInput.files[0];
if (!file) {
alert('Select one file please');
return;
}
// 附加参数
const languageSrc = document.getElementById('language_src').value;
const languageDst = document.getElementById('language_dst').value;
const speakerRecognition = document.getElementById('speakerRecognition').checked;
log('languageSrc: ' + languageSrc);
log('languageDst: ' + languageSrc);
log('Speaker Recognition: ' + speakerRecognition);
document.getElementById('uploadBtn').disabled = true;
document.getElementById('uploadBtn').innerText = 'Decoding ..';
resetProgress();
log('Server URL: ' + serverUrl);
initializeTask(file, languageSrc, languageDst, speakerRecognition);
}
// 初始化任务
function initializeTask(file, languageSrc, languageDst, speakerRecognition) {
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('language_src', languageSrc);
formData.append('language_dst', languageDst);
formData.append('speakerRecognition', speakerRecognition);
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('Initialization failed, please try again');
resetUploadButton();
}
} else {
alert('Initialization failed, please try again');
resetUploadButton();
}
};
xhr.onerror = handleUploadError;
xhr.send(formData);
}
// 上传文件块
function uploadChunks(file) {
log("Uploading ..");
const totalChunks = Math.ceil(file.size / chunkSize);
let chunkIndex = 0;
function uploadNextChunk() {
if (chunkIndex >= totalChunks) {
log("After uploading, start transcribing..");
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(`Failed to upload chunk ${chunkIndex + 1}, please try again`);
resetUploadButton();
}
} else {
alert(`Failed to upload chunk ${chunkIndex + 1}, please try again`);
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 submission failed, please try again');
resetUploadButton();
}
} else {
alert('ASR submission failed, please try again');
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('Request failed, current number of reconnections: ' + failedRequests);
if (failedRequests >= maxFailedRequests) {
clearInterval(intervalId);
alert('Continuous requests failed and the task was not completed.');
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: ${progress.toFixed(2)}%, status: ${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">Play</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="Role" class="role-field">
<input type="text" value="${entry.text}" placeholder="Text" class="text-field">
<input type="text" value="${entry.trans}" placeholder="Translation" class="trans-field">
<label>
<input type="checkbox" ${entry.drop ? 'checked' : ''}> Drop
</label>
`;
setupASREventHandlers(div, entry);
asrList.appendChild(div);
});
}
// 导出识别结果为 JSON 或 SRT 格式
function exportAsrData(format) {
if (asrData.length === 0) {
alert('No ASR data');
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('Please make sure the video file is selected and recognized');
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 = 'generating ...';
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('Subtitle generation failed, please try again');
resetGenerateButton();
}
} else {
alert('Subtitle generation failed, please try again');
resetGenerateButton();
}
};
xhr.onerror = function () {
alert('Subtitle generation failed, please try again');
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, 'Generating subtitles');
}
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("After generation, click on the player above to preview");
}
} 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: ${progress.toFixed(2)}%, status: ${msg}`;
}
// 重置上传按钮状态
function resetUploadButton() {
log('Resetting upload button state');
document.getElementById('uploadBtn').disabled = false;
document.getElementById('uploadBtn').innerText = 'Recognize';
}
// 重置字幕生成按钮状态
function resetGenerateButton() {
document.getElementById('generateSubtitleBtn').disabled = false;
document.getElementById('generateSubtitleBtn').innerText = 'Subtitles';
}
// 重置进度条和界面
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('Please make sure the subtitled video has been generated');
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('Download link generation failed, please try again');
}
} else {
alert('Download failed, please try again');
}
};
xhr.onerror = function () {
alert('An error occurred with the download request, please try again');
};
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('Upload failed, please try again');
resetUploadButton();
}
function log(msg) {
document.getElementById('logContent').innerText = msg;
console.log(msg);
}
</script>
</body>
</html>