Sintetizzatore vocale per DLQ
  • Python 51.8%
  • Shell 48.2%
Find a file
Stefano Moretti 9c71913673 feat: /bulk asincrono con job in background e /bulk-status
Il /bulk sincrono bloccava troppo a lungo per code numerose (~9s/file su Pi);
ora risponde subito 202 con un job_id e la generazione prosegue in background,
consultabile via /bulk-status.
2026-07-14 11:17:26 +02:00
tests Revert last updates 2026-07-02 14:38:17 +02:00
.gitignore feat: endpoint GET /bulk per generazione audio in bulk 2026-07-01 11:00:56 +02:00
abaddon.py feat: /bulk asincrono con job in background e /bulk-status 2026-07-14 11:17:26 +02:00
abaddon.service altro fix 2026-05-18 18:24:19 +02:00
abaddon_logo.svg initial commit — Abaddon voice daemon 2026-05-18 15:22:10 +02:00
audio_settings.sh aggiunte impostazioni audio 2026-06-26 11:22:50 +02:00
config.py feat: /bulk asincrono con job in background e /bulk-status 2026-07-14 11:17:26 +02:00
install_abaddon.sh update installer + README.md 2026-06-15 14:40:04 +02:00
README.md feat: /bulk asincrono con job in background e /bulk-status 2026-07-14 11:17:26 +02:00
uninstall_abaddon.sh aggiunta uninstaller 2026-06-26 12:42:18 +02:00

Abaddon — Voice Daemon

Abaddon logo

Servizio TTS offline per Raspberry Pi / totem x86. Sintetizza testo in MP3, lo salva su disco (cache) e lo espone via HTTP.

Stack: Piper TTS · ffmpeg · mpg123 · Python stdlib (no dipendenze esterne)


Requisiti

  • Linux x86_64 o Raspberry Pi 3/4 (aarch64/armv7l)
  • Python 3.9+
  • Utente di sistema kipanga (usato dal service systemd e dai permessi)
  • Connessione internet solo durante il setup — poi tutto offline

Installazione

git clone https://git.okone.it/s.moretti/abaddon.git && cd abaddon
sudo bash install_abaddon.sh

