ratandeep commited on
Commit
b9183e9
·
verified ·
1 Parent(s): fa32d25

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. arc.py +9 -0
  2. proxy.py +67 -2
  3. radio.html +78 -5
  4. server.py +9 -0
arc.py CHANGED
@@ -179,6 +179,15 @@ def stage_default_mood(stage: str) -> str:
179
  return s["default_mood"]
180
 
181
 
 
 
 
 
 
 
 
 
 
182
  # ---------------------------------------------------------------------------
183
  # System-prompt assembly
184
  # ---------------------------------------------------------------------------
 
179
  return s["default_mood"]
180
 
181
 
182
+ def sample_line(stage: str, seed: int = 0) -> str:
183
+ """A clean, stage-appropriate canned line (fallback when brain text is junk)."""
184
+ s = _STAGE_BY_KEY.get(stage) or _STAGE_BY_KEY["oblivious"]
185
+ lines = s.get("sample_lines") or []
186
+ if not lines:
187
+ return "Stay with me, friend. The night's long and the dial's warm."
188
+ return lines[int(seed) % len(lines)]
189
+
190
+
191
  # ---------------------------------------------------------------------------
192
  # System-prompt assembly
193
  # ---------------------------------------------------------------------------
proxy.py CHANGED
@@ -15,6 +15,7 @@ import datetime
15
  import io
16
  import json
17
  import os
 
18
  import struct
19
  import threading
20
  import wave
@@ -189,6 +190,70 @@ def call_speak(text: str, voice: str = arc.VOICE) -> Dict[str, Any]:
189
  return resp.json()
190
 
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  # ---------------------------------------------------------------------------
193
  # High-level turns (what server.py routes call)
194
  # ---------------------------------------------------------------------------
