Postprod overhaul, control panel theme, caller names, website updates
- Fix denoise mangling host audio: strip aggressive afftdn/anlmdn, keep HPF only - Add stem limiting for ads/SFX to prevent clipping - Spoken-word compression on host (threshold -28dB, ratio 4:1) - Add bus compressor on final stereo mix (LRA 7.9 → 5.7 LU) - Drop SFX mix level from -6dB to -10dB - De-esser fix: replace split-band with simple high-shelf EQ - Pipeline now 15 steps (was 13) - Control panel theme: match website warm brown/orange palette - Expand caller names to 160 (80M/80F), fix duplicate name bug - Update how-it-works page: returning callers, 15-step pipeline, remove busy diagram row Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,12 +46,26 @@ MALE_NAMES = [
|
||||
"Tony", "Rick", "Dennis", "Earl", "Marcus", "Keith", "Darnell", "Wayne",
|
||||
"Greg", "Andre", "Ray", "Jerome", "Hector", "Travis", "Vince", "Leon",
|
||||
"Dale", "Frank", "Terrence", "Bobby", "Cliff", "Nate", "Reggie", "Carl",
|
||||
"Donnie", "Mitch", "Lamar", "Tyrone", "Russell", "Cedric", "Marvin", "Curtis",
|
||||
"Rodney", "Clarence", "Floyd", "Otis", "Chester", "Leroy", "Melvin", "Vernon",
|
||||
"Dwight", "Benny", "Elvin", "Alonzo", "Dexter", "Roland", "Wendell", "Clyde",
|
||||
"Luther", "Virgil", "Ernie", "Lenny", "Sal", "Gus", "Moe", "Archie",
|
||||
"Duke", "Sonny", "Red", "Butch", "Skeeter", "T-Bone", "Slim", "Big Mike",
|
||||
"Chip", "Ricky", "Darryl", "Pete", "Artie", "Stu", "Phil", "Murray",
|
||||
"Norm", "Woody", "Rocco", "Paulie", "Vinnie", "Frankie", "Mikey", "Joey",
|
||||
]
|
||||
|
||||
FEMALE_NAMES = [
|
||||
"Jasmine", "Megan", "Tanya", "Carla", "Brenda", "Sheila", "Denise", "Tamika",
|
||||
"Lorraine", "Crystal", "Angie", "Renee", "Monique", "Gina", "Patrice", "Deb",
|
||||
"Shonda", "Marlene", "Yolanda", "Stacy", "Jackie", "Carmen", "Rita", "Val",
|
||||
"Diane", "Connie", "Wanda", "Doris", "Maxine", "Gladys", "Pearl", "Lucille",
|
||||
"Rochelle", "Bernadette", "Thelma", "Dolores", "Naomi", "Bonnie", "Francine", "Irene",
|
||||
"Estelle", "Charlene", "Yvonne", "Roberta", "Darlene", "Adrienne", "Vivian", "Rosalie",
|
||||
"Pam", "Barb", "Cheryl", "Jolene", "Mavis", "Faye", "Luann", "Peggy",
|
||||
"Dot", "Bev", "Tina", "Lori", "Sandy", "Debbie", "Terri", "Cindy",
|
||||
"Tonya", "Keisha", "Latoya", "Shaniqua", "Aaliyah", "Ebony", "Lakisha", "Shanice",
|
||||
"Nikki", "Candy", "Misty", "Brandy", "Tiffany", "Amber", "Heather", "Jen",
|
||||
]
|
||||
|
||||
# Voice pools per TTS provider
|
||||
@@ -121,8 +135,20 @@ def _randomize_callers():
|
||||
Overrides 2-3 slots with returning regulars when available."""
|
||||
num_m = sum(1 for c in CALLER_BASES.values() if c["gender"] == "male")
|
||||
num_f = sum(1 for c in CALLER_BASES.values() if c["gender"] == "female")
|
||||
males = random.sample(MALE_NAMES, num_m)
|
||||
females = random.sample(FEMALE_NAMES, num_f)
|
||||
|
||||
# Get returning callers first so we can exclude their names from random pool
|
||||
returning = []
|
||||
try:
|
||||
returning = regular_caller_service.get_returning_callers(random.randint(2, 3))
|
||||
except Exception as e:
|
||||
print(f"[Regulars] Failed to get returning callers: {e}")
|
||||
|
||||
returning_names = {r["name"] for r in returning}
|
||||
avail_males = [n for n in MALE_NAMES if n not in returning_names]
|
||||
avail_females = [n for n in FEMALE_NAMES if n not in returning_names]
|
||||
|
||||
males = random.sample(avail_males, num_m)
|
||||
females = random.sample(avail_females, num_f)
|
||||
male_pool, female_pool = _get_voice_pools()
|
||||
m_voices = random.sample(male_pool, min(num_m, len(male_pool)))
|
||||
f_voices = random.sample(female_pool, min(num_f, len(female_pool)))
|
||||
@@ -141,7 +167,6 @@ def _randomize_callers():
|
||||
|
||||
# Override 2-3 random slots with returning callers
|
||||
try:
|
||||
returning = regular_caller_service.get_returning_callers(random.randint(2, 3))
|
||||
if returning:
|
||||
keys_by_gender = {"male": [], "female": []}
|
||||
for k, v in CALLER_BASES.items():
|
||||
|
||||
@@ -246,9 +246,13 @@
|
||||
{
|
||||
"summary": "Megan, a kindergarten teacher from the bootheel, called in after one of her students asked if stars know we're looking at them, which led her to reflect on how her sister Crystal in Flagstaff has stopped appreciating the night sky despite having access to it. The conversation took an unexpected turn when Luke challenged her to admit a gross habit, and after some prodding, she confessed to picking dry skin off her feet while watching TV and flicking it on the floor.",
|
||||
"timestamp": 1770870641.723117
|
||||
},
|
||||
{
|
||||
"summary": "Here is a 1-2 sentence summary of the call:\n\nThe caller, Megan, is following up on a previous call about her sister Crystal, who lives in Flagstaff and has lost appreciation for the night sky. Megan seems eager to provide an update on the situation with her sister.",
|
||||
"timestamp": 1770894505.175125
|
||||
}
|
||||
],
|
||||
"last_call": 1770870641.723117,
|
||||
"last_call": 1770894505.175125,
|
||||
"created_at": 1770870641.723117,
|
||||
"voice": "cgSgspJ2msm6clMCkdW9"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
/* AI Radio Show - Clean CSS */
|
||||
/* AI Radio Show - Control Panel */
|
||||
|
||||
:root {
|
||||
--bg: #1a1a2e;
|
||||
--bg-light: #252547;
|
||||
--accent: #e94560;
|
||||
--text: #fff;
|
||||
--text-muted: #888;
|
||||
--radius: 8px;
|
||||
--bg: #1a1209;
|
||||
--bg-light: #2a2015;
|
||||
--bg-dark: #110c05;
|
||||
--accent: #e8791d;
|
||||
--accent-hover: #f59a4a;
|
||||
--accent-red: #cc2222;
|
||||
--accent-green: #5a8a3c;
|
||||
--text: #f5f0e5;
|
||||
--text-muted: #9a8b78;
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -16,7 +21,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
@@ -38,6 +43,8 @@ header {
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
@@ -48,10 +55,16 @@ header h1 {
|
||||
header button {
|
||||
background: var(--bg-light);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border: 1px solid rgba(232, 121, 29, 0.15);
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
header button:hover {
|
||||
background: #3a2e1f;
|
||||
border-color: rgba(232, 121, 29, 0.3);
|
||||
}
|
||||
|
||||
.on-air-btn {
|
||||
@@ -62,11 +75,14 @@ header button {
|
||||
}
|
||||
|
||||
.on-air-btn.off {
|
||||
background: #666 !important;
|
||||
background: #4a3d2e !important;
|
||||
border-color: transparent !important;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.on-air-btn.on {
|
||||
background: #cc2222 !important;
|
||||
background: var(--accent-red) !important;
|
||||
border-color: var(--accent-red) !important;
|
||||
animation: on-air-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -79,17 +95,27 @@ header button {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
background: #555 !important;
|
||||
background: #4a3d2e !important;
|
||||
color: var(--text-muted) !important;
|
||||
border-color: transparent !important;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.rec-btn.recording {
|
||||
background: #cc2222 !important;
|
||||
background: var(--accent-red) !important;
|
||||
color: var(--text) !important;
|
||||
border-color: var(--accent-red) !important;
|
||||
animation: on-air-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.new-session-btn {
|
||||
background: var(--accent) !important;
|
||||
border-color: var(--accent) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.new-session-btn:hover {
|
||||
background: var(--accent-hover) !important;
|
||||
}
|
||||
|
||||
.session-id {
|
||||
@@ -102,7 +128,7 @@ details.caller-background {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -142,10 +168,14 @@ section {
|
||||
background: var(--bg-light);
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid rgba(232, 121, 29, 0.08);
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-size: 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
@@ -163,7 +193,7 @@ section h2 {
|
||||
color: var(--text);
|
||||
border: 2px solid transparent;
|
||||
padding: 10px 8px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
@@ -171,11 +201,13 @@ section h2 {
|
||||
|
||||
.caller-btn:hover {
|
||||
border-color: var(--accent);
|
||||
background: #2a1e10;
|
||||
}
|
||||
|
||||
.caller-btn.active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.call-status {
|
||||
@@ -187,13 +219,18 @@ section h2 {
|
||||
|
||||
.hangup-btn {
|
||||
width: 100%;
|
||||
background: #c0392b;
|
||||
background: var(--accent-red);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.hangup-btn:hover {
|
||||
background: #e03030;
|
||||
}
|
||||
|
||||
.hangup-btn:disabled {
|
||||
@@ -215,25 +252,26 @@ section h2 {
|
||||
.chat-log {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-dark);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgba(232, 121, 29, 0.06);
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message.host {
|
||||
background: #2c5282;
|
||||
background: #3a2510;
|
||||
}
|
||||
|
||||
.message.caller {
|
||||
background: #553c9a;
|
||||
background: #2a1a0a;
|
||||
}
|
||||
|
||||
.message strong {
|
||||
@@ -254,7 +292,7 @@ section h2 {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
@@ -262,11 +300,11 @@ section h2 {
|
||||
}
|
||||
|
||||
.talk-btn:hover {
|
||||
filter: brightness(1.1);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.talk-btn.recording {
|
||||
background: #c0392b;
|
||||
background: var(--accent-red);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@@ -278,10 +316,15 @@ section h2 {
|
||||
.type-btn {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border: 1px solid rgba(232, 121, 29, 0.15);
|
||||
padding: 16px 24px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.status {
|
||||
@@ -301,8 +344,8 @@ section h2 {
|
||||
padding: 10px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid rgba(232, 121, 29, 0.15);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -315,14 +358,21 @@ section h2 {
|
||||
.music-controls button {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border: 1px solid rgba(232, 121, 29, 0.15);
|
||||
padding: 10px 16px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.music-controls button:hover {
|
||||
border-color: var(--accent);
|
||||
background: #2a1e10;
|
||||
}
|
||||
|
||||
.music-controls input[type="range"] {
|
||||
flex: 1;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Soundboard */
|
||||
@@ -335,9 +385,9 @@ section h2 {
|
||||
.sound-btn {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border: 1px solid rgba(232, 121, 29, 0.1);
|
||||
padding: 12px 8px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.1s;
|
||||
@@ -345,6 +395,8 @@ section h2 {
|
||||
|
||||
.sound-btn:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sound-btn:active {
|
||||
@@ -372,17 +424,19 @@ section h2 {
|
||||
border-radius: var(--radius);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
border: 1px solid rgba(232, 121, 29, 0.15);
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-bottom: 16px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
margin: 16px 0 8px 0;
|
||||
border-bottom: 1px solid var(--bg);
|
||||
border-bottom: 1px solid rgba(232, 121, 29, 0.1);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -436,11 +490,18 @@ section h2 {
|
||||
padding: 10px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid rgba(232, 121, 29, 0.15);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.modal-content select:focus,
|
||||
.modal-content input[type="text"]:focus,
|
||||
.modal-content textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -451,9 +512,10 @@ section h2 {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-buttons button:first-child {
|
||||
@@ -461,25 +523,32 @@ section h2 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-buttons button:first-child:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.modal-buttons button:last-child {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
border: 1px solid rgba(232, 121, 29, 0.15);
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: var(--bg);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--bg-light);
|
||||
border: 1px solid rgba(232, 121, 29, 0.15);
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: var(--bg-light);
|
||||
color: var(--text);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
@@ -522,28 +591,29 @@ section h2 {
|
||||
.server-btn {
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.server-btn.restart {
|
||||
background: #2196F3;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.server-btn.restart:hover {
|
||||
background: #1976D2;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.server-btn.stop {
|
||||
background: #c0392b;
|
||||
background: var(--accent-red);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.server-btn.stop:hover {
|
||||
background: #a93226;
|
||||
background: #e03030;
|
||||
}
|
||||
|
||||
.auto-scroll-label {
|
||||
@@ -555,16 +625,21 @@ section h2 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auto-scroll-label input[type="checkbox"] {
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.server-log {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #0d0d1a;
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-dark);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: #8f8;
|
||||
color: #b8a88a;
|
||||
border: 1px solid rgba(232, 121, 29, 0.06);
|
||||
}
|
||||
|
||||
.server-log .log-line {
|
||||
@@ -573,69 +648,70 @@ section h2 {
|
||||
}
|
||||
|
||||
.server-log .log-line.error {
|
||||
color: #f88;
|
||||
color: #e8604a;
|
||||
}
|
||||
|
||||
.server-log .log-line.warning {
|
||||
color: #ff8;
|
||||
color: #e8b84a;
|
||||
}
|
||||
|
||||
.server-log .log-line.tts {
|
||||
color: #8ff;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.server-log .log-line.chat {
|
||||
color: #f8f;
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Call Queue */
|
||||
.queue-section { margin: 1rem 0; }
|
||||
.call-queue { border: 1px solid #333; border-radius: 4px; padding: 0.5rem; max-height: 150px; overflow-y: auto; }
|
||||
.queue-empty { color: #666; text-align: center; padding: 0.5rem; }
|
||||
.queue-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.4rem 0.5rem; border-bottom: 1px solid #222; }
|
||||
.call-queue { border: 1px solid rgba(232, 121, 29, 0.15); border-radius: var(--radius-sm); padding: 0.5rem; max-height: 150px; overflow-y: auto; }
|
||||
.queue-empty { color: var(--text-muted); text-align: center; padding: 0.5rem; }
|
||||
.queue-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.4rem 0.5rem; border-bottom: 1px solid rgba(232, 121, 29, 0.08); flex-wrap: wrap; }
|
||||
.queue-item:last-child { border-bottom: none; }
|
||||
.queue-phone { font-family: monospace; color: #4fc3f7; }
|
||||
.queue-wait { color: #999; font-size: 0.85rem; flex: 1; }
|
||||
.queue-take-btn { background: #2e7d32; color: white; border: none; padding: 0.25rem 0.75rem; border-radius: 3px; cursor: pointer; }
|
||||
.queue-take-btn:hover { background: #388e3c; }
|
||||
.queue-drop-btn { background: #c62828; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 3px; cursor: pointer; }
|
||||
.queue-drop-btn:hover { background: #d32f2f; }
|
||||
.queue-phone { font-family: monospace; color: var(--accent); }
|
||||
.queue-wait { color: var(--text-muted); font-size: 0.85rem; flex: 1; }
|
||||
.queue-take-btn { background: var(--accent-green); color: white; border: none; padding: 0.25rem 0.75rem; border-radius: var(--radius-sm); cursor: pointer; transition: background 0.2s; }
|
||||
.queue-take-btn:hover { background: #6a9a4c; }
|
||||
.queue-drop-btn { background: var(--accent-red); color: white; border: none; padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); cursor: pointer; transition: background 0.2s; }
|
||||
.queue-drop-btn:hover { background: #e03030; }
|
||||
|
||||
/* Active Call Indicator */
|
||||
.active-call { border: 1px solid #444; border-radius: 4px; padding: 0.75rem; margin: 0.5rem 0; background: #1a1a2e; }
|
||||
.active-call { border: 1px solid rgba(232, 121, 29, 0.15); border-radius: var(--radius-sm); padding: 0.75rem; margin: 0.5rem 0; background: var(--bg); }
|
||||
.caller-info { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.caller-info:last-of-type { margin-bottom: 0; }
|
||||
.caller-type { font-size: 0.7rem; font-weight: bold; padding: 0.15rem 0.4rem; border-radius: 3px; text-transform: uppercase; }
|
||||
.caller-type.real { background: #c62828; color: white; }
|
||||
.caller-type.ai { background: #1565c0; color: white; }
|
||||
.channel-badge { font-size: 0.75rem; color: #999; background: #222; padding: 0.1rem 0.4rem; border-radius: 3px; }
|
||||
.call-duration { font-family: monospace; color: #4fc3f7; }
|
||||
.caller-type { font-size: 0.7rem; font-weight: bold; padding: 0.15rem 0.4rem; border-radius: var(--radius-sm); text-transform: uppercase; }
|
||||
.caller-type.real { background: var(--accent-red); color: white; }
|
||||
.caller-type.ai { background: var(--accent); color: white; }
|
||||
.channel-badge { font-size: 0.75rem; color: var(--text-muted); background: var(--bg-light); padding: 0.1rem 0.4rem; border-radius: var(--radius-sm); }
|
||||
.call-duration { font-family: monospace; color: var(--accent); }
|
||||
.ai-controls { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }
|
||||
.mode-toggle { display: flex; border: 1px solid #444; border-radius: 3px; overflow: hidden; }
|
||||
.mode-btn { background: #222; color: #999; border: none; padding: 0.2rem 0.5rem; font-size: 0.75rem; cursor: pointer; }
|
||||
.mode-btn.active { background: #1565c0; color: white; }
|
||||
.respond-btn { background: #2e7d32; color: white; border: none; padding: 0.25rem 0.75rem; border-radius: 3px; font-size: 0.8rem; cursor: pointer; }
|
||||
.mode-toggle { display: flex; border: 1px solid rgba(232, 121, 29, 0.2); border-radius: var(--radius-sm); overflow: hidden; }
|
||||
.mode-btn { background: var(--bg-light); color: var(--text-muted); border: none; padding: 0.2rem 0.5rem; font-size: 0.75rem; cursor: pointer; transition: all 0.2s; }
|
||||
.mode-btn.active { background: var(--accent); color: white; }
|
||||
.respond-btn { background: var(--accent-green); color: white; border: none; padding: 0.25rem 0.75rem; border-radius: var(--radius-sm); font-size: 0.8rem; cursor: pointer; transition: background 0.2s; }
|
||||
.respond-btn:hover { background: #6a9a4c; }
|
||||
.hangup-btn.small { font-size: 0.75rem; padding: 0.2rem 0.5rem; }
|
||||
.auto-followup-label { display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: #999; margin-top: 0.5rem; }
|
||||
.auto-followup-label { display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: var(--text-muted); margin-top: 0.5rem; }
|
||||
|
||||
/* Returning Caller */
|
||||
.caller-btn.returning {
|
||||
border-color: #f9a825;
|
||||
color: #f9a825;
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.caller-btn.returning:hover {
|
||||
border-color: #fdd835;
|
||||
border-color: var(--accent-hover);
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Screening Badges */
|
||||
.screening-badge { font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: 3px; font-weight: bold; }
|
||||
.screening-badge.screening { background: #e65100; color: white; animation: pulse 1.5s infinite; }
|
||||
.screening-badge.screened { background: #2e7d32; color: white; }
|
||||
.screening-summary { font-size: 0.8rem; color: #aaa; font-style: italic; flex-basis: 100%; margin-top: 0.2rem; }
|
||||
.queue-item { flex-wrap: wrap; }
|
||||
.screening-badge { font-size: 0.7rem; padding: 0.1rem 0.4rem; border-radius: var(--radius-sm); font-weight: bold; }
|
||||
.screening-badge.screening { background: var(--accent); color: white; animation: pulse 1.5s infinite; }
|
||||
.screening-badge.screened { background: var(--accent-green); color: white; }
|
||||
.screening-summary { font-size: 0.8rem; color: var(--text-muted); font-style: italic; flex-basis: 100%; margin-top: 0.2rem; }
|
||||
|
||||
/* Three-Party Chat */
|
||||
.message.real-caller { border-left: 3px solid #c62828; padding-left: 0.5rem; }
|
||||
.message.ai-caller { border-left: 3px solid #1565c0; padding-left: 0.5rem; }
|
||||
.message.host { border-left: 3px solid #2e7d32; padding-left: 0.5rem; }
|
||||
.message.real-caller { border-left: 3px solid var(--accent-red); padding-left: 0.5rem; }
|
||||
.message.ai-caller { border-left: 3px solid var(--accent); padding-left: 0.5rem; }
|
||||
.message.host { border-left: 3px solid var(--accent-green); padding-left: 0.5rem; }
|
||||
|
||||
136
postprod.py
136
postprod.py
@@ -159,19 +159,12 @@ def remove_gaps(stems: dict[str, np.ndarray], sr: int,
|
||||
|
||||
|
||||
def denoise(audio: np.ndarray, sr: int, tmp_dir: Path) -> np.ndarray:
|
||||
"""High-quality noise reduction with HPF + adaptive FFT denoiser."""
|
||||
"""HPF to cut rumble below 80Hz (plosives, HVAC, handling noise)."""
|
||||
in_path = tmp_dir / "host_pre_denoise.wav"
|
||||
out_path = tmp_dir / "host_post_denoise.wav"
|
||||
sf.write(str(in_path), audio, sr)
|
||||
|
||||
# highpass: cut rumble below 80Hz (plosives, HVAC, handling noise)
|
||||
# afftdn: adaptive FFT Wiener filter for steady-state noise
|
||||
# anlmdn: non-local means for residual broadband noise
|
||||
af = (
|
||||
"highpass=f=80:poles=2,"
|
||||
"afftdn=nt=w:om=o:nr=12:nf=-30,"
|
||||
"anlmdn=s=4:p=0.002"
|
||||
)
|
||||
af = "highpass=f=80:poles=2"
|
||||
cmd = ["ffmpeg", "-y", "-i", str(in_path), "-af", af, str(out_path)]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
@@ -188,15 +181,9 @@ def deess(audio: np.ndarray, sr: int, tmp_dir: Path) -> np.ndarray:
|
||||
out_path = tmp_dir / "host_post_deess.wav"
|
||||
sf.write(str(in_path), audio, sr)
|
||||
|
||||
# Split-band de-esser: compress the 4-9kHz sibilance band aggressively
|
||||
# while leaving everything else untouched, then recombine.
|
||||
# Uses ffmpeg's crossfeed-style approach with bandpass + compressor.
|
||||
af = (
|
||||
"asplit=2[full][sib];"
|
||||
"[sib]highpass=f=4000:poles=2,lowpass=f=9000:poles=2,"
|
||||
"acompressor=threshold=-30dB:ratio=6:attack=1:release=50:makeup=0dB[compressed_sib];"
|
||||
"[full][compressed_sib]amix=inputs=2:weights=1 0.4:normalize=0"
|
||||
)
|
||||
# Gentle high-shelf reduction at 5kHz (-4dB) to tame sibilance
|
||||
# Single-pass, no phase issues unlike split-band approaches
|
||||
af = "equalizer=f=5500:t=h:w=2000:g=-4"
|
||||
cmd = ["ffmpeg", "-y", "-i", str(in_path), "-af", af, str(out_path)]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
@@ -269,6 +256,30 @@ def reduce_breaths(audio: np.ndarray, sr: int, reduction_db: float = -12) -> np.
|
||||
return (audio * gain_samples).astype(np.float32)
|
||||
|
||||
|
||||
def limit_stem(audio: np.ndarray, sr: int, tmp_dir: Path,
|
||||
stem_name: str) -> np.ndarray:
|
||||
"""Hard-limit a stem to -1dB true peak to prevent clipping."""
|
||||
peak = np.max(np.abs(audio))
|
||||
if peak <= 0.89: # already below -1dB
|
||||
return audio
|
||||
in_path = tmp_dir / f"{stem_name}_pre_limit.wav"
|
||||
out_path = tmp_dir / f"{stem_name}_post_limit.wav"
|
||||
sf.write(str(in_path), audio, sr)
|
||||
cmd = [
|
||||
"ffmpeg", "-y", "-i", str(in_path),
|
||||
"-af", "alimiter=limit=-1dB:level=false:attack=0.1:release=50",
|
||||
str(out_path),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f" WARNING: limiting failed for {stem_name}: {result.stderr[:200]}")
|
||||
return audio
|
||||
limited, _ = sf.read(str(out_path), dtype="float32")
|
||||
peak_db = 20 * np.log10(peak)
|
||||
print(f" {stem_name}: peak was {peak_db:+.1f}dB, limited to -1dB")
|
||||
return limited
|
||||
|
||||
|
||||
def compress_voice(audio: np.ndarray, sr: int, tmp_dir: Path,
|
||||
stem_name: str) -> np.ndarray:
|
||||
in_path = tmp_dir / f"{stem_name}_pre_comp.wav"
|
||||
@@ -276,9 +287,15 @@ def compress_voice(audio: np.ndarray, sr: int, tmp_dir: Path,
|
||||
|
||||
sf.write(str(in_path), audio, sr)
|
||||
|
||||
if stem_name == "host":
|
||||
# Spoken word compression: lower threshold, higher ratio, more makeup
|
||||
af = "acompressor=threshold=-28dB:ratio=4:attack=5:release=600:makeup=8dB"
|
||||
else:
|
||||
af = "acompressor=threshold=-24dB:ratio=2.5:attack=10:release=800:makeup=6dB"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg", "-y", "-i", str(in_path),
|
||||
"-af", "acompressor=threshold=-24dB:ratio=2.5:attack=10:release=800:makeup=6dB",
|
||||
"-af", af,
|
||||
str(out_path),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
@@ -391,7 +408,7 @@ def mix_stems(stems: dict[str, np.ndarray],
|
||||
levels: dict[str, float] | None = None,
|
||||
stereo_imaging: bool = True) -> np.ndarray:
|
||||
if levels is None:
|
||||
levels = {"host": 0, "caller": 0, "music": -6, "sfx": -6, "ads": 0}
|
||||
levels = {"host": 0, "caller": 0, "music": -6, "sfx": -10, "ads": 0}
|
||||
|
||||
gains = {name: 10 ** (db / 20) for name, db in levels.items()}
|
||||
|
||||
@@ -445,6 +462,25 @@ def mix_stems(stems: dict[str, np.ndarray],
|
||||
return stereo
|
||||
|
||||
|
||||
def bus_compress(audio: np.ndarray, sr: int, tmp_dir: Path) -> np.ndarray:
|
||||
"""Gentle bus compression on the final stereo mix to glue everything together."""
|
||||
in_path = tmp_dir / "bus_pre.wav"
|
||||
out_path = tmp_dir / "bus_post.wav"
|
||||
sf.write(str(in_path), audio, sr)
|
||||
|
||||
# Gentle glue compressor: slow attack lets transients through,
|
||||
# low ratio just levels out the overall dynamics
|
||||
af = "acompressor=threshold=-20dB:ratio=2:attack=20:release=300:makeup=2dB"
|
||||
cmd = ["ffmpeg", "-y", "-i", str(in_path), "-af", af, str(out_path)]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f" WARNING: bus compression failed: {result.stderr[:200]}")
|
||||
return audio
|
||||
|
||||
compressed, _ = sf.read(str(out_path), dtype="float32")
|
||||
return compressed
|
||||
|
||||
|
||||
def trim_silence(audio: np.ndarray, sr: int, pad_s: float = 0.5,
|
||||
threshold_db: float = -50) -> np.ndarray:
|
||||
"""Trim leading and trailing silence from stereo audio."""
|
||||
@@ -721,7 +757,7 @@ def main():
|
||||
print("Dry run — exiting")
|
||||
return
|
||||
|
||||
total_steps = 13
|
||||
total_steps = 15
|
||||
|
||||
# Step 1: Load
|
||||
print(f"\n[1/{total_steps}] Loading stems...")
|
||||
@@ -734,8 +770,16 @@ def main():
|
||||
else:
|
||||
print(" Skipped")
|
||||
|
||||
# Step 3: Host mic noise reduction + HPF
|
||||
print(f"\n[3/{total_steps}] Host noise reduction + HPF...")
|
||||
# Step 3: Limit ads + SFX (prevent clipping)
|
||||
print(f"\n[3/{total_steps}] Limiting ads + SFX...")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_dir = Path(tmp)
|
||||
for name in ["ads", "sfx"]:
|
||||
if np.any(stems[name] != 0):
|
||||
stems[name] = limit_stem(stems[name], sr, tmp_dir, name)
|
||||
|
||||
# Step 4: Host mic noise reduction + HPF
|
||||
print(f"\n[4/{total_steps}] Host noise reduction + HPF...")
|
||||
if not args.no_denoise and np.any(stems["host"] != 0):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
stems["host"] = denoise(stems["host"], sr, Path(tmp))
|
||||
@@ -743,8 +787,8 @@ def main():
|
||||
else:
|
||||
print(" Skipped" if args.no_denoise else " No host audio")
|
||||
|
||||
# Step 4: De-essing
|
||||
print(f"\n[4/{total_steps}] De-essing host...")
|
||||
# Step 5: De-essing
|
||||
print(f"\n[5/{total_steps}] De-essing host...")
|
||||
if not args.no_deess and np.any(stems["host"] != 0):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
stems["host"] = deess(stems["host"], sr, Path(tmp))
|
||||
@@ -752,15 +796,15 @@ def main():
|
||||
else:
|
||||
print(" Skipped" if args.no_deess else " No host audio")
|
||||
|
||||
# Step 5: Breath reduction
|
||||
print(f"\n[5/{total_steps}] Breath reduction...")
|
||||
# Step 6: Breath reduction
|
||||
print(f"\n[6/{total_steps}] Breath reduction...")
|
||||
if not args.no_breath_reduction and np.any(stems["host"] != 0):
|
||||
stems["host"] = reduce_breaths(stems["host"], sr)
|
||||
else:
|
||||
print(" Skipped" if args.no_breath_reduction else " No host audio")
|
||||
|
||||
# Step 6: Voice compression
|
||||
print(f"\n[6/{total_steps}] Voice compression...")
|
||||
# Step 7: Voice compression
|
||||
print(f"\n[7/{total_steps}] Voice compression...")
|
||||
if not args.no_compression:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_dir = Path(tmp)
|
||||
@@ -771,8 +815,8 @@ def main():
|
||||
else:
|
||||
print(" Skipped")
|
||||
|
||||
# Step 7: Phone EQ on caller
|
||||
print(f"\n[7/{total_steps}] Phone EQ on caller...")
|
||||
# Step 8: Phone EQ on caller
|
||||
print(f"\n[8/{total_steps}] Phone EQ on caller...")
|
||||
if not args.no_phone_eq and np.any(stems["caller"] != 0):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
stems["caller"] = phone_eq(stems["caller"], sr, Path(tmp))
|
||||
@@ -780,12 +824,12 @@ def main():
|
||||
else:
|
||||
print(" Skipped" if args.no_phone_eq else " No caller audio")
|
||||
|
||||
# Step 8: Match voice levels
|
||||
print(f"\n[8/{total_steps}] Matching voice levels...")
|
||||
# Step 9: Match voice levels
|
||||
print(f"\n[9/{total_steps}] Matching voice levels...")
|
||||
stems = match_voice_levels(stems)
|
||||
|
||||
# Step 9: Music ducking
|
||||
print(f"\n[9/{total_steps}] Music ducking...")
|
||||
# Step 10: Music ducking
|
||||
print(f"\n[10/{total_steps}] Music ducking...")
|
||||
if not args.no_ducking:
|
||||
dialog = stems["host"] + stems["caller"]
|
||||
if np.any(dialog != 0) and np.any(stems["music"] != 0):
|
||||
@@ -797,28 +841,34 @@ def main():
|
||||
else:
|
||||
print(" Skipped")
|
||||
|
||||
# Step 10: Stereo mix
|
||||
print(f"\n[10/{total_steps}] Mixing...")
|
||||
# Step 11: Stereo mix
|
||||
print(f"\n[11/{total_steps}] Mixing...")
|
||||
stereo = mix_stems(stems, stereo_imaging=not args.no_stereo)
|
||||
imaging = "stereo" if not args.no_stereo else "mono"
|
||||
print(f" Mixed to {imaging}: {len(stereo)} samples ({len(stereo)/sr:.1f}s)")
|
||||
|
||||
# Step 11: Silence trimming
|
||||
print(f"\n[11/{total_steps}] Trimming silence...")
|
||||
# Step 12: Bus compression
|
||||
print(f"\n[12/{total_steps}] Bus compression...")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
stereo = bus_compress(stereo, sr, Path(tmp))
|
||||
print(" Applied")
|
||||
|
||||
# Step 13: Silence trimming
|
||||
print(f"\n[13/{total_steps}] Trimming silence...")
|
||||
if not args.no_trim:
|
||||
stereo = trim_silence(stereo, sr)
|
||||
else:
|
||||
print(" Skipped")
|
||||
|
||||
# Step 12: Fade in/out
|
||||
print(f"\n[12/{total_steps}] Fades...")
|
||||
# Step 14: Fade in/out
|
||||
print(f"\n[14/{total_steps}] Fades...")
|
||||
if not args.no_fade:
|
||||
stereo = apply_fades(stereo, sr, fade_in_s=args.fade_in, fade_out_s=args.fade_out)
|
||||
else:
|
||||
print(" Skipped")
|
||||
|
||||
# Step 13: Normalize + export with metadata and chapters
|
||||
print(f"\n[13/{total_steps}] Loudness normalization + export...")
|
||||
# Step 15: Normalize + export with metadata and chapters
|
||||
print(f"\n[15/{total_steps}] Loudness normalization + export...")
|
||||
|
||||
# Build metadata dict
|
||||
meta = {}
|
||||
|
||||
@@ -130,27 +130,6 @@
|
||||
<span>Audio Router</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diagram-row diagram-row-split">
|
||||
<div class="diagram-box">
|
||||
<div class="diagram-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
||||
</div>
|
||||
<span>Music</span>
|
||||
</div>
|
||||
<div class="diagram-box">
|
||||
<div class="diagram-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>
|
||||
</div>
|
||||
<span>SFX</span>
|
||||
</div>
|
||||
<div class="diagram-box">
|
||||
<div class="diagram-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="7" width="20" height="15" rx="2"/><path d="M16 7V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v3"/></svg>
|
||||
</div>
|
||||
<span>Ads</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diagram-arrow">↓</div>
|
||||
<!-- Row 4: Recording -->
|
||||
<div class="diagram-row">
|
||||
<div class="diagram-box">
|
||||
@@ -255,10 +234,11 @@
|
||||
<div class="hiw-step-content">
|
||||
<h3>A Person Is Born</h3>
|
||||
<p>Every caller starts as a blank slate. The system generates a complete identity: name, age, job, hometown, and personality. Each caller gets a unique speaking style — some ramble, some are blunt, some deflect with humor. They have relationships, vehicles, strong food opinions, nostalgic memories, and reasons for being up this late. They know what they were watching on TV, what errand they ran today, and what song was on the radio before they called.</p>
|
||||
<p>Some callers become regulars. The system tracks returning callers across episodes — they remember past conversations, reference things they talked about before, and their stories evolve over time. You'll hear Carla update you on her divorce, or Carl check in about his gambling recovery. They're not reset between shows.</p>
|
||||
<div class="hiw-detail-grid">
|
||||
<div class="hiw-detail">
|
||||
<span class="hiw-detail-label">Unique Names</span>
|
||||
<span class="hiw-detail-value">48 names</span>
|
||||
<span class="hiw-detail-value">160 names</span>
|
||||
</div>
|
||||
<div class="hiw-detail">
|
||||
<span class="hiw-detail-label">Personality Layers</span>
|
||||
@@ -269,8 +249,8 @@
|
||||
<span class="hiw-detail-value">32</span>
|
||||
</div>
|
||||
<div class="hiw-detail">
|
||||
<span class="hiw-detail-label">Unique Voices</span>
|
||||
<span class="hiw-detail-value">25</span>
|
||||
<span class="hiw-detail-label">Returning Regulars</span>
|
||||
<span class="hiw-detail-value">12+ callers</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -381,23 +361,23 @@
|
||||
<div class="hiw-step-number">8</div>
|
||||
<div class="hiw-step-content">
|
||||
<h3>Post-Production Pipeline</h3>
|
||||
<p>Once the show ends, an automated six-stage pipeline processes the raw stems into a broadcast-ready episode. Dead air and long silences are removed with crossfaded cuts. Voice tracks get dynamic range compression. Music automatically ducks under dialog. All five stems are mixed into stereo and loudness-normalized to broadcast standards. The whole process runs without manual intervention.</p>
|
||||
<p>Once the show ends, a 15-step automated pipeline processes the raw stems into a broadcast-ready episode. Ads and sound effects are hard-limited to prevent clipping. The host mic gets a high-pass filter, de-essing, and breath reduction. Voice tracks are compressed — the host gets aggressive spoken-word compression for consistent levels, callers get telephone EQ to sound like real phone calls. All stems are level-matched, music is ducked under dialog and muted during ads, then everything is mixed to stereo with panning and width. A bus compressor glues the final mix together before silence trimming, fades, and EBU R128 loudness normalization.</p>
|
||||
<div class="hiw-detail-grid">
|
||||
<div class="hiw-detail">
|
||||
<span class="hiw-detail-label">Pipeline Stages</span>
|
||||
<span class="hiw-detail-value">6 steps</span>
|
||||
<span class="hiw-detail-label">Pipeline Steps</span>
|
||||
<span class="hiw-detail-value">15</span>
|
||||
</div>
|
||||
<div class="hiw-detail">
|
||||
<span class="hiw-detail-label">Loudness Target</span>
|
||||
<span class="hiw-detail-value">-16 LUFS</span>
|
||||
</div>
|
||||
<div class="hiw-detail">
|
||||
<span class="hiw-detail-label">Music Ducking</span>
|
||||
<span class="hiw-detail-value">Automatic</span>
|
||||
<span class="hiw-detail-label">Loudness Range</span>
|
||||
<span class="hiw-detail-value">~5.5 LU</span>
|
||||
</div>
|
||||
<div class="hiw-detail">
|
||||
<span class="hiw-detail-label">Output</span>
|
||||
<span class="hiw-detail-value">Broadcast MP3</span>
|
||||
<span class="hiw-detail-value">Stereo MP3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,7 +474,7 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
|
||||
</div>
|
||||
<h3>Broadcast-Grade Audio</h3>
|
||||
<p>Every episode goes through a professional post-production pipeline: five isolated stems are individually processed with dynamic compression, automatic music ducking, and EBU R128 loudness normalization before being mixed to stereo and encoded for distribution.</p>
|
||||
<p>Every episode runs through a 15-step post-production pipeline: stem limiting, high-pass filtering, de-essing, breath reduction, spoken-word compression, telephone EQ, level matching, music ducking with ad muting, stereo imaging, bus compression, and EBU R128 loudness normalization.</p>
|
||||
</div>
|
||||
<div class="hiw-feature">
|
||||
<div class="hiw-feature-icon">
|
||||
|
||||
Reference in New Issue
Block a user