Blog/LLMOps és értékelés

Helyi felolvasás nagyban: 95 cikk narrálása nyílt modellekkel

Három nyílt TTS-modell egy laptopon: hogyan lett 95 cikkből 153 perc felolvasás — a SQLite-soroktól a tagolt szintézisen és a 192 kbit/s AAC-en át az oldal mellett maradó lejátszóig.

··11 perc olvasás

  • Text-to-speech
  • Audio
  • Kokoro
  • FFmpeg
  • Vue
Hullámdiagram a narrációs pipeline-ról: az adatbázsissorok mondatokra hullanak, egy helyi modell szintetizálja őket, AAC-kódolásra kerülnek, és a képernyő szélén ragadó fülről indulnak.

A lényeg röviden

  • A helyi TTS megfordítja a narrálás gazdaságát: 95 cikk és 153 perc hang körülbelül 35 perc számítási időbe és nulla API-díjba kerül — egy plusz cikk határköltsége az áram.
  • Ilyen méretben a modellválasztás sebesség-minőség Pareto, nem benchmark-harc: a Piper volt a leggyorsabb, a Bark sosem fejezett be, a Kokoro v1.0 (82M, ONNX) pedig a proszódiát nyerte nagyjából négyszeres valós idejű sebességgel.
  • Mondatoknál tagolj, 400 karakterig, 80 ms csenddel a darabok között; az okosabb vágópontok szinte semmit nem adnak, amit a hallgató észrevenne.
  • A WAV munkaformátum, nem kézbesítési formátum: a 192 kbit/s-os AAC faststarttel 406 MB-ról 181 MB-ra vágta a méretet, és a letöltés befejezése előtt teszi használhatóvá a hosszt és a pörgetést.
  • A prerenderelt lejátszóban az események verik a feltételezéseket: a loadedmetadata lefuthat a hidratálás előtt, ezért a hosszt durationchange-en, canplay-en és mountkor újra kell olvasni — az állapotot iratkozz fel, ne az értesítésre.

Cikk meghallgatása

0:000:00

Mostantól minden cikkhez tartozik felolvasás: 95 bejegyzés, 153 perc hang, egyetlen laptopon generálva, egyetlen API-hívás nélkül. Ez a poszt a teljes beszámoló — hogyan hasonlítottunk össze három nyílt text-to-speech modellt, hogyan alakítja a pipeline a SQLite-sorokat mondatnyi szintézis-darabokká, miért AAC a kézbesített formátum 192 kbit/s-on, és hogyan épül a lejátszó, hogy a hang együtt fusson az oldallal.

Először a számok, mert ezek keretezik az alábbi minden döntést: 95 cikk (45 blogbejegyzés és 50 eszközismertető), 153 perc kész narráció, körülbelül 35 perc generálási idő, 406 MB köztes WAV 181 MB kézbesített AAC-ra csökkentve, és nulla euró API-díj. Minden egyetlen, 16 GB memóriájú Apple Silicon laptopon futott.

Miért helyi a text-to-speech

A felhőben futó text-to-speech kiváló és negyedévről negyedévre jobb lesz — de mérik. ilyen volumenben a számla minden szóval, minden szerkesztés utáni újraépítéssel, minden hang- vagy sebességkísérlettel nő. A helyi pipeline megfordítja a gazdaságot: egy plusz cikk határköltsége néhány cent áram és két perc várakozás. Ráadásul reprodukálható — ugyanaz a szöveg, ugyanaz a modellverzió és ugyanaz a hang jövőre ugyanazt a fájlt adja, és ez számít, amikor egy szerkesztett cikket újra kell narrálni, hogy a hang ne csússzon el.

Van egy másik ok is: a szöveg nem hagyja el a gépet. Egy még publikálatlan cikk vázlatát ugyanaz a folyamat olvassa, amely közzéteszi, ugyanazon a gépen — és ha egy bekezdés változik, egy slug újrafuttatása egy fájlt állít elő, nem kilencvenötöt.

Három modell, egy laptop

