Ep13 publish, MLX whisper, voicemail system, hero redesign, massive topic expansion
- Switch whisper transcription from faster-whisper (CPU) to lightning-whisper-mlx (GPU) - Fix word_timestamps hanging, use ffprobe for accurate duration - Add Cloudflare Pages Worker for SignalWire voicemail fallback when server offline - Add voicemail sync on startup, delete tracking, save feature - Add /feed RSS proxy to _worker.js (was broken by worker taking over routing) - Redesign website hero section: ghost buttons, compact phone, plain text links - Rewrite caller prompts for faster point-getting and host-following - Expand TOPIC_CALLIN from ~250 to 547 entries across 34 categories - Add new categories: biology, psychology, engineering, math, geology, animals, work, money, books, movies, relationships, health, language, true crime, drunk/high/unhinged callers - Remove bad Inworld voices (Pixie, Dominus), reduce repeat caller frequency - Add audio monitor device routing, uvicorn --reload-dir fix - Publish episode 13 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -349,6 +349,19 @@ section h2 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.music-section select optgroup {
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.music-section select option {
|
||||
color: var(--text);
|
||||
font-weight: normal;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.music-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -725,3 +738,26 @@ section h2 {
|
||||
.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; }
|
||||
|
||||
/* Voicemail */
|
||||
.voicemail-section { margin: 1rem 0; }
|
||||
.voicemail-list { border: 1px solid rgba(232, 121, 29, 0.15); border-radius: var(--radius-sm); padding: 0.5rem; max-height: 200px; overflow-y: auto; }
|
||||
.voicemail-badge { background: var(--accent-red); color: white; font-size: 0.7rem; font-weight: bold; padding: 0.1rem 0.45rem; border-radius: 10px; margin-left: 0.4rem; vertical-align: middle; }
|
||||
.voicemail-badge.hidden { display: none; }
|
||||
.vm-item { display: flex; align-items: center; justify-content: space-between; padding: 0.4rem 0.5rem; border-bottom: 1px solid rgba(232, 121, 29, 0.08); }
|
||||
.vm-item:last-child { border-bottom: none; }
|
||||
.vm-item.vm-unlistened { background: rgba(232, 121, 29, 0.06); }
|
||||
.vm-info { display: flex; gap: 0.6rem; align-items: center; flex: 1; min-width: 0; }
|
||||
.vm-phone { font-family: monospace; color: var(--accent); font-size: 0.85rem; }
|
||||
.vm-time { color: var(--text-muted); font-size: 0.8rem; }
|
||||
.vm-dur { color: var(--text-muted); font-size: 0.8rem; }
|
||||
.vm-actions { display: flex; gap: 0.3rem; flex-shrink: 0; }
|
||||
.vm-btn { border: none; padding: 0.2rem 0.5rem; border-radius: var(--radius-sm); cursor: pointer; font-size: 0.75rem; transition: background 0.2s; }
|
||||
.vm-btn.listen { background: var(--accent); color: white; }
|
||||
.vm-btn.listen:hover { background: var(--accent-hover); }
|
||||
.vm-btn.on-air { background: var(--accent-green); color: white; }
|
||||
.vm-btn.on-air:hover { background: #6a9a4c; }
|
||||
.vm-btn.save { background: #3a7bd5; color: white; }
|
||||
.vm-btn.save:hover { background: #2a5db0; }
|
||||
.vm-btn.delete { background: var(--accent-red); color: white; }
|
||||
.vm-btn.delete:hover { background: #e03030; }
|
||||
|
||||
@@ -65,6 +65,14 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Voicemail -->
|
||||
<section class="voicemail-section">
|
||||
<h2>Voicemail <span id="voicemail-badge" class="voicemail-badge hidden">0</span></h2>
|
||||
<div id="voicemail-list" class="voicemail-list">
|
||||
<div class="queue-empty">No voicemails</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Chat -->
|
||||
<section class="chat-section">
|
||||
<div id="chat" class="chat-log"></div>
|
||||
@@ -224,6 +232,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/app.js?v=15"></script>
|
||||
<script src="/js/app.js?v=17"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,6 +16,15 @@ let tracks = [];
|
||||
let sounds = [];
|
||||
|
||||
|
||||
// --- Helpers ---
|
||||
function _isTyping() {
|
||||
const el = document.activeElement;
|
||||
if (!el) return false;
|
||||
const tag = el.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
||||
}
|
||||
|
||||
|
||||
// --- Safe JSON parsing ---
|
||||
async function safeFetch(url, options = {}, timeoutMs = 30000) {
|
||||
const controller = new AbortController();
|
||||
@@ -51,6 +60,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadSounds();
|
||||
await loadSettings();
|
||||
initEventListeners();
|
||||
loadVoicemails();
|
||||
setInterval(loadVoicemails, 30000);
|
||||
log('Ready. Configure audio devices in Settings, then click a caller to start.');
|
||||
console.log('AI Radio Show ready');
|
||||
} catch (err) {
|
||||
@@ -137,6 +148,20 @@ function initEventListeners() {
|
||||
talkBtn.addEventListener('touchend', e => { e.preventDefault(); stopRecording(); });
|
||||
}
|
||||
|
||||
// Spacebar push-to-talk — blur buttons so Space doesn't also trigger button click
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.code !== 'Space' || e.repeat || _isTyping()) return;
|
||||
e.preventDefault();
|
||||
// Blur any focused button so browser doesn't fire its click
|
||||
if (document.activeElement?.tagName === 'BUTTON') document.activeElement.blur();
|
||||
startRecording();
|
||||
});
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.code !== 'Space' || _isTyping()) return;
|
||||
e.preventDefault();
|
||||
stopRecording();
|
||||
});
|
||||
|
||||
// Type button
|
||||
document.getElementById('type-btn')?.addEventListener('click', () => {
|
||||
document.getElementById('type-modal')?.classList.remove('hidden');
|
||||
@@ -630,11 +655,31 @@ async function loadMusic() {
|
||||
const previousValue = select.value;
|
||||
select.innerHTML = '';
|
||||
|
||||
tracks.forEach((track, i) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = track.file;
|
||||
option.textContent = track.name;
|
||||
select.appendChild(option);
|
||||
// Group tracks by genre
|
||||
const genres = {};
|
||||
tracks.forEach(track => {
|
||||
const genre = track.genre || 'Other';
|
||||
if (!genres[genre]) genres[genre] = [];
|
||||
genres[genre].push(track);
|
||||
});
|
||||
|
||||
// Sort genre names, but put "Other" last
|
||||
const genreOrder = Object.keys(genres).sort((a, b) => {
|
||||
if (a === 'Other') return 1;
|
||||
if (b === 'Other') return -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
genreOrder.forEach(genre => {
|
||||
const group = document.createElement('optgroup');
|
||||
group.label = genre;
|
||||
genres[genre].forEach(track => {
|
||||
const option = document.createElement('option');
|
||||
option.value = track.file;
|
||||
option.textContent = track.name;
|
||||
group.appendChild(option);
|
||||
});
|
||||
select.appendChild(group);
|
||||
});
|
||||
|
||||
// Restore previous selection if it still exists
|
||||
@@ -1225,3 +1270,93 @@ async function stopServer() {
|
||||
log('Failed to stop server: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Voicemail ---
|
||||
let _currentVmAudio = null;
|
||||
|
||||
async function loadVoicemails() {
|
||||
try {
|
||||
const res = await fetch('/api/voicemails');
|
||||
const data = await res.json();
|
||||
renderVoicemails(data);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
function renderVoicemails(voicemails) {
|
||||
const list = document.getElementById('voicemail-list');
|
||||
const badge = document.getElementById('voicemail-badge');
|
||||
if (!list) return;
|
||||
|
||||
const unlistened = voicemails.filter(v => !v.listened).length;
|
||||
if (badge) {
|
||||
badge.textContent = unlistened;
|
||||
badge.classList.toggle('hidden', unlistened === 0);
|
||||
}
|
||||
|
||||
if (voicemails.length === 0) {
|
||||
list.innerHTML = '<div class="queue-empty">No voicemails</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = voicemails.map(v => {
|
||||
const date = new Date(v.timestamp * 1000);
|
||||
const timeStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
const mins = Math.floor(v.duration / 60);
|
||||
const secs = v.duration % 60;
|
||||
const durStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
||||
const unlistenedCls = v.listened ? '' : ' vm-unlistened';
|
||||
return `<div class="vm-item${unlistenedCls}" data-id="${v.id}">
|
||||
<div class="vm-info">
|
||||
<span class="vm-phone">${v.phone}</span>
|
||||
<span class="vm-time">${timeStr}</span>
|
||||
<span class="vm-dur">${durStr}</span>
|
||||
</div>
|
||||
<div class="vm-actions">
|
||||
<button class="vm-btn listen" onclick="listenVoicemail('${v.id}')">Listen</button>
|
||||
<button class="vm-btn on-air" onclick="playVoicemailOnAir('${v.id}')">On Air</button>
|
||||
<button class="vm-btn save" onclick="saveVoicemail('${v.id}')">Save</button>
|
||||
<button class="vm-btn delete" onclick="deleteVoicemail('${v.id}')">Del</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function listenVoicemail(id) {
|
||||
if (_currentVmAudio) {
|
||||
_currentVmAudio.pause();
|
||||
_currentVmAudio = null;
|
||||
}
|
||||
_currentVmAudio = new Audio(`/api/voicemail/${id}/audio`);
|
||||
_currentVmAudio.play();
|
||||
fetch(`/api/voicemail/${id}/mark-listened`, { method: 'POST' }).then(() => loadVoicemails());
|
||||
}
|
||||
|
||||
async function playVoicemailOnAir(id) {
|
||||
try {
|
||||
await safeFetch(`/api/voicemail/${id}/play-on-air`, { method: 'POST' });
|
||||
log('Playing voicemail on air');
|
||||
loadVoicemails();
|
||||
} catch (err) {
|
||||
log('Failed to play voicemail: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveVoicemail(id) {
|
||||
try {
|
||||
await safeFetch(`/api/voicemail/${id}/save`, { method: 'POST' });
|
||||
log('Voicemail saved to archive');
|
||||
} catch (err) {
|
||||
log('Failed to save voicemail: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVoicemail(id) {
|
||||
if (!confirm('Delete this voicemail?')) return;
|
||||
try {
|
||||
await safeFetch(`/api/voicemail/${id}`, { method: 'DELETE' });
|
||||
loadVoicemails();
|
||||
} catch (err) {
|
||||
log('Failed to delete voicemail: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user