Файли
bdi_podman_serverconf/containers/talkbot/README.md
T

238 рядки
13 KiB
Markdown

# 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:
```bash
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:
```bash
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
```bash
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
```bash
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_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.
- [x] ~~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
```bash
# 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
```