Lo script install_abaddon.sh richiede privilegi di root (apt, chown, loginctl) e:

  • installa git e curl se mancanti;
  • clona/aggiorna la repository in TARGET_DIR (default /var/www/kipanga/system/abaddon, sovrascrivibile via variabile d'ambiente TARGET_DIR);
  • chiede la cartella di salvataggio MP3 e la scrive in config.py;
  • assegna i permessi all'utente kipanga;
  • installa ffmpeg e mpg123 tramite apt;
  • scarica il binario Piper TTS 2023.11.14-2 in ./piper/;
  • scarica la voce italiana it_IT-paola-medium in ./piper/;
  • scarica la voce inglese en_US-hfc_female-medium in ./piper/;
  • genera e abilita il systemd user service per kipanga (con il Python rilevato automaticamente) e attiva il linger per l'avvio headless.

Durante il setup viene chiesta la cartella di salvataggio MP3:

==> Cartella di salvataggio file MP3 (cache)
   Percorso [/var/www/kipanga/system/abaddon/audios]:

Premi Invio per accettare il default o inserisci un percorso custom. La cartella viene creata automaticamente e il valore viene scritto in config.py.

Configurazione

Modifica config.py per cambiare i parametri dopo l'installazione:

Parametro Default Note
PORT 9966 porta <1024 richiede sudo
CACHE_DIR /var/www/kipanga/system/abaddon/audios impostato da install_abaddon.sh
PIPER_BIN ./piper/piper installato da install_abaddon.sh
MODEL_PATH ./piper/it_IT-paola-medium.onnx voce italiana (default)
MODEL_PATH_EN ./piper/en_US-hfc_female-medium.onnx voce inglese (lang=en)
PIPER_HZ 22050 sample rate dei modelli Piper
PAUSE_MS 150 durata pausa |P| in millisecondi
LENGTH_SCALE 1.25 velocità voce: 1.0 = normale, >1.0 = lento
MP3_HZ 48000 sample rate file MP3 di output
MP3_BITRATE 320k bitrate file MP3 di output

Avvio

Manuale

python3 abaddon.py
# [TTS] Pronto  →  http://localhost:9966
# [TTS] Cache   →  /var/www/kipanga/system/abaddon/audios  (0 file)

Il server è multi-thread (ThreadingHTTPServer) e ascolta su 0.0.0.0:PORT.

Automatico all'avvio (systemd)

Gestito da install_abaddon.sh, che rileva il Python installato e genera il service (abaddon.service, con i placeholder %PYTHON% e %SCRIPT_DIR% risolti) nel profilo systemd user di kipanga. Per gestione manuale:

sudo -u kipanga XDG_RUNTIME_DIR=/run/user/$(id -u kipanga) systemctl --user status abaddon
sudo -u kipanga XDG_RUNTIME_DIR=/run/user/$(id -u kipanga) systemctl --user restart abaddon
sudo -u kipanga XDG_RUNTIME_DIR=/run/user/$(id -u kipanga) journalctl --user -u abaddon -f

install_abaddon.sh abilita automaticamente il linger (loginctl enable-linger kipanga) così il servizio parte anche senza login attivo (headless/SSH).


API

Tutte le risposte sono JSON (UTF-8). Sono impostati gli header CORS e Access-Control-Allow-Private-Network per le richieste cross-origin / Private Network Access dei browser (incluso il preflight OPTIONS).

Sintesi frase — GET /speak

GET /speak?text=<testo>[&lang=it|en][&folder=<percorso>]

Sintetizza il testo, salva il file MP3 nella cartella indicata (parametro folder, altrimenti CACHE_DIR) e restituisce l'hash del file. Lingua di default it; valori ammessi it ed en (altri valori → it).

# Cartella default (CACHE_DIR), lingua italiana
curl "http://localhost:9966/speak?text=Serviamo+il+numero+A1+allo+sportello+3&lang=it"

# Lingua inglese
curl "http://localhost:9966/speak?text=Now+serving+A1+at+counter+3&lang=en"

# Cartella custom
curl "http://localhost:9966/speak?text=Serviamo+il+numero+A1&folder=/var/www/audio"

Risposta:

{ "hash": "33f40c2a...", "file": "33f40c2a....mp3" }

Se il file è già in cache la risposta è istantanea. Il nome file è SHA-1("<lang>:<testo normalizzato>"), quindi lingue diverse producono file distinti.

Generazione in bulk — GET /bulk

GET /bulk?codice_coda=<cod>&sportello=<S>&count=<N>[&lang=it,en][&folder=<percorso>]

Genera un file MP3 per ogni numero da 1 a N (numero = <cod><i>, es. A1, A2, … A200; se codice_coda è vuoto il numero è 1, 2, …) e per ogni lingua indicata in lang (separate da virgola, default it). Il testo è costruito dal template per lingua definito in config.py (BULK_TEMPLATES), con i marker di pausa |P|.

Importante: i template in BULK_TEMPLATES devono combaciare byte-per-byte con quelli usati dal frontend per l'annuncio del singolo numero (/speak). Solo così l'hash SHA-1 coincide e l'audio pre-generato viene trovato in cache alla chiamata; altrimenti verrebbe risintetizzato, vanificando la pre-generazione.

I file usano lo stesso schema di naming SHA-1 di /speak, quindi la cache è condivisa: i file già presenti vengono saltati istantaneamente. codice_coda può essere vuoto; sportello è obbligatorio. count è limitato a BULK_MAX (default 1000).

Asincrono: la sintesi è lenta (~secondi per file: 200 numeri possono richiedere minuti su Raspberry Pi), quindi /bulk non attende. Valida i parametri, avvia la generazione in un thread di background e risponde subito 202 Accepted con un job_id. L'avanzamento si segue con /bulk-status.

curl "http://localhost:9966/bulk?codice_coda=A&sportello=3&count=200&lang=it,en"

Risposta immediata (202):

{
  "job_id": "9f3c1a2b…",
  "state": "running",
  "total": 400,
  "count": 200,
  "langs": ["it", "en"],
  "sportello": "3",
  "codice_coda": "A"
}
  • total: numeri da produrre (count × lingue supportate).

Parametri non validi (sportello vuoto, count fuori 1..BULK_MAX, nessuna lingua supportata) → 400.

Stato job bulk — GET /bulk-status

GET /bulk-status?job_id=<id>

Ritorna lo stato del job avviato da /bulk (404 se job_id sconosciuto).

curl "http://localhost:9966/bulk-status?job_id=9f3c1a2b…"
{
  "job_id": "9f3c1a2b…",
  "state": "done",
  "total": 400,
  "generated": 180,
  "cached": 220,
  "errors_count": 0,
  "errors": [],
  "dest": "/var/www/kipanga/system/abaddon/audios"
}
  • state: runningdone (oppure error se il job è fallito in blocco).
  • generated/cached: aggiornati in tempo reale durante l'esecuzione.
  • errors: dettaglio dei problemi non fatali (popolato a fine job).

I job sono tenuti in memoria (si azzerano al riavvio del servizio).

Riproduzione file — GET /audio/<hash>.mp3

Serve un MP3 dalla CACHE_DIR con Content-Type: audio/mpeg. Il nome file deve corrispondere a ^[0-9a-f]{40}\.mp3$ (protezione da path traversal); altrimenti 400. File mancante → 404.

curl "http://localhost:9966/audio/33f40c2a....mp3" --output out.mp3

Marker di pausa |P|

Inserisci |P| nel testo per aggiungere silenzio (durata: PAUSE_MS in config.py):

curl "http://localhost:9966/speak?text=Serviamo+il+numero+%7CP%7C+A1+%7CP%7C+allo+sportello+3"

Il marker è case-insensitive. Frasi con pause diverse producono file MP3 distinti.

File in cache — GET /cache

curl http://localhost:9966/cache
{ "cached_files": 42, "folder": "/var/www/kipanga/system/abaddon/audios" }

IP del client — GET /client-ip

Restituisce l'indirizzo IP del chiamante.

curl http://localhost:9966/client-ip
{ "client_ip": "192.168.1.50" }

Cache

Ogni frase viene sintetizzata una sola volta:

<folder>/<sha1-del-testo>.mp3

La sintesi usa una scrittura atomica (file temporaneo → rename) e un lock per-hash, così la cache resta consistente anche con richieste concorrenti. La cache sopravvive ai riavvii. Per pre-generare tutte le frasi note:

python3 - <<'EOF'
from abaddon import synth_mp3
frasi = [l.strip() for l in open("frasi.txt") if l.strip()]
for i, f in enumerate(frasi, 1):
    print(f"[{i}/{len(frasi)}] {f}")
    synth_mp3(f)
print("Fatto.")
EOF

Troubleshooting

Piper non trovato / permission denied

ls -la ./piper/piper          # deve mostrare -rwxr-xr-x
chmod +x ./piper/piper        # se manca il bit eseguibile

Porta già in uso

ss -tlnp | grep 9966
# Cambia PORT in config.py

Log del daemon

sudo -u kipanga XDG_RUNTIME_DIR=/run/user/$(id -u kipanga) journalctl --user -u abaddon -f