12 KiB
Nextcloud Talk voice-translate bot
Bot per Nextcloud Talk: quando arriva un messaggio vocale in una conversazione dove il bot è presente, lo trascrive, lo traduce nella lingua preferita del destinatario e risponde con il testo tradotto (l'audio sintetizzato verrà allegato una volta completata l'integrazione con l'upload file, vedi TODO).
Architettura
Nextcloud Talk (webhook bot, bots-v1)
→ talkbot/server.py (questo container)
→ whisper.cpp : trascrizione audio → testo + lingua rilevata
→ llama.cpp : traduzione testo → lingua del destinatario
→ qwen-tts : sintesi vocale del testo tradotto
- se esiste voice-samples/<talk-username>.wav → voce clonata
(istanza qwen-tts in modalità voiceclone)
- altrimenti → voce preset di default (istanza qwen-tts in
modalità customvoice)
→ talkbot/server.py posta la risposta in chat (bot message API)
Il container è generico e riusabile: lo stesso bot (stesso secret) può
essere aggiunto a più conversazioni Talk. Il token della conversazione
arriva in ogni richiesta webhook (target.id), quindi non serve
un'istanza per chat.
Voce personalizzata per utente
Se in VOICE_SAMPLES_DIR (di default /app/voice-samples, montato da
~/talkbot-voice-samples via Quadlet) esiste un file chiamato
<talk-username>.wav, il bot userà quella voce (voice cloning) invece
della voce preimpostata di default.
<talk-username> è lo user ID Nextcloud del mittente del messaggio
vocale (lo stesso usato per determinare la lingua di destinazione).
Qualità del cloning: puoi opzionalmente affiancare al campione un
file <talk-username>.txt con la trascrizione esatta di cosa dice il
.wav (es. sara.wav + sara.txt). Se presente, il bot userà la
modalità ICL (in-context learning) di qwen-tts, che dà una clonazione più
fedele. Se manca il .txt, si usa automaticamente x_vector_only_mode
(più veloce, qualità leggermente inferiore, non richiede trascrizione).
Il testo di riferimento puoi ottenerlo facilmente trascrivendo tu stesso il campione con whisper:
curl -X POST http://localhost:8080/inference \
-F "file=@sara.wav" -F "response_format=json"
e salvando il campo text risultante in sara.txt.
Il container qwen-tts carica entrambi i modelli (CustomVoice e Base)
nello stesso processo (QWEN_TTS_LOAD=customvoice,voiceclone, default),
così un'unica istanza espone sia /speech (voce preset) che
/speech/clone (voce clonata): il bot punta a un solo QWEN_TTS_URL e
sceglie l'endpoint giusto in base alla presenza del campione utente.
Questo costa più RAM/tempo di avvio rispetto a un solo modello, ma evita
di dover gestire due container qwen-tts separati.
File
| File | Scopo |
|---|---|
server.py |
Logica del bot: webhook, pipeline whisper→llama.cpp→qwen-tts, risposta |
entrypoint.sh |
Avvia uvicorn, valida le variabili d'ambiente richieste |
talkbot.Containerfile |
Immagine del container |
talkbot.container |
Quadlet Podman/systemd per l'esecuzione come servizio |
README.md |
Questo file |
Setup
1. Registrare il bot su Nextcloud
Serve accesso CLI (occ) al server Nextcloud:
occ talk:bot:install "TranslateBot" <SECRET> https://talkbot.example.tld/webhook \
--feature=webhook
<SECRET> deve corrispondere a NC_BOT_SECRET nel Quadlet.
L'URL deve essere raggiungibile dal server Nextcloud (attenzione a
firewall/reti interne se Nextcloud e il bot non sono sulla stessa rete).
Per aggiungere il bot a una conversazione specifica, va abilitato dalle impostazioni della conversazione stessa (icona bot) o via API conversazioni.
2. App password Nextcloud
NC_ADMIN_USER / NC_ADMIN_PASSWORD sono usate per:
- leggere la lingua preferita del mittente (
/ocs/v1.php/cloud/users/{id}) - scaricare l'allegato audio del messaggio vocale
- caricare l'audio tradotto generato (upload lato Files)
Usa una app password dedicata (Impostazioni personali → Sicurezza → Password per app), non la password reale dell'account.
3. Build e avvio
podman build -t talkbot:latest -f talkbot.Containerfile .
cp talkbot.container ~/.config/containers/systemd/
# modifica i valori NC_URL / NC_BOT_SECRET / NC_ADMIN_USER / NC_ADMIN_PASSWORD
# e gli URL di whisper/llama.cpp/qwen-tts nel file .container
systemctl --user daemon-reload
systemctl --user start talkbot
journalctl --user -u talkbot -f
4. Test rapido
curl http://localhost:8100/health
# {"status":"ok"}
Il vero test end-to-end richiede un messaggio vocale reale inviato in una conversazione Talk dove il bot è abilitato: vedi sezione "Stato dei test".
Variabili d'ambiente
| Variabile | Obbligatoria | Default | Note |
|---|---|---|---|
NC_URL |
sì | — | URL base Nextcloud, senza slash finale |
NC_BOT_SECRET |
sì | — | Secret condiviso usato in occ talk:bot:install |
NC_ADMIN_USER |
sì | — | Utente Nextcloud per API/download/upload |
NC_ADMIN_PASSWORD |
sì | — | App password di NC_ADMIN_USER |
WHISPER_URL |
no | http://whisper:8080 |
Endpoint whisper-server |
LLAMACPP_URL |
no | http://llamacpp:7000 |
Endpoint llama.cpp (OpenAI-compatible chat) |
LLAMACPP_MODEL |
no | (vuoto) | Nome modello, se il tuo router llama.cpp lo richiede |
LLAMACPP_TIMEOUT |
no | 600 |
Timeout (secondi) per la chiamata a llama.cpp |
QWEN_TTS_URL |
no | http://qwen-tts:8000 |
Endpoint qwen-tts (deve girare con QWEN_TTS_LOAD includendo sia customvoice che voiceclone, default) |
QWEN_TTS_TIMEOUT |
no | 1800 |
Timeout (secondi) per la chiamata a qwen-tts |
VOICE_SAMPLES_DIR |
no | /app/voice-samples |
Cartella con i campioni <talk-username>.wav per il cloning |
DEFAULT_TARGET_LANGUAGE |
no | en |
Lingua di fallback se non si riesce a leggere quella dell'utente |
QWEN_TTS_SPEAKER |
no | Ryan |
Voce preset usata quando non c'è un campione utente |
QUEUE_DB_PATH |
no | /app/data/queue.db |
File SQLite della coda dei messaggi in attesa (vedi sotto) |
Coda dei messaggi
Il webhook non elabora i messaggi in modo sincrono: fa solo il parsing/verifica della richiesta e mette il job in coda, rispondendo subito a Nextcloud. Un singolo worker in background consuma la coda in sequenza (un messaggio alla volta, per non sovraccaricare whisper/llama.cpp/qwen-tts con richieste in parallelo su hardware limitato).
La coda è persistita su SQLite (QUEUE_DB_PATH, di default
/app/data/queue.db, montato come volume in talkbot.container) invece che
in memoria: un job viene rimosso dalla tabella solo a elaborazione completata,
quindi se il container si blocca o viene riavviato mentre ci sono messaggi in
coda (anche quello attualmente in lavorazione), al riavvio successivo vengono
ripresi ed elaborati normalmente, senza perdita.
Stato dei test / cosa manca ancora
Questo file va aggiornato man mano che verifichiamo il comportamento reale contro un'istanza Nextcloud Talk vera. Alla creazione (2 settembre 2026):
- Non ancora testato contro un webhook reale. La struttura del
payload per i messaggi vocali (dove si trova esattamente l'URL/ID del
file allegato dentro
object.content.parameters) è assunta dalla documentazione generica dei bot, ma va confermata con un vero messaggio vocale: il parametro potrebbe chiamarsi diversamente o avere una struttura leggermente diversa da quella ipotizzata infile_paramdentroserver.py. - Download dell'allegato audio:
download_attachment()assume chefile_param["link"]sia un URL scaricabile direttamente con Basic Auth. Da verificare se serve invece passare dal WebDAV (/remote.php/dav/files/...) o da un endpoint di preview/download specifico dei messaggi di chat. - Upload e allegazione dell'audio tradotto:
upload_and_share_audio()è un placeholder. Attualmente il bot risponde solo con il testo tradotto (viasend_bot_message), non ancora con il file audio allegato in chat. Serve capire l'API esatta di Talk per condividere un file già presente nei Files dell'utente dentro una conversazione (probabilmentePOST /ocs/v2.php/apps/spreed/api/v1/chat/{token}/shareo simile — da verificare nella doc "Chat management"). - Lingua in chat di gruppo:
get_user_language(actor_id)prende la lingua di chi ha inviato il messaggio, non del destinatario. Per chat 1:1 o per un bot dedicato a un singolo utente va bene così; per gruppi con più utenti/lingue diverse la logica "traduci nella lingua del destinatario" va ripensata (quale destinatario? tutti? uno alla volta?). - Rilevamento lingua sorgente: whisper-server deve essere lanciato
con
WHISPER_LANGUAGE=auto(o comunque senza lingua fissa) perchédetected_langintranscribe()sia affidabile. - Gestione errori/timeout: i tre servizi (whisper, llama.cpp, qwen-tts) sono chiamati in sequenza sincrona nello stesso worker HTTP; su CPU la sintesi qwen-tts può richiedere decine di secondi. Da valutare se serve un timeout più alto lato Nextcloud o passare a elaborazione asincrona con risposta differita.
Doppia istanza qwen-tts: risolto, qwen-tts ora carica entrambi i modelli (CustomVoice + Base) nello stesso processo viaQWEN_TTS_LOAD=customvoice,voiceclone(default). Un solo container, un soloQWEN_TTS_URL, entrambi gli endpoint disponibili. Nota: questo raddoppia la RAM e il tempo di avvio rispetto a un solo modello (due checkpoint da 1.7B caricati in memoria su CPU), tienine conto se il sistema ha risorse limitate — in tal caso si può restringere a un solo modello conQWEN_TTS_LOAD=customvoice(ovoiceclone), sapendo che l'endpoint mancante risponderà 400.- Provenienza dei campioni voce: al momento i file
voice-samples/<talk-username>.wavvanno creati manualmente sull'host (es. copiandoli dal container qwen-tts o da altre registrazioni). Non c'è ancora un modo per l'utente di caricare il proprio campione dalla chat stessa (es. "invia un vocale e digli /set-voice per usarlo come tuo campione"). - Sicurezza rete: il Quadlet pubblica la porta 8100 su tutte le interfacce; se Nextcloud non è sulla stessa rete Podman/host, va esposto solo dove necessario (reverse proxy, firewall).
Note di debug utili
# Log del bot
journalctl --user -u talkbot -f
# Verifica manuale della pipeline, bypassando Nextcloud:
curl -X POST http://localhost:8080/inference -F "file=@test.wav" -F "response_format=json"
curl -X POST http://localhost:7000/v1/chat/completions -H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Translate into Italian: Hello"}]}'
curl -X POST http://localhost:8000/speech -H "Content-Type: application/json" \
-d '{"text":"Ciao","language":"Italian","speaker":"Ryan"}' --output test-tts.wav