A Piper indult, mert a legjobb értelemben unalmas: kis ONNX-modell (a LibriTTS high hang körülbelül 137 MB), espeak-ng fonémázás, 22,05 kHz kimenet, generálás nagyjából ötszörös valós idejű sebességgel. Az eredmény tiszta és konsztisztens — egy kompetens hírolvasó —, de egyben egyenletes. A hosszú cikkek kissé laposak.

Az első akadály a csomagolás volt, nem a minőség: a prebuilt macOS bináris csak x86_64-re készült, és Apple Siliconon megtagadta a linkelést az arm64-es espeak-ng könyvtárral architektúra-ütközés miatt. A Python csomag teljesen kikerüli a binárist, és percek alatt futott. Hasznos emlékeztető: az „nem fut" gyakran eszközlánc-probléma, nem modellprobléma.

A Bark volt a három legcsábítóbb: expresszív, képes nevetésre és sóhajtásra, publikált mintákkal, amelyek bármelyik másik modellt metronómnak hangoltatnak. Ez volt az is, amelyik sosem produkált használható mondatot. A jelenlegi PyTorch a torch.load alapértelmezését weights_only-ra állította, így a checkpoint kicsomagolása addig bukott, amíg nem patchelték; amikor a betöltés működött, a CPU-s inferencia elég lassú volt, hogy 95 cikk a nap nagy részét vigye. Az expresszivitás nem érte meg ezt az árat — a Bark erre a munkára kutatási játék marad.

A Kokoro v1.0 a kokoro-onnx kötéseken keresztül volt a megtalálás: 82 millió paraméter, körülbelül 325 MB ONNX, 24 kHz kimenet, 54 hang, és generálás nagyjából négyszeres valós idejű sebességgel, érezhetően jobb proszódiával, mint a Piper — a mondatok leteszik a hangsúlyukat, a számok és rövidítések pedig nem a kisebb modellre jellemző akadozással szólnak. Így szól most minden cikk ezen az oldalon.

ModellSúlyokKimenetSebesség ezen a gépenÍtélet
Piper, LibriTTS highkörülbelül 137 MB, ONNX22,05 kHz WAVkörülbelül 5x valós időGyors és konsztisztens, kissé lapos
Bark a Sunótólkörülbelül 2 GB, PyTorch24 kHz WAVa költségkereten belül sosem készült elExpresszív, itt gyakorlatlan
Kokoro v1.082M, körülbelül 325 MB, ONNX24 kHz WAVkörülbelül 4x valós időTermészetes proszódia; a kiszállított hang

A generálási pipeline

A tartalmi adatbázis az egyetlen igazságforrás, ezért a pipeline közvetlenül onnan olvas: cím, leírás, kulcsmondatok és GYIK egyik oldalon, a cikk blokklistája a másikon. Semmit nem kapar le a renderelt oldalról. Ha egy bekezdés a posztban van, narrálja — és ha tábla, callout vagy kódblokk, akkor úgy bontja szét, hogy a hallgató követni tudja: a táblasorok vesszővel elválasztott sorokká válnak, a callout-címeket fejezetként mondják, a kód úgy hangzik, ahogy íródott.

Két keretfeltétel formálja a pipeline középső részét. A modellek levágják a hosszú bemenetet, ezért a cikkeket bontani kell; a proszódiát nem szabad félbevágni, ezért a vágások mondatvégeken landolnak. A chunker szándékosan egyszerű: mondatok gyűjtése 400 karakterig, kiürítés a határon, a darabok 80 milliszekundum csenddel fűzve. Az okosabb vágópontok — bekezdésvégek, fejezetek mint intonációs újraindítás — szinte semmit nem hoztak; a mondat az a mértékegység, amit a hallgató valóban észrevesz.

A chunker

def chunks(text, limit=400):
    """Accumulate sentences up to limit characters; never split mid-sentence."""
    out, buf = [], ""
    for sentence in text.split(". "):
        cand = (buf + " " + sentence).strip()
        if len(cand) > limit and buf:
            out.append(buf + ".")
            buf = sentence
        else:
            buf = cand
    if buf:
        out.append(buf)
    return out

pieces = []
for part in chunks(article_text):
    audio, _ = model.create(part, voice="af_bella")
    pieces.append(audio)
    pieces.append(np.zeros(int(24000 * 0.08)))   # 80 ms between chunks
