Never miss the daily Presse Citron quiz segment on RFJ (Radio Fréquence Jura).
The app records the live stream every day, transcribes it locally (Whisper), extracts the quiz topic with a local LLM (Ollama), stores everything in SQLite, and serves a small React web player to browse past segments by date.
RFJ Icecast stream ──▶ data/recordings/DD-MM-YYYY.mp3 ──▶ local transcription ──▶ topic
│ │
└──────────────▶ SQLite (data/citron.db) ◀───────────┘
│
FastAPI ──▶ React player (pick a date, listen)
The recorder just does an HTTP GET of RFJ's public live MP3 stream — no browser, no sound card, no ffmpeg — so it runs fine on a headless server.
Everything at once — creates the venv, installs backend + frontend, builds the UI, migrates the old pickle, installs the daily cron job, and starts the server:
./run.shSubcommands: ./run.sh setup | serve | dev | schedule [HH:MM] | record [...].
...or step by step
python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[record]'
python -m scripts.migrate_pickle # import old daily_themes.pkl into SQLite
npm --prefix frontend install && npm --prefix frontend run buildTwo things run.sh does not manage: cron (already a system service) and
Ollama for topic extraction — install it separately
(curl -fsSL https://ollama.com/install.sh | sh && ollama pull llama3.2). The
Whisper model downloads itself on first use.
| Command | What it does |
|---|---|
citron-serve |
FastAPI + the built web player on http://127.0.0.1:8000 |
citron-record |
Record the stream right now (8 min), then transcribe |
citron-record --test --seconds 30 |
30 s throwaway capture to check the stream is reachable (no DB, no transcription) |
citron-record --seconds 120 --no-transcribe |
Record a custom length, skip transcription |
citron-schedule |
Long-running daily job (default 11:48 local) |
npm --prefix frontend run dev |
Vite dev server on :5173, proxying /api to :8000 |
Pick one — both fire citron-record at 11:48 local time every day:
./deploy/install-cron.sh # user crontab, no root, runs even when logged out
./deploy/install-cron.sh 12:05 # or a different HH:MMor, system-wide with systemd (survives reboot, logs to journald):
sudo cp deploy/citron-record.{service,timer} /etc/systemd/system/
sudoedit /etc/systemd/system/citron-record.service # fix User=/paths
sudo systemctl daemon-reload && sudo systemctl enable --now citron-record.timer
systemctl list-timers citron-record.timercitron-schedule is a third option (a foreground schedule loop, honouring
CITRON_SCHEDULE_AT) if you'd rather not use cron or systemd.
Drop an MP3 named DD-MM-YYYY.mp3 into data/recordings/. It shows up in the
player immediately; hit Transcrire in the UI (or POST /api/recordings/DD-MM-YYYY/transcribe)
to fill in the topic, or type the topic directly in the player.
Everything is environment-driven — see citron/config.py. Common ones:
CITRON_WHISPER_MODEL, CITRON_OLLAMA_MODEL, CITRON_RECORDINGS_DIR,
CITRON_PORT, CITRON_SCHEDULE_AT.