|
<!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; |
|
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"> |
|
|
|
<div style="margin: 10px;"> |
|
<label for="language_src">Select language (source):</label> |
|
<select id="language_src" name="language_src"> |
|
|
|
</select> |
|
</div> |
|
|
|
|
|
<div style="margin: 10px;"> |
|
<label for="language_dst">Select language (target):</label> |
|
<select id="language_dst" name="language_dst"> |
|
|
|
</select> |
|
</div> |
|
|
|
|
|
<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; |
|
let asrData = []; |
|
let serverUrl = "https://www.funsound.cn/st"; |
|
const chunkSize = 1 * 1024 * 1024; |
|
|
|
|
|
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() { |
|
|
|
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'); |
|
|
|
|
|
data.content.input_languages.forEach(lang => { |
|
const option = document.createElement('option'); |
|
option.value = lang; |
|
option.textContent = lang; |
|
languageSrcSelect.appendChild(option); |
|
}); |
|
|
|
|
|
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)); |
|
|
|
|
|
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)}%`; |
|
} |
|
|
|
|
|
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); |
|
}); |
|
} |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
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)); |
|
|
|
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'; |
|
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`; |
|
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> |
|
|