Add post-production pipeline: stem recorder, postprod script, recording UI

New stem recording system captures 5 time-aligned WAV files (host, caller,
music, sfx, ads) during live shows. Standalone postprod.py processes stems
into broadcast-ready MP3 with gap removal, voice compression, music ducking,
and EBU R128 loudness normalization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 17:53:32 -07:00
parent 356bf145b8
commit 7d88c76f90
12 changed files with 1528 additions and 363 deletions

View File

@@ -75,6 +75,19 @@ header button {
50% { opacity: 0.7; }
}
.rec-btn {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
background: #555 !important;
transition: background 0.2s;
}
.rec-btn.recording {
background: #cc2222 !important;
animation: on-air-pulse 2s ease-in-out infinite;
}
.new-session-btn {
background: var(--accent) !important;
}
@@ -85,17 +98,29 @@ header button {
font-weight: normal;
}
.caller-background {
details.caller-background {
font-size: 0.85rem;
color: var(--text-muted);
padding: 10px;
background: var(--bg);
border-radius: var(--radius);
margin-bottom: 12px;
line-height: 1.4;
}
.caller-background.hidden {
details.caller-background summary {
cursor: pointer;
padding: 8px 10px;
font-weight: bold;
color: var(--text);
font-size: 0.8rem;
}
details.caller-background > div {
padding: 0 10px 10px;
white-space: pre-wrap;
}
details.caller-background.hidden {
display: none;
}

View File

@@ -12,6 +12,7 @@
<h1>Luke at The Roost</h1>
<div class="header-buttons">
<button id="on-air-btn" class="on-air-btn off">OFF AIR</button>
<button id="rec-btn" class="rec-btn" title="Record stems for post-production">REC</button>
<button id="new-session-btn" class="new-session-btn">New Session</button>
<button id="export-session-btn">Export</button>
<button id="settings-btn">Settings</button>
@@ -49,7 +50,10 @@
</label>
</div>
<div id="call-status" class="call-status">No active call</div>
<div id="caller-background" class="caller-background hidden"></div>
<details id="caller-background-details" class="caller-background hidden">
<summary>Caller Background</summary>
<div id="caller-background"></div>
</details>
<button id="hangup-btn" class="hangup-btn" disabled>Hang Up</button>
</section>

View File

@@ -85,6 +85,31 @@ function initEventListeners() {
});
}
// Stem recording toggle
const recBtn = document.getElementById('rec-btn');
if (recBtn) {
let stemRecording = false;
recBtn.addEventListener('click', async () => {
try {
if (!stemRecording) {
const res = await safeFetch('/api/recording/start', { method: 'POST' });
stemRecording = true;
recBtn.classList.add('recording');
recBtn.textContent = '⏺ REC';
log('Stem recording started: ' + res.dir);
} else {
const res = await safeFetch('/api/recording/stop', { method: 'POST' });
stemRecording = false;
recBtn.classList.remove('recording');
recBtn.textContent = 'REC';
log('Stem recording stopped');
}
} catch (err) {
log('Recording error: ' + err.message);
}
});
}
// Export session
document.getElementById('export-session-btn')?.addEventListener('click', exportSession);
@@ -400,11 +425,12 @@ async function startCall(key, name) {
if (aiInfo) aiInfo.classList.remove('hidden');
if (aiName) aiName.textContent = name;
// Show caller background
// Show caller background in disclosure triangle
const bgDetails = document.getElementById('caller-background-details');
const bgEl = document.getElementById('caller-background');
if (bgEl && data.background) {
if (bgDetails && bgEl && data.background) {
bgEl.textContent = data.background;
bgEl.classList.remove('hidden');
bgDetails.classList.remove('hidden');
}
document.querySelectorAll('.caller-btn').forEach(btn => {
@@ -428,8 +454,8 @@ async function newSession() {
conversationSince = 0;
// Hide caller background
const bgEl = document.getElementById('caller-background');
if (bgEl) bgEl.classList.add('hidden');
const bgDetails = document.getElementById('caller-background-details');
if (bgDetails) bgDetails.classList.add('hidden');
// Reload callers to get new session ID
await loadCallers();
@@ -455,8 +481,8 @@ async function hangup() {
document.querySelectorAll('.caller-btn').forEach(btn => btn.classList.remove('active'));
// Hide caller background
const bgEl = document.getElementById('caller-background');
if (bgEl) bgEl.classList.add('hidden');
const bgDetails2 = document.getElementById('caller-background-details');
if (bgDetails2) bgDetails2.classList.add('hidden');
// Hide AI caller indicator
document.getElementById('ai-caller-info')?.classList.add('hidden');