ONNX è un formato di file per una rete neurale addestrata: i pesi e il grafo di calcolo, congelati, senza dipendenza dal framework che l’ha addestrata. Un modello esportato in ONNX può girare attraverso ONNX Runtime, un piccolo motore di inferenza che non fa altro che eseguire quel grafo. Non sa come il modello sia stato addestrato, non supporta l’addestramento e non ha bisogno che PyTorch o TensorFlow siano installati.
Diverse nostre librerie rispettano un’unica regola: in fase di esecuzione, le uniche dipendenze sono onnxruntime e numpy. Non “per lo più” — l’importazione del pacchetto stesso non trascina mai un framework di addestramento. audiosronnx (estensione di banda e riduzione del rumore),
voiceclonnx (clonazione vocale), speakeronnx (embedding del parlante), speechonnxmetrics
(valutazione), stressonnx (accento di parola), vadonnx (rilevazione dell’attività vocale) e
phoonnx (fonemizzazione e text-to-speech) la seguono tutte, ciascuna nel proprio pacchetto PyPI. Altre due, phoonnx.js e precise-onnx-js, applicano la stessa idea nel
browser con onnxruntime-web al posto suo.
Perché preoccuparsene
Il modo ovvio di distribuire un modello vocale è tenere il framework di addestramento anche per l’inferenza. È comodo durante lo sviluppo. È una zavorra in produzione.
- Dimensione dell’installazione. Un’installazione PyTorch + CUDA arriva a gigabyte prima ancora di aver caricato un solo modello.
onnxruntimeenumpyinsieme sono poche decine di megabyte. - Nessun CUDA da gestire. Far corrispondere un driver GPU, una versione del toolkit CUDA e una build del framework è una fonte ricorrente di rotture. ONNX Runtime solo-CPU salta tutto questo, e continua comunque a eseguire lo stesso grafo su una GPU dove ne è disponibile una.
- Gira su hardware modesto. Un Raspberry Pi o un portatile di dieci anni fa può eseguire
onnxruntimecomodamente. Di norma non può eseguire uno stack PyTorch completo a una velocità utilizzabile, o installarlo del tutto su una scheda a 32 bit o con memoria limitata. - Un solo artefatto, ogni piattaforma. Lo stesso file
.onnxgira senza modifiche su Linux, macOS, Windows — e, tramiteonnxruntime-web, dentro una scheda del browser. Non c’è un passo di esportazione separato per ogni obiettivo. - Nessun conflitto di versione tra addestramento e servizio. Uno stack di addestramento fissa versioni specifiche di framework e CUDA. Uno stack di servizio vuole l’insieme di dipendenze più piccolo e stabile possibile. Separarli significa aggiornare l’uno senza rompere l’altro.
Cosa costa
Il vincolo è reale, e non è gratuito.
Non si può affinare in processo. Un grafo ONNX non ha ottimizzatore, né passaggio all’indietro.
Ognuna di queste librerie tratta i modelli come artefatti fissi: li si carica e li si esegue. L’addestramento o l’affinamento avviene separatamente, con il framework originale, e il risultato viene esportato in ONNX in seguito. stressonnx e speechonnxmetrics mantengono entrambi un extra opzionale export che include torch puramente per quel passo di conversione offline
— mai per l’inferenza.
Non ogni architettura si esporta in modo pulito. Flusso di controllo dinamico, kernel CUDA personalizzati,
o operazioni senza equivalente ONNX possono bloccare un’esportazione diretta. Il README di audiosronnx lo documenta direttamente: mantiene una lista dei non pubblicati
di modelli che ha valutato e respinto, con le ragioni, anziché fingere che ogni modello di ricerca si porti sempre.
Il preprocessing deve essere reimplementato a mano. Un framework come PyTorch o Kaldi
fornisce implementazioni veloci e testate di STFT (trasformare una forma d’onda in uno spettrogramma),
caratteristiche mel-filterbank e ricampionamento. Una volta che il modello stesso non dipende più da
quel framework, non può farlo nemmeno il suo preprocessing — speakeronnx reimplementa un
filtro log-mel a 80 bande in NumPy puro esattamente per questo motivo, e audiosronnx fa lo
stesso per STFT e ricampionamento. È più codice da far funzionare correttamente, e richiede i propri
test di parità rispetto all’originale.
Un compito, più motori, un’unica API
I modelli vocali addestrati variano enormemente per lingua, condizione di registrazione e dominio di destinazione. Un modello di verifica del parlante addestrato su parlato letto pulito può fallire su audio telefonico. Un modello di clonazione vocale calibrato per il trasferimento del timbro in inglese può perdere intelligibilità su lingue tonali. Non esiste un unico modello che vinca ovunque, perciò impegnarsi su uno solo a priori è una scommessa.
Ogni libreria di questa famiglia sceglie un unico compito e avvolge diversi modelli pubblicati indipendenti dietro un’unica interfaccia, così che cambiare motore sia una modifica di una riga anziché una riscrittura.
audiosronnx separa i suoi due compiti — riduzione del rumore ed estensione di banda (trasformare
una registrazione a banda stretta, come audio telefonico a 8 kHz, in un segnale dal suono più pieno, a frequenza di campionamento
più alta) — dietro due loader, ciascuno sostenuto da diversi motori:
from audiosronnx import load_denoise, load_sr
clean, rate = load_denoise("dpdfnet").denoise("noisy_call.wav") # remove noise
wide, _ = load_sr("lavasr").upscale(clean, rate) # extend to 48 kHz
load_denoise registra attualmente dieci riduttori di rumore (dpdfnet, mossformer2, frcrn,
mpsenet, gtcrn, cmgan, metadenoiser, mossformergan, voicefixer,
deepfilternet), da un modello di 0,54 MB a uno di 415 MB, sotto licenze diverse.
load_sr registra sette estensori di banda (lavasr, novasr, flowhigh,
hifiganbwe, apbwe, sidon, callenhancer). I modelli dominati — quelli che un altro motore
batte su ogni asse misurato — restano comunque nel registro, così che un risultato di benchmark pubblicato
resti riproducibile su richiesta.
voiceclonnx adotta lo stesso approccio per la clonazione vocale — convertire la voce in una
registrazione esistente perché suoni come un diverso parlante di riferimento, senza passare per il
testo:
from voiceclonnx import VoiceCloner
cloner = VoiceCloner(engine="facodec")
out = cloner.clone_voice("source.wav", "reference.wav", "out.wav")
Sono registrati dieci motori (facodec, openvoice, chatterbox, triaan, cosyvoice,
bicodec, knnvc, focalcodec, lscodec, rvc), che spaziano su sei famiglie di modelli
distinte — scambio di caratteristiche kNN, codec fattorizzato, flow-matching, trasferimento del colore tonale,
LM-codec autoregressivo e codec disaccoppiato dal parlante. Dietro le quinte ciascuno viene fornito con
numeri pubblicati di intelligibilità e somiglianza del parlante, così che scegliere un motore sia un
confronto, non un lancio di moneta.
vadonnx applica lo schema alla rilevazione dell’attività vocale — decidere quali parti di un
flusso audio contengano affatto del parlato:
from vadonnx import load_vad
vad = load_vad("silero")
segments = vad.get_speech_segments(audio, sample_rate=16000)
# -> [SpeechSegment(start=0.32, end=2.27), SpeechSegment(start=3.27, end=4.45), ...]
Sono registrate sei famiglie di modelli (silero, marblenet, pyannote, fsmn,
speechbrain, ten), e una IOSignature dichiarativa permette a un unico motore generico
di pilotarne la maggior parte, o di puntare a qualsiasi file VAD .onnx personalizzato.
speakeronnx estrae un embedding del parlante — un vettore a lunghezza fissa che riassume
chi sta parlando, indipendentemente da cosa ha detto — e confronta due embedding per similarità coseno
per verificare se due clip appartengono allo stesso parlante:
from speakeronnx import SpeakerEmbedder, cosine
embedder = SpeakerEmbedder(model="wespeaker-resnet34")
alice1 = embedder.embed("alice_clip1.wav")
alice2 = embedder.embed("alice_clip2.wav")
print(cosine(alice1, alice2)) # e.g. 0.82 - same speaker
Registra nove modelli su quattro famiglie di architetture (WeSpeaker, CAM++, ERes2Net, ReDimNet), con dimensioni di embedding e licenze pubblicate.
stressonnx sceglie l’accento di parola per i front-end di text-to-speech — quale sillaba di una parola
porta l’enfasi, un’informazione che molte lingue non esplicitano (il russo за́мок, castello,
contro замо́к, lucchetto, condividono ogni lettera). Registra una pipeline neurale per il russo, una
seconda per l’ucraino e il bielorusso, e un backend basato su regole e vocabolario che copre 26
lingue senza alcuna inferenza neurale:
from stressonnx import stress
stress("старинный замок стоит на горе", "ru")
# 'стари́нный за́мок сто́ит на горе́'
phoonnx fonemizza il testo (trasforma le parole scritte nelle unità sonore che un modello TTS
consuma) ed esegue il text-to-speech su 17 motori di sintesi registrati e
voci esportate da diversi ecosistemi (phoonnx nativo, Piper, Mimic3, Coqui, MMS,
Transformers):
import wave
from phoonnx.voice import TTSVoice
voice = TTSVoice.load("model.onnx", "model.json")
with wave.open("hello.wav", "wb") as wav_file:
voice.synthesize_wav("Hello world!", wav_file)
phoonnx.js porta gli stessi percorsi di tokenizzazione nel browser con
onnxruntime-web, e precise-onnx-js porta la rilevazione della parola di attivazione (estrazione di caratteristiche MFCC
più un classificatore ONNX, compatibile con i modelli Mycroft Precise) in
JavaScript, entrambi senza un server:
import { loadVoice, synthesizeWav } from "phoonnx";
import { getVoice } from "phoonnx/voices";
const voice = await loadVoice(getVoice("phoonnx_eu-ES_dii_unicode")!);
const blob = await synthesizeWav(voice, "Kaixo mundua!");
I pesi per audiosronnx (18 modelli pubblicati) e voiceclonnx (10 modelli pubblicati)
vivono come download separati sull’organizzazione Hugging Face di TigreGótico, recuperati al primo utilizzo e messi in cache localmente, così che
scegliere un motore diverso sia una modifica di configurazione, non una ridistribuzione.
Chiudere il cerchio: giudicare i motori invece di indovinare
Registrare molti motori dietro un’unica API ripaga solo se si può dire quale sia
effettivamente migliore per il proprio input. È a questo che serve speechonnxmetrics: una libreria di metriche costruita sullo stesso vincolo numpy + onnxruntime, così che valutare un modello non costi nulla in più da installare.
Raggruppa le metriche in tre tipi. Gli stimatori MOS senza riferimento — UTMOS, DNSMOS, NISQA, SIGMOS — prevedono un Mean Opinion Score, il voto di naturalezza da 1 a 5 che un panel di ascoltatori umani darebbe a una clip, senza bisogno di un riferimento pulito con cui confrontarsi. Le metriche intrusive — STOI (intelligibilità obiettiva a breve termine), SI-SDR (rapporto segnale-distorsione invariante di scala), MCD (distorsione mel-cepstrale) — necessitano di un riferimento pulito corrispondente e misurano quanto l’output vi si avvicini. Le metriche testuali basate su ASR — WER (tasso di errore sulle parole) e CER (tasso di errore sui caratteri) — eseguono un riconoscitore vocale sull’output e confrontano la trascrizione con il testo atteso, individuando i casi in cui un modello produce un audio che suona bene ma dice le parole sbagliate.
import speechonnxmetrics as s
print(s.score("degraded.wav", ["utmos"]))
# -> {'utmos': 4.41...}
print(s.score("clone_output.wav", ["stoi", "mcd", "si_sdr"], ref="source.wav"))
# -> {'stoi': 0.662..., 'mcd': 10.459..., 'si_sdr': -26.937...}
Questo trasforma la scelta del motore da un test d’ascolto in una tabella. voiceclonnx
pubblica esattamente quel confronto per i suoi dieci motori di clonazione — WER rispetto alla
trascrizione sorgente più un punteggio separato di somiglianza del parlante per ciascuno, così che “facodec
dà lo 0% di WER” o “lscodec scambia il WER per un trasferimento del timbro più forte” sono affermazioni misurate,
non impressioni. Moltiplicate questo per lingue e condizioni di registrazione e il confronto manuale
smette di essere realistico; una metrica oggettiva è ciò che rende un registro a dieci motori
utilizzabile invece che opprimente.
Dove è utile
Se avete bisogno di elaborazione vocale offline — ripulire una registrazione, clonare una voce,
rilevare chi sta parlando, o sintetizzarne una — su hardware che non vedrà mai una GPU,
questa è la forma da cercare: una piccola dipendenza di runtime, una scelta tra modelli pubblicati
invece di un unico predefinito fisso, e un modo per misurare quale funzioni davvero per il vostro
caso. Ognuna delle librerie sopra è a un pip install di distanza, con licenza MIT o Apache a livello
di codice (i pesi dei singoli modelli portano le proprie licenze a monte, documentate per
motore), e gira allo stesso modo su un portatile, un server o un Raspberry Pi.
Contattateci tramite /contact o guardate cos’altro costruiamo su /services.