Files
tts/podcast_audios/project_chapter8_demo/config/player_demo.html
2026-01-19 10:27:41 +08:00

566 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MOSS-TTSD Podcast Player - Chapter 8 Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 1200px;
width: 100%;
height: 90vh;
display: grid;
grid-template-columns: 2fr 1fr;
overflow: hidden;
}
.main-area {
padding: 40px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 2em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.header .subtitle {
font-size: 1.1em;
opacity: 0.9;
}
.avatars-container {
display: flex;
justify-content: center;
align-items: center;
gap: 60px;
margin: 40px 0;
}
.avatar {
text-align: center;
transition: all 0.3s ease;
}
.avatar-circle {
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
border: 4px solid transparent;
display: flex;
align-items: center;
justify-content: center;
font-size: 3em;
margin: 0 auto 15px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.avatar.s1 .avatar-circle {
background: linear-gradient(135deg, rgba(135, 206, 235, 0.3), rgba(70, 130, 180, 0.5));
}
.avatar.s2 .avatar-circle {
background: linear-gradient(135deg, rgba(152, 251, 152, 0.3), rgba(50, 205, 50, 0.5));
}
.avatar.active .avatar-circle {
border-color: #ffd700;
box-shadow: 0 0 30px rgba(255, 215, 0, 0.6);
transform: scale(1.1);
}
.avatar.speaking::before {
content: '';
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
background: radial-gradient(circle, rgba(255, 215, 0, 0.3) 0%, transparent 70%);
border-radius: 50%;
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { transform: scale(0.8); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.5; }
100% { transform: scale(0.8); opacity: 1; }
}
.avatar-name {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 5px;
}
.avatar-role {
font-size: 0.9em;
opacity: 0.8;
}
.audio-player-container {
background: rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 20px;
margin-top: 30px;
}
.audio-player {
width: 100%;
border-radius: 8px;
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
}
.control-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
font-size: 1em;
transition: all 0.3s ease;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.control-btn.playing {
background: #4caf50;
}
.sidebar {
background: rgba(0, 0, 0, 0.2);
padding: 30px 20px;
overflow-y: auto;
}
.sidebar h2 {
font-size: 1.5em;
margin-bottom: 20px;
color: #ffd700;
}
.subtitle-line {
padding: 15px;
margin: 10px 0;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
border-left: 4px solid transparent;
transition: all 0.3s ease;
cursor: pointer;
}
.subtitle-line:hover {
background: rgba(255, 255, 255, 0.15);
}
.subtitle-line.active {
border-left-color: #ffd700;
background: rgba(255, 215, 0, 0.15);
transform: translateX(5px);
}
.subtitle-line.s1 {
border-left-color: #87ceeb;
}
.subtitle-line.s2 {
border-left-color: #98fb98;
}
.subtitle-line.s1.active {
border-left-color: #ffd700;
}
.subtitle-line.s2.active {
border-left-color: #ffd700;
}
.speaker-label {
font-weight: bold;
font-size: 0.9em;
margin-bottom: 5px;
}
.subtitle-line.s1 .speaker-label {
color: #87ceeb;
}
.subtitle-line.s2 .speaker-label {
color: #98fb98;
}
.subtitle-text {
line-height: 1.5;
font-size: 1em;
}
.time-display {
font-size: 0.8em;
opacity: 0.7;
margin-top: 5px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: 1.2em;
}
.loading::after {
content: '';
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #ffd700;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
height: 100vh;
}
.sidebar {
max-height: 40vh;
}
.avatars-container {
gap: 30px;
}
.avatar-circle {
width: 80px;
height: 80px;
font-size: 2em;
}
}
</style>
</head>
<body>
<div class="container">
<div class="main-area">
<div class="header">
<h1>MOSS-TTSD Podcast Player</h1>
<div class="subtitle">Chapter 8: Tom Clancy's Geopolitical Prophecy</div>
</div>
<div class="avatars-container">
<div class="avatar s1" id="avatar-s1">
<div class="avatar-circle">🎤</div>
<div class="avatar-name">Speaker 1</div>
<div class="avatar-role">Host</div>
</div>
<div class="avatar s2" id="avatar-s2">
<div class="avatar-circle">🎙️</div>
<div class="avatar-name">Speaker 2</div>
<div class="avatar-role">Guest</div>
</div>
</div>
<div class="audio-player-container">
<audio id="audio-player" class="audio-player" controls>
<source src="chapter8_english_demo.wav" type="audio/wav">
Your browser does not support the audio element.
</audio>
<div class="controls">
<button id="play-btn" class="control-btn">▶️ Play</button>
<button id="sync-btn" class="control-btn">🔄 Sync</button>
</div>
</div>
</div>
<div class="sidebar">
<h2>Dialogue Script</h2>
<div id="subtitle-container">
<div class="loading">Loading subtitles...</div>
</div>
</div>
</div>
<script>
class PodcastPlayer {
constructor() {
this.audio = document.getElementById('audio-player');
this.playBtn = document.getElementById('play-btn');
this.syncBtn = document.getElementById('sync-btn');
this.subtitleContainer = document.getElementById('subtitle-container');
this.avatarS1 = document.getElementById('avatar-s1');
this.avatarS2 = document.getElementById('avatar-s2');
this.subtitles = [];
this.currentSubtitleIndex = -1;
this.isPlaying = false;
this.init();
}
async init() {
await this.loadSubtitles();
this.setupEventListeners();
this.renderSubtitles();
}
async loadSubtitles() {
try {
const response = await fetch('chapter8_english_demo.srt');
const srtText = await response.text();
this.subtitles = this.parseSRT(srtText);
} catch (error) {
console.error('Failed to load subtitles:', error);
// Fallback: generate dummy subtitles for demo
this.subtitles = this.generateDummySubtitles();
}
}
parseSRT(srtText) {
const subtitles = [];
const blocks = srtText.trim().split('\n\n');
for (const block of blocks) {
const lines = block.split('\n').filter(line => line.trim()); // 过滤空行
if (lines.length >= 3) {
const timeLine = lines[1];
const textLines = lines.slice(2);
// 找到第一个非空且包含[S1]或[S2]的行
const speakerLine = textLines.find(line => line.includes('[S1]') || line.includes('[S2]'));
if (!speakerLine) continue;
const [startTime, endTime] = timeLine.split(' --> ');
const speaker = speakerLine.includes('[S1]') ? 's1' : 's2';
const text = speakerLine.replace(/\[S[12]\]\s*/, '');
subtitles.push({
start: this.parseTime(startTime),
end: this.parseTime(endTime),
speaker: speaker,
text: text
});
}
}
return subtitles;
}
generateDummySubtitles() {
return [
{ start: 0, end: 3.5, speaker: 's1', text: "Today we're discussing Tom Clancy. A thriller writer, yet called by the author the most underrated geopolitical analyst in American history." },
{ start: 3.5, end: 5.2, speaker: 's2', text: "That's a bold claim. Tell me more." },
{ start: 5.2, end: 9.8, speaker: 's1', text: "His 2000 novel 'The Bear and the Dragon' predicted the 2022 Russia-Ukraine war and the China-Russia 'no-limits' alliance, 22 years in advance." },
{ start: 9.8, end: 12.5, speaker: 's1', text: "How did a guy writing airport novels in Maryland see more clearly than the entire CIA?" },
{ start: 12.5, end: 16.2, speaker: 's2', text: "This isn't just prediction, it's simulation. He built Soviet naval strategy models so accurate that the Pentagon invited him to wargame with generals." },
{ start: 16.2, end: 19.0, speaker: 's2', text: "He understood one key thing: war is a system, not an event." },
{ start: 19.0, end: 23.5, speaker: 's1', text: "'The Hunt for Red October' is a primer on submarine acoustics. 'Clear and Present Danger' explains drug cartels and congressional oversight." },
{ start: 23.5, end: 26.0, speaker: 's1', text: "But 'The Bear and the Dragon' is truly scary." },
{ start: 26.0, end: 30.5, speaker: 's2', text: "In the book, China and Russia form a military alliance, invade Alaska, catch America off-guard, paralyze NATO." },
{ start: 30.5, end: 34.8, speaker: 's2', text: "But here's the brilliant part: the war doesn't end in nuclear apocalypse. It ends in ceasefire and a new Cold War stalemate." },
{ start: 34.8, end: 39.2, speaker: 's1', text: "In 2022, China and Russia didn't invade Alaska, but they did form a 'no-limits' alliance that caught the West off-guard." },
{ start: 39.2, end: 41.0, speaker: 's1', text: "Why was Clancy right?" },
{ start: 41.0, end: 45.5, speaker: 's2', text: "Because he understood the mathematics of power. When China's economy became 10 times Russia's, the math changed." },
{ start: 45.5, end: 49.0, speaker: 's2', text: "Russia could no longer be a peer player, it had to become a junior partner. This isn't ideology, it's arithmetic." },
{ start: 49.0, end: 54.5, speaker: 's1', text: "The scariest part: when Clancy wrote the book, China's GDP was still smaller than Italy's. Everyone thought he was crazy." },
{ start: 54.5, end: 58.0, speaker: 's1', text: "But he saw the trajectory. He saw that the 21st century would be Asian." },
{ start: 58.0, end: 62.5, speaker: 's2', text: "China had two choices: dominate its neighbors or merge with them. China chose to merge. Russia had no choice." },
{ start: 62.5, end: 66.8, speaker: 's2', text: "That's why Clancy isn't a novelist, he's an analyst. He read the same data as everyone else, but he knew how to read it for blood." }
];
}
parseTime(timeStr) {
const [time, ms] = timeStr.split(',');
const [hours, minutes, seconds] = time.split(':');
return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds) + parseInt(ms) / 1000;
}
setupEventListeners() {
this.audio.addEventListener('timeupdate', () => {
this.updateSubtitle();
});
this.audio.addEventListener('play', () => {
this.isPlaying = true;
this.playBtn.textContent = '⏸️ Pause';
this.playBtn.classList.add('playing');
});
this.audio.addEventListener('pause', () => {
this.isPlaying = false;
this.playBtn.textContent = '▶️ Play';
this.playBtn.classList.remove('playing');
});
this.playBtn.addEventListener('click', () => {
if (this.audio.paused) {
this.audio.play();
} else {
this.audio.pause();
}
});
this.syncBtn.addEventListener('click', () => {
this.syncSubtitles();
});
}
updateSubtitle() {
const currentTime = this.audio.currentTime;
const subtitleIndex = this.findSubtitleIndex(currentTime);
if (subtitleIndex !== this.currentSubtitleIndex) {
console.log('Subtitle changed:', subtitleIndex, 'at time:', currentTime);
this.currentSubtitleIndex = subtitleIndex;
this.highlightSubtitle(subtitleIndex);
this.updateAvatar(subtitleIndex >= 0 ? this.subtitles[subtitleIndex].speaker : null);
}
}
findSubtitleIndex(time) {
for (let i = 0; i < this.subtitles.length; i++) {
if (time >= this.subtitles[i].start && time < this.subtitles[i].end) {
return i;
}
}
return -1;
}
highlightSubtitle(index) {
// Remove previous active class
const prevActive = document.querySelector('.subtitle-line.active');
if (prevActive) {
prevActive.classList.remove('active');
}
// Add active class to current subtitle
if (index >= 0) {
const element = document.querySelector(`[data-subtitle-index="${index}"]`);
if (element) {
console.log('Highlighting subtitle:', index, element);
element.classList.add('active');
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
console.error('Subtitle element not found for index:', index);
}
}
}
updateAvatar(speaker) {
// Reset avatars
this.avatarS1.classList.remove('active', 'speaking');
this.avatarS2.classList.remove('active', 'speaking');
// Activate current speaker
if (speaker === 's1') {
console.log('Activating S1 avatar');
this.avatarS1.classList.add('active', 'speaking');
} else if (speaker === 's2') {
console.log('Activating S2 avatar');
this.avatarS2.classList.add('active', 'speaking');
} else {
console.log('No speaker active');
}
}
renderSubtitles() {
this.subtitleContainer.innerHTML = '';
this.subtitles.forEach((subtitle, index) => {
const lineElement = document.createElement('div');
lineElement.className = `subtitle-line ${subtitle.speaker}`;
lineElement.setAttribute('data-subtitle-index', index);
lineElement.innerHTML = `
<div class="speaker-label">[${subtitle.speaker.toUpperCase()}]</div>
<div class="subtitle-text">${subtitle.text}</div>
<div class="time-display">${this.formatTime(subtitle.start)} - ${this.formatTime(subtitle.end)}</div>
`;
lineElement.addEventListener('click', () => {
this.audio.currentTime = subtitle.start;
this.audio.play();
});
this.subtitleContainer.appendChild(lineElement);
});
}
formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
syncSubtitles() {
// Recalculate subtitle timings based on audio duration
const audioDuration = this.audio.duration;
if (audioDuration && this.subtitles.length > 0) {
const subtitleDuration = audioDuration / this.subtitles.length;
this.subtitles.forEach((subtitle, index) => {
subtitle.start = index * subtitleDuration;
subtitle.end = (index + 1) * subtitleDuration;
});
console.log('Subtitles synced to audio duration:', audioDuration);
}
}
}
// Initialize player when page loads
document.addEventListener('DOMContentLoaded', () => {
new PodcastPlayer();
});
</script>
</body>
</html>