Files
mediaplayer/sentence_api/static/dub-share.html
2026-08-26 10:46:09 +08:00

155 lines
5.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>口语配音</title>
<style>
body{margin:0;background:#0b0f13;color:#e7edf3;font-family:-apple-system,BlinkMacSystemFont,sans-serif}
.wrap{max-width:900px;margin:0 auto;padding:20px}
video{width:100%;border-radius:12px;background:#000;aspect-ratio:16/9}
h1{font-size:22px}.muted{color:#94a3b8;font-size:14px}
.controls{display:flex;gap:10px;margin:14px 0}
button{flex:1;height:44px;border:0;border-radius:10px;color:#fff;background:#2563eb;font-weight:700;font-size:15px}
.active{background:#14b8a6}
.sentences{display:flex;flex-direction:column;gap:8px}
.sentence{padding:12px;border-radius:10px;background:#1c2228;color:#cbd5e1;line-height:1.5}
.active-sentence{outline:2px solid #14b8a6;color:#fff}
.selected-sentence{background:#334155;color:#fff}
.score{margin-top:6px;font-size:12px;color:#94a3b8}
.score.good{color:#14b8a6}.score.low{color:#f97316}
.footer{margin-top:16px;font-size:13px;color:#94a3b8}
.footer a{color:#60a5fa;text-decoration:none}
</style>
<div class="wrap">
<h1 id="title">加载中…</h1>
<p class="muted" id="status">正在读取配音数据</p>
<video id="video" playsinline controls></video>
<div class="controls">
<button id="original">播放原音</button>
<button id="dubbing">播放配音</button>
</div>
<div class="sentences" id="sentences"></div>
</div>
<script>
const video = document.getElementById('video');
const originalButton = document.getElementById('original');
const dubbingButton = document.getElementById('dubbing');
const statusText = document.getElementById('status');
const sentenceList = document.getElementById('sentences');
let dubAudio = new Audio();
dubAudio.preload = 'auto';
let segments = [];
let selectedIndex = 0;
let mode = null;
function stopAll() {
video.pause();
dubAudio.pause();
originalButton.classList.remove('active');
dubbingButton.classList.remove('active');
mode = null;
}
function playOriginal() {
stopAll();
mode = 'original';
originalButton.classList.add('active');
video.muted = false;
playSelectedSentence();
}
function syncDub() {
const timeMs = video.currentTime * 1000;
const sentence = segments[selectedIndex];
if (sentence && timeMs >= sentence.end_ms) {
video.pause();
dubAudio.pause();
video.currentTime = sentence.end_ms / 1000;
return;
}
const active = segments.find(item => timeMs >= item.start_ms && timeMs < item.end_ms);
document.querySelectorAll('.sentence').forEach((node, index) => {
node.classList.toggle('active-sentence', segments[index] === active);
});
if (mode !== 'dubbing') return;
if (!active) {
dubAudio.pause();
return;
}
const source = active.audio_url;
if (dubAudio.dataset.src !== source) {
dubAudio.dataset.src = source;
dubAudio.src = source;
}
const target = Math.max(0, timeMs - active.start_ms) / 1000;
if (Math.abs(dubAudio.currentTime - target) > 0.25 && Number.isFinite(target)) {
dubAudio.currentTime = target;
}
if (video.paused || dubAudio.paused) dubAudio.play().catch(() => {});
}
function playDubbing() {
stopAll();
mode = 'dubbing';
dubbingButton.classList.add('active');
video.muted = true;
playSelectedSentence();
}
function playSelectedSentence() {
const sentence = segments[selectedIndex];
if (!sentence) return;
video.currentTime = sentence.start_ms / 1000;
dubAudio.dataset.src = '';
dubAudio.src = '';
video.play().catch(() => {});
}
async function load() {
try {
const shareId = location.pathname.split('/').pop();
const shareResponse = await fetch(`/api/v1/dub-shares/${shareId}`);
if (!shareResponse.ok) throw new Error(`HTTP ${shareResponse.status}`);
const share = await shareResponse.json();
segments = share.segments;
const detailResponse = await fetch(`/api/v1/videos/${share.video_hash}`);
const detail = await detailResponse.json();
video.src = detail.video.stream_url;
document.title = share.title;
document.getElementById('title').textContent = share.title;
statusText.textContent = `${segments.length} 句配音`;
segments.forEach(item => {
const node = document.createElement('div');
node.className = 'sentence';
if (item === segments[selectedIndex]) node.classList.add('selected-sentence');
node.textContent = item.text || `${item.index + 1}`;
node.onclick = () => {
selectedIndex = segments.indexOf(item);
document.querySelectorAll('.sentence').forEach((other, index) => {
other.classList.toggle('selected-sentence', index === selectedIndex);
});
video.currentTime = item.start_ms / 1000;
};
const score = document.createElement('div');
score.className = 'score' + (item.overall_score >= 80 ? ' good' : item.overall_score && item.overall_score < 60 ? ' low' : '');
score.textContent = `总分 ${Number(item.overall_score || 0).toFixed(1)}`;
if (item.recognized_text) score.textContent += ` · 识别:${item.recognized_text}`;
node.appendChild(score);
sentenceList.appendChild(node);
});
originalButton.onclick = playOriginal;
dubbingButton.onclick = playDubbing;
video.addEventListener('timeupdate', syncDub);
video.addEventListener('play', () => { if (mode === 'dubbing') syncDub(); });
video.addEventListener('pause', () => dubAudio.pause());
video.addEventListener('seeked', syncDub);
} catch (error) {
statusText.textContent = `加载失败:${error.message}`;
}
}
load();
</script>
<p class="footer">了解更多或下载跟读App<a href="https://www.pgyer.com/genduchong" target="_blank" rel="noopener">www.pgyer.com/genduchong</a></p>