@@ -202,7 +267,7 @@ def broadcast_turn(
202
  """
203
  system = arc.build_system_prompt(stage, "broadcast", topic=topic)
204
  brain = call_brain(system, [{"role": "user", "content": "Go on air."}])
205
- text = brain.get("text", "")
206
  mood = brain.get("mood", arc.stage_default_mood(stage))
207
  arc_cue = brain.get("arc_cue", "none")
208
  speak = call_speak(text)
@@ -241,7 +306,7 @@ def call_turn(stage: str, meter: int, audio_b64: str) -> Dict[str, Any]:
241
  brain = call_brain(
242
  system, [{"role": "user", "content": caller_text or "(crackle)"}]
243
  )
244
- text = brain.get("text", "")
245
  mood = brain.get("mood", arc.stage_default_mood(stage))
246
  arc_cue = brain.get("arc_cue", "none")
247
 
 
15
  import io
16
  import json
17
  import os
18
+ import re
19
  import struct
20
  import threading
21
  import wave
 
190
  return resp.json()
191
 
192
 
193
+ # ---------------------------------------------------------------------------
194
+ # DJ-text sanitizer: a small 1B model sometimes leaks the JSON labels into the
195
+ # spoken text ("...mood: warm") or emits placeholders ("<sentence 1>"). We never
196
+ # want the DJ to SAY those, so we scrub the text before TTS and, if what's left
197
+ # is degenerate, fall back to a clean stage-appropriate canned line.
198
+ # ---------------------------------------------------------------------------
199
+ _LABEL_RE = re.compile(
200
+ r"[\(\[]?\s*\b(?:mood|arc[_ ]?cue)\b\s*[:=]\s*[a-z_]+\s*[\)\]]?", re.IGNORECASE
201
+ )
202
+ _PLACEHOLDER_RE = re.compile(r"<[^>]{0,40}>|\bsentence\s*\d+\b", re.IGNORECASE)
203
+
204
+
205
+ def _clean_dj_text(text: Optional[str]) -> Optional[str]:
206
+ """Strip label leakage / markdown; return cleaned spoken text or None if junk."""
207
+ t = text or ""
208
+ t = _LABEL_RE.sub("", t) # drop "mood: warm" / "arc_cue: none"
209
+ t = re.sub(r"[*#`]+", "", t) # drop markdown emphasis/headings
210
+ t = t.strip()
211
+ if len(t) >= 2 and t[0] in "\"'" and t[-1] in "\"'":
212
+ t = t[1:-1].strip() # unwrap a fully-quoted line
213
+ t = re.sub(r"\s+", " ", t).strip() # it's spoken: collapse whitespace
214
+ # Degenerate? empty / placeholder / angle-bracket template / too few letters.
215
+ if not t or _PLACEHOLDER_RE.search(t) or ("<" in t and ">" in t):
216
+ return None
217
+ if len(re.sub(r"[^a-zA-Z]", "", t)) < 6:
218
+ return None
219
+ return t
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # Pre-cached "stall" lines: instant filler audio played the moment a caller stops
224
+ # talking, while the real reply generates underneath -- so the call never has
225
+ # dead air. Synthesized once (per container) via Modal /speak, then cached.
226
+ # ---------------------------------------------------------------------------
227
+ _STALL_LINES = [
228
+ "Mm. Good question, friend. Let me sit with that a second.",
229
+ "That's a thing to ask at this hour. Hold on now, let me find the words.",
230
+ "Hm. Give me a breath, caller. The night makes me slow.",
231
+ "Let me think on that one. Stay right there with me.",
232
+ ]
233
+ _stall_cache: Optional[List[str]] = None
234
+ _stall_lock = threading.Lock()
235
+
236
+
237
+ def get_stalls() -> List[str]:
238
+ """Return a list of base64 WAV stall clips (cached after first synthesis)."""
239
+ global _stall_cache
240
+ if _stall_cache is not None:
241
+ return _stall_cache
242
+ with _stall_lock:
243
+ if _stall_cache is not None:
244
+ return _stall_cache
245
+ clips: List[str] = []
246
+ for line in _STALL_LINES:
247
+ try:
248
+ sp = call_speak(line)
249
+ if sp.get("audio_b64"):
250
+ clips.append(sp["audio_b64"])
251
+ except Exception:
252
+ pass
253
+ _stall_cache = clips
254
+ return clips
255
+
256
+
257
  # ---------------------------------------------------------------------------
258
  # High-level turns (what server.py routes call)
259
  # ---------------------------------------------------------------------------
 
267
  """
268
  system = arc.build_system_prompt(stage, "broadcast", topic=topic)
269
  brain = call_brain(system, [{"role": "user", "content": "Go on air."}])
270
+ text = _clean_dj_text(brain.get("text", "")) or arc.sample_line(stage, meter)
271
  mood = brain.get("mood", arc.stage_default_mood(stage))
272
  arc_cue = brain.get("arc_cue", "none")
273
  speak = call_speak(text)
 
306
  brain = call_brain(
307
  system, [{"role": "user", "content": caller_text or "(crackle)"}]
308
  )
309
+ text = _clean_dj_text(brain.get("text", "")) or arc.sample_line(stage, meter)
310
  mood = brain.get("mood", arc.stage_default_mood(stage))
311
  arc_cue = brain.get("arc_cue", "none")
312
 
radio.html CHANGED
@@ -751,8 +751,14 @@ function initAudio(){
751
  Sound.crackleGain.connect(Sound.analyser);
752
  Sound.crackleSrc.start(0);
753
 
 
 
 
 
 
754
  // ----- hiss bed: noise -> lowpass -> hissGain -> analyser
755
  const buf = makeNoiseBuffer(ctx);
 
756
  Sound.hissSrc = ctx.createBufferSource();
757
  Sound.hissSrc.buffer = buf;
758
  Sound.hissSrc.loop = true;
@@ -824,6 +830,11 @@ function updateHissForTuning(){
824
  let pad = (State.powered && locked) ? 0.05 : 0.0;
825
  if (Sound._voicing) pad *= 0.55; // sit back under the voice
826
  rampGain(Sound.padGain, pad, 0.8);
 
 
 
 
 
827
  }
828
 
829
  function rampGain(node, target, t){
@@ -834,6 +845,58 @@ function rampGain(node, target, t){
834
  node.gain.linearRampToValueAtTime(target, now + t);
835
  }
836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  // short burst of noise (glitch beat)
838
  function noiseBurst(dur, level){
839
  if (!Sound.ctx) return;
@@ -1143,7 +1206,7 @@ function handleArcCue(cue){
1143
  const now = Sound.ctx.currentTime;
1144
  Sound.bandpass.frequency.cancelScheduledValues(now);
1145
  Sound.bandpass.frequency.setValueAtTime(Sound.bandpass.frequency.value, now);
1146
- Sound.bandpass.frequency.linearRampToValueAtTime(1100, now+0.8);
1147
  Sound.bandpass.Q.linearRampToValueAtTime(0.4, now+0.8);
1148
  }
1149
  } else {
@@ -1153,7 +1216,7 @@ function handleArcCue(cue){
1153
  const now = Sound.ctx.currentTime;
1154
  Sound.bandpass.frequency.cancelScheduledValues(now);
1155
  Sound.bandpass.frequency.setValueAtTime(Sound.bandpass.frequency.value, now);
1156
- Sound.bandpass.frequency.linearRampToValueAtTime(1700, now+0.5);
1157
  Sound.bandpass.Q.linearRampToValueAtTime(0.7, now+0.5);
1158
  }
1159
  }
@@ -1237,11 +1300,18 @@ function stopCall(e){
1237
 
1238
  async function onCallStop(){
1239
  const blob = new Blob(Sound.recChunks, { type: (Sound.recorder && Sound.recorder.mimeType) || "audio/webm" });
1240
- showCaption("… the DJ is responding …", "status");
1241
  State.busy = true;
1242
  let audio_b64 = "";
1243
  try{ audio_b64 = await blobToB64(blob); }catch(e){}
1244
- const data = await api("/api/call", { stage:State.stage, meter:Math.round(State.meter), audio_b64 });
 
 
 
 
 
 
 
 
1245
  State.busy = false;
1246
  if (data && typeof data.meter_delta === "number") addMeter(data.meter_delta);
1247
  await playSegment(data, data && data.caller_text);
@@ -1280,6 +1350,8 @@ async function powerOn(){
1280
  els.onairLabel.textContent = "RECEIVING";
1281
  updateHissForTuning();
1282
  rampGain(Sound.crackleGain, 0.05, 0.6); // bring up the vinyl/room bed
 
 
1283
  State.lastVoiceEnd = performance.now();
1284
  showCaption("… tuning in …", "status");
1285
  setTimeout(()=>broadcast(), 400);
@@ -1294,7 +1366,8 @@ function powerOff(){
1294
  els.onairLabel.textContent = "STANDBY";
1295
  setBroadcasting(false);
1296
  try{ if (Sound.voiceEl) Sound.voiceEl.pause(); }catch(e){}
1297
- if (Sound.ctx){ rampGain(Sound.hissGain, 0, 0.3); rampGain(Sound.padGain, 0, 0.3); rampGain(Sound.crackleGain, 0, 0.3); }
 
1298
  showCaption("Turn the power knob to tune in…");
1299
  }
1300
 
 
751
  Sound.crackleGain.connect(Sound.analyser);
752
  Sound.crackleSrc.start(0);
753
 
754
+ // ----- lo-fi percussion bus (kick + brushed hat), gain-gated like the pad ---
755
+ Sound.drumGain = ctx.createGain();
756
+ Sound.drumGain.gain.value = 0.0;
757
+ Sound.drumGain.connect(Sound.analyser);
758
+
759
  // ----- hiss bed: noise -> lowpass -> hissGain -> analyser
760
  const buf = makeNoiseBuffer(ctx);
761
+ Sound.noiseBuf = buf; // reused by the brushed-hat percussion
762
  Sound.hissSrc = ctx.createBufferSource();
763
  Sound.hissSrc.buffer = buf;
764
  Sound.hissSrc.loop = true;
 
830
  let pad = (State.powered && locked) ? 0.05 : 0.0;
831
  if (Sound._voicing) pad *= 0.55; // sit back under the voice
832
  rampGain(Sound.padGain, pad, 0.8);
833
+
834
+ // --- lo-fi percussion: rides with the pad, ducks well under the voice ---
835
+ let drum = (State.powered && locked) ? 0.35 : 0.0;
836
+ if (Sound._voicing) drum *= 0.4;
837
+ if (Sound.drumGain) rampGain(Sound.drumGain, drum, 0.6);
838
  }
839
 
840
  function rampGain(node, target, t){
 
845
  node.gain.linearRampToValueAtTime(target, now + t);
846
  }
847
 
848
+ // ---- lo-fi percussion: soft kick + brushed hat on a slow, sparse pattern ----
849
+ function playKick(t){
850
+ if (!Sound.ctx || !Sound.drumGain) return;
851
+ const o = Sound.ctx.createOscillator(), g = Sound.ctx.createGain();
852
+ o.type = "sine";
853
+ o.frequency.setValueAtTime(115, t);
854
+ o.frequency.exponentialRampToValueAtTime(45, t + 0.12);
855
+ g.gain.setValueAtTime(0.0001, t);
856
+ g.gain.exponentialRampToValueAtTime(0.9, t + 0.005);
857
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 0.30);
858
+ o.connect(g); g.connect(Sound.drumGain);
859
+ o.start(t); o.stop(t + 0.33);
860
+ }
861
+ function playHat(t){
862
+ if (!Sound.ctx || !Sound.drumGain || !Sound.noiseBuf) return;
863
+ const s = Sound.ctx.createBufferSource(), g = Sound.ctx.createGain(), hp = Sound.ctx.createBiquadFilter();
864
+ s.buffer = Sound.noiseBuf;
865
+ hp.type = "highpass"; hp.frequency.value = 7500;
866
+ g.gain.setValueAtTime(0.0001, t);
867
+ g.gain.exponentialRampToValueAtTime(0.16, t + 0.003);
868
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 0.06);
869
+ s.connect(hp); hp.connect(g); g.connect(Sound.drumGain);
870
+ s.start(t); s.stop(t + 0.08);
871
+ }
872
+ function startDrums(){
873
+ if (Sound.drumTimer || !Sound.ctx) return;
874
+ const bpm = 76, stepDur = (60 / bpm) / 2; // eighth notes
875
+ const KICK = [1,0,0,0,1,0,0,0], HAT = [0,0,1,0,0,0,1,0];
876
+ let step = 0, nextTime = Sound.ctx.currentTime + 0.12;
877
+ function tick(){
878
+ while (nextTime < Sound.ctx.currentTime + 0.25){
879
+ if (KICK[step % 8]) playKick(nextTime);
880
+ if (HAT[step % 8]) playHat(nextTime);
881
+ nextTime += stepDur; step++;
882
+ }
883
+ Sound.drumTimer = setTimeout(tick, 60);
884
+ }
885
+ tick();
886
+ }
887
+ function stopDrums(){
888
+ if (Sound.drumTimer){ clearTimeout(Sound.drumTimer); Sound.drumTimer = null; }
889
+ }
890
+
891
+ // ---- stall lines: prefetch instant filler clips to mask call latency ----
892
+ async function fetchStalls(){
893
+ try{
894
+ const r = await fetch("/api/stalls");
895
+ if (r.ok){ const j = await r.json(); if (Array.isArray(j.stalls)) return j.stalls; }
896
+ }catch(_){}
897
+ return [];
898
+ }
899
+
900
  // short burst of noise (glitch beat)
901
  function noiseBurst(dur, level){
902
  if (!Sound.ctx) return;
 
1206
  const now = Sound.ctx.currentTime;
1207
  Sound.bandpass.frequency.cancelScheduledValues(now);
1208
  Sound.bandpass.frequency.setValueAtTime(Sound.bandpass.frequency.value, now);
1209
+ Sound.bandpass.frequency.linearRampToValueAtTime(2000, now+0.8);
1210
  Sound.bandpass.Q.linearRampToValueAtTime(0.4, now+0.8);
1211
  }
1212
  } else {
 
1216
  const now = Sound.ctx.currentTime;
1217
  Sound.bandpass.frequency.cancelScheduledValues(now);
1218
  Sound.bandpass.frequency.setValueAtTime(Sound.bandpass.frequency.value, now);
1219
+ Sound.bandpass.frequency.linearRampToValueAtTime(3400, now+0.5);
1220
  Sound.bandpass.Q.linearRampToValueAtTime(0.7, now+0.5);
1221
  }
1222
  }
 
1300
 
1301
  async function onCallStop(){
1302
  const blob = new Blob(Sound.recChunks, { type: (Sound.recorder && Sound.recorder.mimeType) || "audio/webm" });
 
1303
  State.busy = true;
1304
  let audio_b64 = "";
1305
  try{ audio_b64 = await blobToB64(blob); }catch(e){}
1306
+ // Fire the real request FIRST, then mask its latency with an instant stall
1307
+ // line that plays while the reply generates underneath -- no dead air.
1308
+ const respP = api("/api/call", { stage:State.stage, meter:Math.round(State.meter), audio_b64 });
1309
+ showCaption("… the DJ is responding …", "status");
1310
+ if (Sound.stalls && Sound.stalls.length){
1311
+ const stall = Sound.stalls[Math.floor(Math.random() * Sound.stalls.length)];
1312
+ try{ await playVoiceB64(stall); }catch(_){}
1313
+ }
1314
+ const data = await respP;
1315
  State.busy = false;
1316
  if (data && typeof data.meter_delta === "number") addMeter(data.meter_delta);
1317
  await playSegment(data, data && data.caller_text);
 
1350
  els.onairLabel.textContent = "RECEIVING";
1351
  updateHissForTuning();
1352
  rampGain(Sound.crackleGain, 0.05, 0.6); // bring up the vinyl/room bed
1353
+ startDrums();
1354
+ if (!Sound.stalls) fetchStalls().then(s=>{ Sound.stalls = s; });
1355
  State.lastVoiceEnd = performance.now();
1356
  showCaption("… tuning in …", "status");
1357
  setTimeout(()=>broadcast(), 400);
 
1366
  els.onairLabel.textContent = "STANDBY";
1367
  setBroadcasting(false);
1368
  try{ if (Sound.voiceEl) Sound.voiceEl.pause(); }catch(e){}
1369
+ if (Sound.ctx){ rampGain(Sound.hissGain, 0, 0.3); rampGain(Sound.padGain, 0, 0.3); rampGain(Sound.crackleGain, 0, 0.3); rampGain(Sound.drumGain, 0, 0.3); }
1370
+ stopDrums();
1371
  showCaption("Turn the power knob to tune in…");
1372
  }
1373
 
server.py CHANGED
@@ -131,4 +131,13 @@ def create_api() -> FastAPI:
131
  def api_seek(req: SeekReq):
132
  return {"stage": arc.meter_to_stage(req.meter)}
133
 
 
 
 
 
 
 
 
 
 
134
  return api
 
131
  def api_seek(req: SeekReq):
132
  return {"stage": arc.meter_to_stage(req.meter)}
133
 
134
+ @api.get("/api/stalls")
135
+ def api_stalls():
136
+ # Pre-synthesized filler clips the radio plays instantly while a real
137
+ # call reply generates (so the call never has dead air).
138
+ try:
139
+ return {"stalls": proxy.get_stalls()}
140
+ except Exception:
141
+ return {"stalls": []}
142
+
143
  return api