funsound / index.html
QuadraV's picture
Update index.html
ee4b467 verified
raw
history blame
20.6 kB
<!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/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>