sf.write(tmp_wav, np.concatenate(pieces), 24000)

Egy hang olvassa fel mind a 95 cikket. Ez af_bella, miután ugyanazt a bekezdést a 54 hang többjével is lejátszottuk. A konzisztencia a lényeg: az archívumnak egy kiadványnak kell hangzania, nem lottónak. A narráció minden cikk angol szövegét olvassa fel — a német és magyar oldalakról is —, és a cikk angol felolvasásaként van megjelölve, nem fordításaként.

A WAV munkaformátum, nem kézbesítési formátum

A szintézis lépés 24 kHz, 16 bites PCM-et ír: helyes, pörgethető, tömörítetlen — és körülbelül 406 MB 153 perc hangért. A lemezen ártalmatlan, a hálózaton hiba, és mindent prerenderelő oldalon egy nagyságrenddel a legnagyobb asset-osztály lenne.

A kézbesített formátum AAC M4A-konténerben, 192 kbit/s, mono. Ez a bitsebesség bőkezű beszédre — 96 és 128 kbit/s 24 kHz-en már átlátszóan hangzik —, de a 192 tartalékot hagy, és cikkenként körülbelül egy megabájtba kerül. A konténer ugyanannyit számít, mint a kodek: a faststart a moov atomot a fájl elejére teszi, így a böngésző letöltés vége előtt ismeri a hosszt és pörgetni tud. Nélküle a lejátszók 0:00-n állnak, és az utolsó bájtig nem hajlandóak tekerni.

ffmpeg -i article.wav -c:a aac -b:a 192k -ac 1 -movflags +faststart article.m4a

406 MB lett 181 MB — 55 százalék csökkenés, minden fájl 1,5 MB alatt, sima range-kérésekkel és cache-headerekkel kiszállítva. A WAV-fájlok sosem érik el a build kimenetét: a kódoló sikerét követően töröljük őket.

A lejátszó

A kései hangot senki sem hallja, ezért a lejátszó csak a metadatokat tölti le: a header lejön, a hossz kiderül, és egyetlen minta sem töltődik le, amíg az olvasó meg nem nyomja a Playt. Semmi sem indul automatikusan — az az oldal, amelyik megszólal, olyan oldal, amit bezárnak.

Minden cikk egy soros lejátszót kap a tartalomjegyzék alatt: play és pause, pörgethető haladásvonal aktuális és teljes idővel, és 1x, 1,25x, 1,5x és 2x között váltó sebességkapcsoló. A pörgető natív range input az oldal stílusában — a billentyűzetes navigáció és a képernyőolvasó-szemantika örökölve, nem újraimplementálva.

A soros lejátszó hasztalan, amint alá görget az ember, ezért átadja a helyét a nézet jobb szélére rögzített ragadós fülnek. Egy IntersectionObserver figyeli a sort: amint eltűnik a képernyőről, a fül megjelenik; visszagörgetve félreáll. A fül alulról töltődik a haladással — a fejlédés egy pillantással olvasható —, és tiszteletben tartja a prefers-reduced-motiont, akárcsak a felület többi része.

Mi tört össze menet közben

  • A csomagolás kétszer verte a modelleket. A Piper csak x86_64-es binárist szállított arm64-es gépre, a Bark checkpoint-betöltése pedig akkor bukott, amikor a PyTorch a weights_only alapértelmezést fordította. Egyik hibának sem volt köze a beszédhez.
  • A verzió-eltolódás valós. A kokoro-onnx csomag int32-ként adta át a speed paramétert, miközben a kiszállított modell floatet vár — egy soros javítás, két soros stacktrace olvasásával megtalálva, nem találgatva.
  • A hangot ellenőrizd, nem csak a fájlt.
  • Az ffprobe minden kimeneten fogta ki a hossz-problémákat, mielőtt böngészőhöz értek volna; a létező fájl nem játszó fájl.
  • A régi formátumok nehezen halnak meg. Az első kör WAV-okat hagyott a build kimenetén, és csak azért derült ki, mert egy kérés rossz bájtszámot adott vissza.

