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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user