Dosyalar
bdi_podman_serverconf/containers/talkbot

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 URL base Nextcloud, senza slash finale
NC_BOT_SECRET Secret condiviso usato in occ talk:bot:install
NC_ADMIN_USER Utente Nextcloud per API/download/upload
NC_ADMIN_PASSWORD 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_API_KEY no (vuoto) API key per llama.cpp (header Authorization: Bearer ...); se vuoto, nessun header è inviato
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)

NC_URL e hostname interno: se punti NC_URL all'hostname del container invece che al dominio pubblico (es. http://nextcloud:80 anziché https://tuo.dominio.tld), Nextcloud rifiuta le chiamate OCS con 400 Bad Request a meno che quell'hostname non sia nei trusted_domains. Aggiungilo con:

podman exec -u www-data nextcloud php occ config:system:set trusted_domains <indice_libero> --value=nextcloud

(usa il primo indice numerico non ancora occupato: controllalo con occ config:system:get trusted_domains). In alternativa, più semplice, lascia NC_URL sul dominio pubblico già fidato.

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 in file_param dentro server.py.
  • Download dell'allegato audio: download_attachment() assume che file_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 (via send_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 (probabilmente POST /ocs/v2.php/apps/spreed/api/v1/chat/{token}/share o 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_lang in transcribe() 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 via QWEN_TTS_LOAD=customvoice,voiceclone (default). Un solo container, un solo QWEN_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 con QWEN_TTS_LOAD=customvoice (o voiceclone), sapendo che l'endpoint mancante risponderà 400.
  • Provenienza dei campioni voce: al momento i file voice-samples/<talk-username>.wav vanno 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