A számok

MutasatóÉrték
Narrált cikkek95 (45 blogbejegyzés, 50 eszközismertető)
Kész hang153 perc
Generálási falidőkörülbelül 35 perc, 4,5x valós idő
Csúcsfoglalás2 GB alatt
Köztes WAV406 MB
Kiszállított AAC181 MB, körülbelül 1 MB cikkenként
API-költség0 EUR

Együtt olvasva a számok azt mondják, hogy ennek a pipeline-nak a költsége a türelem, nem a pénz. Egy teljes újraépítés — hangcsere után vagy olyan szerkesztés után, ami minden cikket érint — bőven egy óra alatt megvan egy olyan gépen, ami közben tovább használható. Ez az érv a helyi text-to-speech mellett ekkora léptékben: nem az, hogy felülmúl egy felhős Frontier-hangot expresszivitásban, hanem az, hogy a narrációt alapértelmezéssé teszi, nem költségrovattá.

Mit csinálnék másképp

A Kokoroval kezdeni. A shoot-out nem volt kidobott idő — modelleket összehasonlítani az, ami a választást védhetővé teszi —, de a kiszállított pipeline a Kokoróval egyetlen jelöltként is azonos lett volna. Másodszor: közvetlenül a szintézisből AAC-be kódolni, WAV-okat nem tárolni; a köztes formátum egy takarító lépést és 406 MB sosem szükséges fájlt adott hozzá. Harmadszor: a lejátszó médiaeseményeit feliratkozandó állapotként kezelni, nem elkapandó értesítésként — és a hossz-hiba sosem történik meg.

Ami marad, az a hatókör. A narráció egyelőre angol nyelvű — egy hang, egy nyelv, 95 cikk —, és a nyelvenkénti hangok az egyértelmű következő lépés, ha a német és magyar olvasóközönség kéri. Addig az angol narráció az oldal minden terméknevének kiejtési útmutatója is — ez maga egy csendes hasznosság.

Források

  1. Kokoro onnx: futtatókötések
  2. Kokoro-82M modellkártya
  3. Piper text-to-speech
  4. Bark a Sunótól
  5. FFmpeg AAC-encoder dokumentáció
  6. torch.load weights_only dokumentáció
  7. ESpeak NG fonémázó

Gyakori kérdések

Miért nem felhő alapú text-to-speech API-t használsz?

Mert 95 cikknél a mért díjak minden szerkesztésnél és újraépítésnél számítanak, és mert a reprodukálhatóság fontosabb: ugyanaz a szöveg ugyanazzal a modellverzióval jövőre ugyanazt a fájlt állítja elő. A felhőben futó hangok továbbra is az expresszív csúcson győznek, de nem egy teljes archívum gazdaságosságában.

Melyik modell hangzik a legjobban?

A Kokoro v1.0 — és ez szállít ki: 82 millió paraméter, természetes hangsúlyozás, tiszta szám- és terméknevek. A Piper a LibriTTS hanggal szorosan mögötte, érezhetően gyorsabban. A Bark a három legexpresszívebb, de túl lassú volt ehhez a volumenhez.

Mennyi idő egy teljes futás?

Körülbelül 35 perc mind a 95 cikkre egy Apple Silicon laptopon — nagyjából 4,5-szörös valós idő —, 2 GB alatti csúcsfoglalással. Egyetlen cikk körülbelül 20 másodperc alatt áll újra elő.

Miért AAC (M4A) és nem MP3 vagy Opus?

192 kbit/s-on a kodekkülönbségek beszédnél hallhatatlanok, szólt a döntés a csomagolásról: az AAC natívan fut mindenhol, Safari alatt is, a faststart a letöltés vége előtt megmutatja a hosszt, az Opus kisebb lenne, de a böngészőtámogatása egyenetlen WebM-en kívül.

Lézik német és magyar narráció is?

Egyelőre nem. A narráció minden cikk angol eredetét olvassa fel; maga a lejátszó felülete teljesen lokalizált angolul, németül és magyarul.

Pont erre van szükséged?

Írj a projektedről vagy a pozícióról – szívesen hallok felőled.