Files
mediaplayer/sentence_api/static/dub-share.html
2026-09-05 13:25:12 +08:00

179 lines
6.7 KiB
HTML

<!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:16px auto 0;max-width:900px;padding:12px;border-radius:10px;background:#1c2228;font-size:13px;color:#cbd5e1;line-height:1.5}
.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';
dubAudio.playbackRate = 1;
let segments = [];
let selectedIndex = 0;
let mode = null;
const PLAY_EXTEND_MS = 500;
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;
const sentence = segments[selectedIndex];
if (!sentence) return;
video.currentTime = sentence.start_ms / 1000;
video.play().catch(() => {});
}
function highlightActiveSentence() {
const timeMs = video.currentTime * 1000;
const selected = segments[selectedIndex];
const active = (selected && timeMs >= selected.start_ms && timeMs < selected.end_ms + PLAY_EXTEND_MS)
? selected
: 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);
});
return { timeMs, selected };
}
function onVideoTimeUpdate() {
const { timeMs, selected } = highlightActiveSentence();
if (!selected) return;
// Only bound the muted video window. Dubbing audio plays at its original speed/duration.
if (timeMs >= selected.end_ms + PLAY_EXTEND_MS) {
video.pause();
if (mode === 'original') {
video.currentTime = (selected.end_ms + PLAY_EXTEND_MS) / 1000;
}
}
}
function playDubbing() {
stopAll();
mode = 'dubbing';
dubbingButton.classList.add('active');
video.muted = true;
const sentence = segments[selectedIndex];
if (!sentence || !sentence.audio_url) return;
video.currentTime = sentence.start_ms / 1000;
dubAudio.pause();
dubAudio.playbackRate = 1;
dubAudio.src = sentence.audio_url;
const startBoth = () => {
try { dubAudio.currentTime = 0; } catch (_) {}
video.play().catch(() => {});
dubAudio.play().catch(() => {});
};
if (dubAudio.readyState >= 2) {
startBoth();
} else {
dubAudio.onloadeddata = () => {
dubAudio.onloadeddata = null;
startBoth();
};
dubAudio.load();
}
}
dubAudio.addEventListener('ended', () => {
if (mode === 'dubbing') {
video.pause();
}
});
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' : '');
const details = item.score_details || {};
score.textContent = `总分 ${Number(item.overall_score || 0).toFixed(1)}`;
[
['内容分', details.content_score],
['流畅度', details.fluency_score],
['时长分', details.duration_score],
['停顿分', details.pause_score],
['语速分', details.speech_rate_score],
].forEach(([label, value]) => {
if (value != null) score.textContent += ` · ${label} ${Number(value).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', onVideoTimeUpdate);
video.addEventListener('seeked', highlightActiveSentence);
// Keep dubbing audio independent: do not pause/seek it to force video alignment.
} catch (error) {
statusText.textContent = `加载失败:${error.message}`;
}
}
load();
</script>
<div class="wrap"><p class="footer">八哥口语,让您一句一句背诵,三个月掌握一门地道外语 <a href="https://www.pgyer.com/genduchong" target="_blank" rel="noopener">欢迎下载体验</a></p></div>