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="https://www.funsound.cn/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%; | |
height: 80%; | |
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; | |
height: calc(100% - 50px); | |
flex-grow: 1; | |
overflow: hidden; | |
} | |
.video-container { | |
flex: 0 0 40%; | |
margin: 10px; | |
border: 1px solid #444; | |
border-radius: 8px; | |
padding: 15px; | |
box-sizing: border-box; | |
background-color: #333; | |
overflow: hidden; | |
} | |
.asr-container { | |
flex: 0 0 60%; | |
margin: 10px; | |
border: 1px solid #444; | |
border-radius: 8px; | |
padding: 15px; | |
box-sizing: border-box; | |
background-color: #333; | |
overflow-y: auto; | |
position: relative; | |
display: flex; | |
flex-direction: column; | |
} | |
video { | |
width: 100%; | |
height: 300px; | |
background-color: #000; | |
border-radius: 8px; | |
margin-bottom: 15px; | |
} | |
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: 100px; | |
padding: 5px; | |
border: 1px solid #555; | |
background-color: #444; | |
color: #eaeaea; | |
border-radius: 4px; | |
flex: 0 0 100px; /* 保持宽度固定为 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; | |
} | |
.progress-bar { | |
width: 100%; | |
background-color: #444; | |
margin-top: 10px; | |
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; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="title"> | |
<img src="https://www.funsound.cn/static/logo_whisper.png" alt="Funsound Logo"> | |
Funsound语音识别 (Whisper版) | |
</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> | |
<!-- 新增任务选择单选框 --> | |
<div style="margin: 10px 0;"> | |
<label for="taskType">任务类型:</label> | |
<label><input type="radio" name="task" value="transcribe" checked> 识别 (transcribe)</label> | |
<label><input type="radio" name="task" value="translate"> 翻译 (translate)</label> | |
</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> | |
<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> | |
</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> | | |
Funasr版本: <a href="https://www.funsound.cn">Funsound-Funasr</a> | |
</footer> | |
<script> | |
const serverUrl = "https://www.funsound.cn/whisper"; | |
let currentTaskId = null; | |
let asrData = []; | |
document.getElementById('videoInput').addEventListener('change', function (event) { | |
const file = event.target.files[0]; | |
document.getElementById('uploadBtn').disabled = file.size > 300 * 1024 * 1024; | |
const videoPlayer = document.getElementById('videoPlayer'); | |
const videoURL = URL.createObjectURL(file); | |
videoPlayer.src = videoURL; | |
}); | |
function uploadFile() { | |
const fileInput = document.getElementById('videoInput'); | |
const file = fileInput.files[0]; | |
if (!file) { | |
alert('请先选择一个文件'); | |
return; | |
} | |
// 获取选择的任务类型 | |
const taskType = document.querySelector('input[name="task"]:checked').value; | |
document.getElementById('uploadBtn').disabled = true; | |
document.getElementById('uploadBtn').innerText = '转写中...'; | |
resetProgress(); | |
const xhr = new XMLHttpRequest(); | |
xhr.open('POST', `${serverUrl}/submit`, true); | |
xhr.upload.onprogress = updateUploadProgress; | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
currentTaskId = response.content; | |
monitorTaskProgress(currentTaskId); | |
} else { | |
alert('上传失败,请重试'); | |
resetUploadButton(); | |
} | |
}; | |
xhr.onerror = handleUploadError; | |
const formData = new FormData(); | |
formData.append('file', file); | |
formData.append('task', taskType); // 将任务类型添加到请求中 | |
xhr.send(formData); | |
} | |
function resetProgress() { | |
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('asrList').innerHTML = ""; | |
document.getElementById('logContent').innerText = ""; | |
} | |
function updateUploadProgress(event) { | |
if (event.lengthComputable) { | |
const percentComplete = (event.loaded / event.total) * 100; | |
document.getElementById('uploadProgress').firstElementChild.style.width = `${percentComplete}%`; | |
document.getElementById('uploadProgress').firstElementChild.innerText = `${percentComplete.toFixed(2)}%`; | |
} | |
} | |
function handleUploadError() { | |
alert('上传失败,请重试'); | |
resetUploadButton(); | |
} | |
function resetUploadButton() { | |
document.getElementById('uploadBtn').disabled = false; | |
document.getElementById('uploadBtn').innerText = '上传并识别'; | |
} | |
function monitorTaskProgress(taskId) { | |
let retries = 0; | |
const maxRetries = 3600; | |
const intervalId = setInterval(function () { | |
const xhr = new XMLHttpRequest(); | |
xhr.open('GET', `${serverUrl}/task_prgs/${taskId}`, true); | |
xhr.onload = function () { | |
if (xhr.status === 200) { | |
const response = JSON.parse(xhr.responseText); | |
const status = response.content.status; | |
const progress = response.content.prgs; | |
if (progress) { | |
updateRecognitionProgress(progress.cur / progress.total * 100, progress.msg); | |
} | |
if (status === "SUCCESS") { | |
clearInterval(intervalId); | |
asrData = response.content.result; | |
displayResults(asrData); | |
resetUploadButton(); | |
} else if (status === "FAIL") { | |
clearInterval(intervalId); | |
alert('识别任务失败'); | |
resetUploadButton(); | |
} | |
} else { | |
alert('获取进度失败,请重试'); | |
clearInterval(intervalId); | |
resetUploadButton(); | |
} | |
}; | |
xhr.onerror = function () { | |
alert('获取进度失败,请重试'); | |
clearInterval(intervalId); | |
resetUploadButton(); | |
}; | |
xhr.send(); | |
retries += 1; | |
if (retries >= maxRetries) { | |
clearInterval(intervalId); | |
alert('超出最大重试次数,任务未完成'); | |
resetUploadButton(); | |
} | |
}, 1000); | |
} | |
function updateRecognitionProgress(progress, 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) { | |
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"> | |
<label> | |
<input type="checkbox" ${entry.drop ? 'checked' : ''}> 丢弃 | |
</label> | |
`; | |
const startInput = div.querySelector('.start-time'); | |
const endInput = div.querySelector('.end-time'); | |
const textInput = div.querySelector('input.text-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); | |
}); | |
textInput.addEventListener('input', () => { | |
entry.text = textInput.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); | |
}); | |
asrList.appendChild(div); | |
}); | |
} | |
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\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 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'); | |
} | |
</script> | |
</body> | |
</html> | |