Full app audit: 24 fixes across backend, frontend, infra, content, social
Critical fixes:
- Fix hangup-during-respond crash (null caller guard)
- Fix double-click caller race condition
- Stem recorder: non-daemon thread, disk error handling, 30s flush timeout
- Frontend startCall() error handling
High priority:
- Devon: filter tool errors from speech, shorter monitor prompt, 30s interval
- TTS ghost message fix (add to history after TTS, not before)
- Expand banned phrase list (12 new phrases)
- Increase returning callers from 1 to 2 per session
- Platform-tailored social posts with staggered scheduling
- YouTube dynamic tags from episode content
- Social post retry logic (2 attempts, 5s delay)
- Frontend: error handling on all raw fetch calls
Medium:
- stem_recorder null check race (local var capture in audio.py)
- Reactive shape directive expanded
- REACT TO LUKE moved higher in caller prompt
- Devon tenure updated ("few weeks" not "first day")
- D shortcut Escape to unfocus
- Volume slider debounced (150ms)
- Settings modal widened to 550px
- Backup script (daily MariaDB dump + data/ rsync to NAS)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,13 +19,15 @@ class StemRecorder:
|
||||
self._queues: dict[str, deque] = {}
|
||||
self._writer_thread: threading.Thread | None = None
|
||||
self._start_time: float = 0.0
|
||||
self._write_errors: int = 0
|
||||
|
||||
def start(self):
|
||||
self._start_time = time.time()
|
||||
self._running = True
|
||||
self._write_errors = 0
|
||||
for name in STEM_NAMES:
|
||||
self._queues[name] = deque()
|
||||
self._writer_thread = threading.Thread(target=self._writer_loop, daemon=True)
|
||||
self._writer_thread = threading.Thread(target=self._writer_loop, daemon=False)
|
||||
self._writer_thread.start()
|
||||
print(f"[StemRecorder] Recording started -> {self.output_dir}")
|
||||
|
||||
@@ -67,39 +69,57 @@ class StemRecorder:
|
||||
)
|
||||
positions[name] = 0
|
||||
|
||||
while self._running or any(len(q) > 0 for q in self._queues.values()):
|
||||
did_work = False
|
||||
try:
|
||||
while self._running or any(len(q) > 0 for q in self._queues.values()):
|
||||
did_work = False
|
||||
for name in STEM_NAMES:
|
||||
q = self._queues[name]
|
||||
while q:
|
||||
did_work = True
|
||||
msg_type, audio_data, source_sr = q.popleft()
|
||||
resampled = self._resample(audio_data, source_sr)
|
||||
if len(resampled) == 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
if msg_type == "sporadic":
|
||||
elapsed = time.time() - self._start_time
|
||||
expected_pos = int(elapsed * self.sample_rate)
|
||||
if expected_pos > positions[name]:
|
||||
gap = expected_pos - positions[name]
|
||||
files[name].write(np.zeros(gap, dtype=np.float32))
|
||||
positions[name] = expected_pos
|
||||
|
||||
files[name].write(resampled)
|
||||
positions[name] += len(resampled)
|
||||
except Exception as e:
|
||||
self._write_errors += 1
|
||||
if self._write_errors <= 5:
|
||||
print(f"[StemRecorder] Write error on {name}: {e}")
|
||||
elif self._write_errors == 6:
|
||||
print(f"[StemRecorder] Suppressing further write errors")
|
||||
|
||||
if not did_work:
|
||||
time.sleep(0.02)
|
||||
|
||||
# Pad all stems to same length
|
||||
max_pos = max(positions.values()) if positions else 0
|
||||
for name in STEM_NAMES:
|
||||
q = self._queues[name]
|
||||
while q:
|
||||
did_work = True
|
||||
msg_type, audio_data, source_sr = q.popleft()
|
||||
resampled = self._resample(audio_data, source_sr)
|
||||
if len(resampled) == 0:
|
||||
continue
|
||||
try:
|
||||
if positions[name] < max_pos:
|
||||
files[name].write(np.zeros(max_pos - positions[name], dtype=np.float32))
|
||||
except Exception as e:
|
||||
print(f"[StemRecorder] Final pad error on {name}: {e}")
|
||||
finally:
|
||||
for name, f in files.items():
|
||||
try:
|
||||
f.close()
|
||||
except Exception as e:
|
||||
print(f"[StemRecorder] Error closing {name}.wav: {e}")
|
||||
|
||||
if msg_type == "sporadic":
|
||||
elapsed = time.time() - self._start_time
|
||||
expected_pos = int(elapsed * self.sample_rate)
|
||||
if expected_pos > positions[name]:
|
||||
gap = expected_pos - positions[name]
|
||||
files[name].write(np.zeros(gap, dtype=np.float32))
|
||||
positions[name] = expected_pos
|
||||
|
||||
files[name].write(resampled)
|
||||
positions[name] += len(resampled)
|
||||
|
||||
if not did_work:
|
||||
time.sleep(0.02)
|
||||
|
||||
# Pad all stems to same length
|
||||
max_pos = max(positions.values()) if positions else 0
|
||||
for name in STEM_NAMES:
|
||||
if positions[name] < max_pos:
|
||||
files[name].write(np.zeros(max_pos - positions[name], dtype=np.float32))
|
||||
files[name].close()
|
||||
|
||||
print(f"[StemRecorder] Writer done. {max_pos} samples ({max_pos / self.sample_rate:.1f}s)")
|
||||
total_errors = self._write_errors
|
||||
err_msg = f", {total_errors} write errors" if total_errors else ""
|
||||
print(f"[StemRecorder] Writer done. {max_pos} samples ({max_pos / self.sample_rate:.1f}s{err_msg})")
|
||||
|
||||
def stop(self) -> dict[str, str]:
|
||||
if not self._running:
|
||||
@@ -107,7 +127,9 @@ class StemRecorder:
|
||||
|
||||
self._running = False
|
||||
if self._writer_thread:
|
||||
self._writer_thread.join(timeout=10.0)
|
||||
self._writer_thread.join(timeout=30.0)
|
||||
if self._writer_thread.is_alive():
|
||||
print("[StemRecorder] Warning: writer thread still running after 30s")
|
||||
self._writer_thread = None
|
||||
|
||||
paths = {}
|
||||
|
||||
Reference in New Issue
Block a user