238 рядки
13 KiB
Markdown
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
|
|
```
|