AgileTTS — API
Il contratto delle API: due strati, un solo servizio. Il fronte è l'interfaccia OpenAI-compatibile con chiavi (per i prodotti, quando AgileTTS sarà maturo); il motore è il sintetizzatore residente, raggiungibile solo dalla rete interna WireGuard.
I due strati
| strato | versione | dove | stato |
|---|---|---|---|
| motore | AGILETTS-v0.9 + cancello d'attacco | rete interna WireGuard | produzione, residente (WARM), modello 0.6B |
| fronte | AGILETTS-FRONTE-v0.6 | https://api.agiletts.agile.software | consegnato, non ancora in linea |
Autenticazione (fronte)
Chiave nell'intestazione Authorization: Bearer ats_… oppure X-Api-Key. Ogni chiave è per un prodotto e un tenant, con uno scope (speech, stream, voices), un limite al minuto e una quota di caratteri al mese. I testi non vengono mai registrati: si contano solo richieste, caratteri e tempi.
Chiave passe-partout di prova: per chi integra per la prima volta, una chiave di ambito test con limiti bassi (20/min, 20000 caratteri/mese di default) e una scadenza dichiarata (di default 24 h), utile per uno smoke test prima di chiedere una chiave di prodotto. Scaduta risponde 403 KEY_EXPIRED. Il fronte accetta anche la chiave passe-partout condivisa fra tutti i servizi sovrani di Agile Software, la stessa per ognuno. Si richiede al centralino interno.
Fronte — rotte OpenAI-compatibili
| Metodo e percorso | Cosa fa |
|---|---|
POST /v1/audio/speech | sintesi vocale: model = agiletts (alias tts-1, tts-1-hd, gpt-4o-mini-tts), input ≤ 4096 caratteri (oltre 2000 il fronte spezza a fine frase e riunisce), voice, response_format = wav | pcm | pcm16k | mp3 | opus | flac | aac, speed 0,5–2,0, stream: true per l'audio a pezzi (solo pcm/pcm16k) |
GET /v1/models, GET /v1/models/{id} | i modelli disponibili, con created, owned_by, aliases; /v1/models/tts-1 restituisce l'oggetto con alias_di: "agiletts" |
GET /v1/voices | le voci che la chiave può usare: id, lingua, lessico, ritmo, politica di ripiego — mai il riferimento né id esterni |
GET /v1/usage | i consumi della propria chiave: richieste, caratteri, millisecondi, audio prodotto, errori, stato e rotazioni della chiave — mai i testi |
GET /v1/admin/keys, POST /v1/admin/keys, GET /v1/admin/keys/{id}, POST /v1/admin/keys/{id}/rotate, POST /v1/admin/keys/{id}/revoke | la console per azienda: un cliente elenca, crea, ruota e revoca le proprie chiavi da sé, con una credenziale di console (mai una chiave di prodotto). La chiave in chiaro si vede solo alla creazione; le chiavi di un'altra azienda rispondono come una chiave che non esiste; ogni operazione, e ogni rifiuto, resta scritta Dal 30/9 l’azienda si nomina e non si indovina: chi chiede le chiavi o le persone di un’altra azienda riceve un rifiuto, dove prima riceveva le proprie senza che nessuno glielo dicesse. |
GET /v1/admin/utenti, POST /v1/admin/utenti, GET /v1/admin/utenti/{id}, POST /v1/admin/utenti/{id}/sospendi, POST /v1/admin/utenti/{id}/riattiva, POST /v1/admin/utenti/{id}/ruolo | le persone di un'azienda, dalla console: l'amministratore elenca, invita, sospende, riattiva e cambia ruolo ai propri utenti da sé. Chi sospende sé stesso, e chiunque lasci l'azienda senza amministratori attivi, si sente dire di no; le persone di un'altra azienda rispondono come una persona che non esiste; ogni operazione, e ogni rifiuto, resta scritta Dal 30/9 l’azienda si nomina e non si indovina: chi chiede le chiavi o le persone di un’altra azienda riceve un rifiuto, dove prima riceveva le proprie senza che nessuno glielo dicesse. |
GET /v1/admin/operazioni | il registro di chi ha fatto che cosa dentro l’azienda, letto dall’azienda: chi ha emesso una chiave e quando, chi l’ha revocata, chi ha invitato chi, e anche i tentativi rifiutati. Si filtra per persona, per azione e per intervallo di tempo, e si legge a pagine. Un utente semplice non lo vede; le righe di un’altra azienda non ci sono mai, e chi le chiede riceve un rifiuto invece della propria azienda senza dirlo. Anche leggerlo lascia la sua riga: guardare i dati di un’azienda è un accesso ai suoi dati. |
GET /v1/admin/tenants/{id}/piano, PUT /v1/admin/tenants/{id}/piano | il piano concordato con l'azienda — quante richieste al minuto, quanti caratteri al mese — scritto dove il servizio lo può far valere. Lo scrive solo chi amministra tutto il servizio: un amministratore d'azienda che se lo alzasse da sé avrebbe un tetto per modo di dire. Lui lo legge, insieme al consumo del mese, e quel numero è lo stesso su cui il servizio rifiuta. Un'azienda senza piano risponde «nessun piano», non «azienda sconosciuta»; l'azienda altrui e una che non esiste rispondono allo stesso modo |
GET /v1/admin/licenze, POST /v1/admin/licenze, GET /v1/admin/licenze/{id}, POST /v1/admin/licenze/{id}/sospendi, POST /v1/admin/licenze/{id}/riattiva, POST /v1/admin/licenze/{id}/rinnova, POST /v1/admin/licenze/{id}/revoca, POST /v1/admin/licenze/{id}/chiavi, DELETE /v1/admin/licenze/{id}/chiavi, GET /v1/admin/licenze/{id}/uso(stesse rotte anche senza il prefisso di versione, la forma comune a tutti i servizi Agile: POST /admin/licenze, GET /admin/licenze, GET /admin/licenze/{id}, POST /admin/licenze/{id}/sospendi, POST /admin/licenze/{id}/riattiva, POST /admin/licenze/{id}/rinnova, POST /admin/licenze/{id}/revoca, POST /admin/licenze/{id}/chiavi, DELETE /admin/licenze/{id}/chiavi, GET /admin/licenze/{id}/uso) | la licenza: il diritto di un’azienda (o di una persona) di usare AgileTTS, con piano, ambiti, quota del mese, limite al minuto, inizio e scadenza. Le chiavi stanno sotto una licenza: la licenza si sospende e tutte le sue chiavi tacciono insieme, si riattiva e riprendono — senza ridistribuire niente ai prodotti; ruotare una chiave non tocca la licenza. Lo stato che conta è quello vero: una licenza con la scadenza di ieri si legge scaduta anche se in tabella c’è scritto attiva, e le sue chiavi ricevono un rifiuto che dice quale licenza e perché. Una scaduta si rinnova (non si riattiva); una revocata non torna: se ne rilascia un’altra. L’uso si misura per licenza ed è la riga che va in fattura, comprese le chiavi revocate e ruotate. I numeri di partenza del piano prova vengono dalla capacità misurata del servizio: 50000 caratteri al mese, 10 richieste al minuto, 30 giorni (l’uno per cento della capacità di servizio dichiarata, 5,6 milioni di caratteri al mese per scheda). standard ed enterprise sono su misura e non hanno numeri di partenza: si scrivono all’emissione, e chiederne una senza numeri è un rifiuto che lo dice. Il ciclo di vita lo governa chi amministra tutto il servizio; l’amministratore d’azienda legge le sue licenze e ci emette le chiavi dentro i limiti. La licenza di un’altra azienda risponde come una che non esiste |
POST /v1/richieste-licenza (pubblica, senza chiave), GET /v1/admin/richieste(anche senza il prefisso di versione: POST /richieste-licenza, GET /admin/richieste) | il modulo «richiedi una licenza» della pagina pubblica: azienda, referente, email, piano e note. Non manda una email e non inghiotte: la richiesta si registra con un numero e la pagina lo dice («richiesta registrata n. 7»), un giro sul box la porta a chi la deve leggere e la segna avvisata solo se l’avviso è partito davvero. I prezzi non stanno in nessuna risposta: sono su richiesta. Due guardie, perché la rotta è pubblica: un limite di richieste per indirizzo e un campo nascosto che una persona non vede — se arriva pieno l’ha riempito un robot, e la richiesta viene rifiutata dicendolo, non accettata a vuoto |
POST /tts, POST /tts-stream | passaggio storico: stesso corpo del motore, con chiave → l'iniezione nei prodotti è cambiare indirizzo e aggiungere l'intestazione |
GET /health | stato del fronte e del motore: model_loaded, caldo, coda, trim_attacco, normalizza, formati, testi_lunghi, alias_modelli, politiche |
Motore — rotte interne
Solo rete interna WireGuard, nessuna autenticazione. JSON in ingresso, audio in uscita, una sintesi per volta. Il corpo delle richieste è lo stesso del passaggio storico del fronte.
| Metodo e percorso | Cosa fa |
|---|---|
GET /health | stato del motore: versione, model_loaded, warm, VRAM libera, trim_attacco, voci caricate, contatori (requests, synth_total_s, identity_retries, …) |
GET /voices | le voci con riferimento di testo, lessico, ritmo (atempo_target_wpm) e parametri per voce |
POST /tts | sintesi in blocco: text (obbligatorio, ≤ 2000 caratteri), voice (default patrizia), format = wav | wav16k | pcm16k | mp3 | mp3_16k, apply_lexicon, atempo, trim_attacco. Risposta: corpo audio + intestazione X-AgileTTS-Meta |
POST /tts-stream | streaming a pezzi: stessi campi, format = pcm24k (default) | pcm16k; corpo PCM s16le mono a pezzi finché finisce |
POST /warm, POST /admin/unload | carica il modello (/warm) o lo scarica (/admin/unload, solo regia); in produzione WARM lo ricarica alla prossima richiesta |
Formati audio
- Blocco:
wav(24 kHz float→pcm),wav16k,pcm16k(grezzo s16le mono 16 kHz, telefonia),mp3,mp3_16k. - OpenAI (fronte):
wav,pcm(24 kHz s16le mono),pcm16k,mp3,opus(ogg 48 kb/s),flac,aac(adts 96 kb/s) — gli ultimi tre rifatti dal fronte con ffmpeg. - Streaming (fronte): solo
pcm/pcm16k, corpo chunked. speed0,5–2,0 (fuori intervallo viene stretto);stream_formatdiverso daaudio→400 UNSUPPORTED_FORMAT.
Errori
| HTTP | codice | significato |
|---|---|---|
| 401 | KEY_MISSING, KEY_INVALID | chiave assente o sconosciuta |
| 403 | KEY_REVOKED, KEY_ROTATED, KEY_EXPIRED, SCOPE_NOT_ALLOWED, PLAN_EXCEEDED | chiave revocata, ruotata (grazia finita), scaduta (chiave di prova), senza il permesso richiesto, oppure chiave che da sola prometterebbe più del piano dell'azienda |
| 404 | UNKNOWN_VOICE, UNKNOWN_MODEL, TENANT_NOT_FOUND | voce o modello non esistenti, oppure (solo per chi amministra tutto il servizio) un piano scritto su un'azienda che non esiste |
| 400 | BAD_REQUEST, UNSUPPORTED_FORMAT | corpo non valido o formato non supportato |
| 409 | ALREADY_EXISTS, SELF_SUSPEND_FORBIDDEN, LAST_ADMIN | solo sulla console per azienda: persona gia' invitata, amministratore che sospende se' stesso, oppure operazione che lascerebbe l'azienda senza amministratori attivi |
| 413 | TEXT_TOO_LONG | oltre 4096 caratteri sul fronte (oltre 2000 sul motore) |
| 429 | RATE_LIMIT, QUOTA_EXCEEDED, TENANT_RATE_LIMIT, TENANT_QUOTA_EXCEEDED | troppe richieste al minuto o quota mensile esaurita: della singola chiave, oppure dell'intera azienda sulla somma delle sue chiavi (con Retry-After) |
| 503 | BUSY, ENGINE_DOWN, SHUTTING_DOWN | motore occupato (con Retry-After), guardia VRAM: non disponibile, oppure il fronte si sta fermando per un ridispiego (con Retry-After: chi sta già parlando finisce, chi arriva riprova fra pochi secondi) |
| 502 | ENGINE_ERROR | errore interno del motore |
| 500 | synth_failed, stream_failed | sintesi fallita (blocco) o stream fallito (solo prima dei primi byte) |
| 501 | pcm16k_unavailable, streaming_not_available | formato o streaming non disponibile sul motore |
Gli errori del fronte sono in forma OpenAI {"error":{"code","message","type"}}. Sui 5xx il fronte non ripiega mai da sé: dice al cliente cosa può fare (X-AgileTTS-Ripiego e error.ripiego), la decisione resta a chi chiama.
Intestazioni di risposta
X-Request-Id: identifica ogni richiesta.X-AgileTTS-Via: chi ha parlato (sempreagiletts; mai un ripiego silenzioso).X-AgileTTS-Meta: sul blocco, le misure della sintesi (voce, caratteri, parole, secondi,wpm,rtf,identity_sim,identity_retries,attacco, …).X-AgileTTS-Pezzi: sui testi lunghi spezzati, il numero di pezzi riuniti.X-AgileTTS-Chiave-Scade/X-AgileTTS-Chiave-Nuova: durante la rotazione, scadenza della chiave in grazia e id della nuova.Retry-After: sui 503 e sui 429, quando riprovare, e dal 30/9 lo stesso numero sta anche nel corpo (error.retry_after_s). Sui tetti a finestra è il numero esatto — i secondi che mancano allo scoccare del minuto, o alla riapertura del mese per la quota, conerror.reset_atche porta l'istante — e sul motore pieno è quel che resta alla richiesta in corso, misurato, spalmato su due secondi perché dodici clienti rifiutati insieme non tornino nello stesso istante. I rifiuti che non si riprovano (401, 403, 404, 413, 502) non portano nessun numero.
Esempio
curl https://api.agiletts.agile.software/v1/audio/speech \
-H "Authorization: Bearer $AGILETTS_KEY" -H "Content-Type: application/json" \
-d '{"model":"agiletts","input":"Buongiorno, come posso aiutarla?","voice":"serena","response_format":"wav"}' \
-o saluto.wav
Con il client OpenAI ufficiale basta cambiare base_url e la chiave: model: "agiletts" (o "tts-1") e response_format: "opus" funzionano senza modifiche al codice.
Client per i prodotti
Oltre al client OpenAI ufficiale (che parla col fronte senza modifiche), il repo porta un client di esempio in pura libreria standard, client/agiletts_client.py. Per chi preferisce la riga di comando c'è client/agiletts-cli.py, il client a riga di comando in pura libreria standard che avvolge client/agiletts_client.py: sintesi a blocco o in streaming su file (-o), models/model/voices/usage, chiave da --api-key o AGILETTS_API_KEY, errori su stderr con esito non-zero. La guida passo passo per spostare un prodotto sul fronte — URL, chiave, formato, timeout, 503 BUSY e ripiego, il «vai» e il rollback uno per volta — è in docs/ADOZIONE.md.