Pokušao sam da napravim prevodilac za blog. Na kraju sam napravio sistem provera.
Praktični postmortem o automatizaciji prevoda bloga na ruski i srpski — i zašto je pravi posao postao zaštita Markdown-a, fusnota, jezičkih pravila i stanja pregleda, umesto potrage za savršenim modelom.
Počeo sam sa jednostavnom željom: imao sam engleske članke na dzarlax.dev, i želeo sam rusku i srpsku verziju bez toga da svaki post pretvorim u projekat za vikend.
Na prvi pogled sve je delovalo kao nešto što može da se automatizuje. Izvorni tekst je već bio u Markdown formatu. Sajt je već imao rutiranje prilagođeno jeziku. Pišem u Obsidian vaultu. Paketi članaka su predvidivi:
articles/<slug>/index.md
articles/<slug>/index.ru.md
articles/<slug>/index.rs.md
Zato je početno pitanje zvučalo tehnički: koji model za prevođenje da koristim?
Ispostavilo se da je to bilo pogrešno pitanje. Pravo pitanje je bilo: koje delove procesa prevođenja treba automatizovati, a koje ostaviti uredniku?
Konačni odgovor nije bio herojski end-to-end prevodilac. Bio je to mnogo manje romantičan sistem: generator nacrta okružen determinističkim QA-om. Model prevodi. Alatni lanac štiti Markdown, linkove, fusnote, inline kod, jezička ograničenja, budžete izvršavanja i stanje pregleda. Finalni prolaz kroz tekst dešava se u ChatGPT aplikaciji i u mom editoru, jer tamo tekst zaista postaje dobar.
Ovako sam došao do toga.
Prevodilac za koji sam mislio da mi treba
Prva verzija bila je očigledna: poslati članak dovoljno snažnom lokalnom LLM-u sa glosarom i instrukcijom za ton teksta. Već sam imao Ollamu. Mogao sam da pokrenem aya-expanse:32b. Model je bio dovoljno velik da deluje uverljivo i dovoljno višejezičan da bude primamljiv.
Za ruski je često bio prihvatljiv. Za srpski na latinici — nije.
Neuspesi nisu bili dramatični u uobičajenom benchmark smislu. Model nije svaki put štampao besmislice. Davao je tekst koji je bio dovoljno blizu ispravnog da bude opasan: slovensku prozu sa ostacima češkog, poljskog, hrvatskog ili bosanskog na mestima gde mi je trebao srpski na latinici. Tečan čitalac bi to uhvatio. Ja ne znam srpski dovoljno dobro da sve to pouzdano primetim sam.
Zatim sam probao klasičnu dvofaznu šemu:
Koristiti OPUS-MT / Marian za sirov prevod.
Koristiti veći LLM za završno poliranje.
Na papiru je to delovalo privlačno. Specijalizovani NMT model trebalo bi da zna gramatiku; LLM bi trebalo da zna stil.
U praksi je granica bila mutna. NMT faza je kvarila nazive brendova i oštećivala inline Markdown. Faza poliranja ponekad je popravljala ton, ali je usput izmišljala strukturu. Jedno pokretanje je dodalo fantomski naslov najvišeg nivoa. Drugo je promenilo pravila interpunkcije za koja mu je eksplicitno rečeno da ih sačuva. LLM se nije ponašao kao mehanički lektor. Ponašao se kao pisac.
To je korisno kada vam treba pisac. Skupo je kada vam treba zakrpa.
Prvi koristan baseline bio je dosadan
TranslateGemma je promenila početnu tačku.
Pokretanje translategemma:12b nije bilo savršeno, ali je dobro radilo važne dosadne stvari: čuvalo je brendove, čuvalo inline kod, održavalo Markdown strukturu, izbegavalo srpsku salatu i završavalo brzo.
Arhiva ranih eksperimenata u _meta/translation-experiments na kraju je izgledala ovako:
| Pokretanje | Pipeline | Rezultat |
|---|---|---|
| Aya 32B | jednofazni lokalni LLM | Prihvatljivo za RU, nepouzdano za srpski na latinici |
| OPUS-MT -> Aya 32B | NMT plus poliranje | Bolja gramatika, ali oštećenja brendova i Markdown-a plus izmišljena struktura |
| TranslateGemma 12B | jednofazni model za prevođenje | Najbolji strukturni baseline, dovoljno brz za stvarnu upotrebu |
| TranslateGemma 12B -> Aya 32B | prevod plus poliranje | Regresija u strukturi i interpunkciji, nepouzdano ispravljanje termina iz glosara |
Iznenađujući rezultat nije bio to što je TranslateGemma bila najbolji pisac. Nije bila. Rezultat je bio to što je bila najbolji proizvođač nacrta. To je uži posao, i mnogo ga je lakše učiniti pouzdanim.
U tom trenutku sam i dalje mislio da ću možda dodati drugi model za poliranje. Benchmark je uglavnom ubio tu ideju.
Benchmark je odgovorio na drugo pitanje
Napravio sam šire poređenje na dva članka:
kratak post sa mnogo brendova:
sleeping-terminal-1000-dollars;duži post iz serije Health Dashboard sa mnogo termina:
part-4-readiness-stopped-being-one-number.
Kandidati su bili porodica TranslateGemma, Gemma 3, Aya 32B i NLLB. Probe su namerno bile neuglamurozne: naslovi, inline kod, brend tokeni, duga crta, formatiranje brojeva, curenje srpske ćirilice i očigledni ostaci češkog/poljskog/bosanskog/hrvatskog.
Već je sama tabela latencije bila dovoljna da resetuje očekivanja:
| Model | Kratak članak, RU+RS | Duži članak, RU+RS | Napomene |
| TranslateGemma 4B | 29s | 47s | Brz, kvalitet nacrta upotrebljiv za rad |
| TranslateGemma 12B | 54s | 89s | Najbolji podrazumevani kompromis |
| TranslateGemma 27B | 536s | ~870s | Ponekad bolji, ali mnogo sporiji |
| Gemma 3 12B | 47s | 116s | Dobra proza, veći rizik od problema sa formatiranjem |
| Gemma 3 27B | 521s | ~1700s na jednom RS delu | Spor, mešovita poboljšanja |
| Aya 32B | 863s | ~1880s | Spor i rizičan za strukturu |
| NLLB 1.3B | 2022s na kratkom pokretanju | diskvalifikovan | Nije pouzdano preživeo Markdown |
Bilo je primamljivih detalja. Gemma 3 je ponekad birala bolji srpski termin. TranslateGemma 27B je ponekad tražila manje ručnih ispravki po glosaru. Ali svako poboljšanje donosilo je novu regresiju: zapadni format valuta, engleske imenice sa srpskim sufiksima ili znatno duže vreme izvršavanja.
Najvažniji rezultat benchmarka bio je o tome šta ne treba automatizovati.
Glosar je imao uslovna pravila poput: ostaviti readiness na engleskom kada se odnosi na konkretan recovery score; u drugim slučajevima prevesti. Nijedan model to nije radio dosledno. Tabela u uporednoj belešci učinila je zaključak skoro neizbežnim: ovo nije problem modela za prevođenje. To je urednička odluka sa lokalnim kontekstom. Treba da je donese čovek.
Tako je nastalo prvo pravilo dizajna:
Ne trošiti budžet automatizacije na procene koje svejedno zahtevaju ljudsku proveru.
Zatim je masovno pokretanje puklo na staromodan način
Sledeća greška nije bila lingvistička. Bila je mehanička.
Masovno pokretanje preko osam delova Health Dashboard-a koristilo je limit izlaza koji je bio premali za duge članke. Model nije izbacio grešku. Samo je stao.
Šteta je imala vrlo specifičan obrazac:
definicije fusnota su nestajale, jer su bile na kraju izvornog teksta;
završni odeljci su nestajali u dužim člancima;
neki blokovi koda su nestajali;
srpska ćirilica je curila u izlaz na latinici.
Ovo je bilo korisno, jer je razjasnilo drugo pravilo dizajna:
Prevod ne postaje validan samo zato što izgleda kao normalna proza. Validan je tek nakon strukturnih provera.
To je gurnulo pipeline ka chunkovanju, zaštiti placeholdera, ponovnim pokušajima i budžetima.
Alat je počeo da štiti fenced code blocks, inline kod i reference na fusnote pre slanja chunkova modelu. Počeo je da beleži profile pokretanja. Dobio je čvrste limite poput TRANSLATION_MAX_MODEL_CALLS i TRANSLATION_MAX_CHUNK_RETRIES, jer je retry loop bez budžeta samo budući billing incident sa lepšim logovima.
Quality gate je takođe prestao da bude vibe check. Morao je da obori pokretanje ako placeholder nestane, definicija fusnote ostane bez reference ili srpska latinica sadrži ćirilicu van dozvoljenih citiranih primera.
GPT nano preko API-ja učinio je ekonomiju dosadnom
Posle eksperimenata sa lokalnim modelom, probao sam openai/gpt-4.1-nano preko OpenRouter-a.
Prvo pokretanje nalik production-u namerno nije bilo all-in. Obuhvatilo je sve članke osim već objavljenog posta o uspavanom terminalu, na ruskom i srpskom.
Sažetak je bio nezgrapan, ali informativan:
| Metrika | Rezultat |
| Pokušaja zadataka | 18 |
| Završenih zadataka | 9 |
| Neuspelih ili prekinutih zadataka | 9 |
| Posmatrani profilisani trošak | $0.048265 |
| Ukupno trajanje završenih zadataka | 30.8 min |
Važna stavka nije trošak, iako je bio dovoljno nizak da prestane da bude glavni problem. Važno je to što su neuspešni zadaci obično padali posle prevođenja chunkova, na lokalnom quality gate-u.
Većina ruskih zadataka je prošla. Srpski zadaci su se iznova zaustavljali na Serbian Latin output contains Cyrillic outside code blocks. Jedan ruski zadatak je pao zbog nedostajućeg placeholdera za fusnotu. To nisu bili razlozi da odustanem od hosted modela. To su bili razlozi da poboljšam sistem provera oko njega.
Sledeća pokretanja dodala su strože retry promptove, normalizaciju srpskog, ponovne pokušaje za konkretne placeholdere, precheck artefakte u _meta/translation-precheck i debug izlaz koji nije zatrpavao direktorijume članaka.
Preostalo srpsko strict-v2 pokretanje zatvorilo je nedostajuće delove:
| Članak | Jezik | Status | Sekunde | Najsporiji chunk |
| Deo 2 | RS | done | 72.9 | section, 11.7s |
| Deo 3 | RS | done | 66.8 | footnotes, 11.8s |
| Deo 5 | RS | done | 80.8 | footnotes, 12.5s |
| Deo 6 | RS | done | 112.6 | footnotes, 18.6s |
| Deo 7 | RS | done | 133.3 | section, 33.4s |
| Deo 8 | RS | done | 220.7 | section, 72.9s |
To je bio trenutak kada je izbor modela postao manje zanimljiv od sistema oko njega.
GPT nano je bio dovoljno jeftin. TranslateGemma je bila dovoljno lokalna. Nijedan od njih nije uklonio potrebu za završnom redakturom. Oba su postala prihvatljiva kao motori za nacrte, ako okolni alatni lanac može da mi kaže kada su polomili strukturu.
Nezgodan deo: aplikacija je i dalje pisala bolju prozu
Posle svega toga, završno poliranje članka i dalje je često bolje radilo u ChatGPT aplikaciji.
To može da zvuči kao neuspeh. Ja ne mislim tako.
Lokalni pipeline je pokušavao da radi dva posla odjednom:
da proizvede strukturno siguran prevod;
da proizvede finalnu prozu spremnu za objavu u mom stilu.
Prvi posao je inženjerski. Drugi je urednički.
Kada sam oba gurao u jedan automatizovani prolaz, dobijao sam loše kompromise. Jači rewrite modeli poboljšavali su neke rečenice, ali su oštećivali Markdown ili pomerali terminologiju. Modeli fokusirani na prevođenje čuvali su strukturu, ali su ponekad davali tekst koji se čitao kao korektan mašinski prevod. Deterministička skripta može da pronađe propuštenu fusnotu. Ne može da proceni da li ruski naslov zvuči kao fraza koju bi čovek zaista napisao.
Zato najproduktivniji workflow nije bio end-to-end automatizacija, već podela rada:
English canonical article
-> draft translation
-> structural QA
-> ChatGPT app editorial pass
-> post-edit structural QA
-> draft:false + mark-reviewed
Aplikaciji je dozvoljeno da bude urednik proze. Repozitorijum je odgovoran za to da se ta izmena može bezbedno zalepiti nazad.
Konačni alat je sistem provera
Trenutni blog-translator nije magični prevodilac. To je sistem provera sa oštrim granicama.
Radi sledeće stvari:
generiše nacrte prevoda za
index.ru.mdiindex.rs.md;štiti Markdown strukture pre poziva modela;
deli duge članke na chunkove i beleži profile po svakom chunku;
poštuje budžete ponovnih pokušaja i poziva modela;
arhivira pokretanja u
_meta/translation-runsi_meta/translation-experiments;prati stanje prevedeno/pregledano/zastarelo u
_meta/translation-registry.json;pokreće deterministički QA nad člancima i prevedenim stranicama serija.
Najnoviji deo je translation-qa.py. Namerno je dosadan. Proverava frontmatter, stanje draft kada se to zatraži, reference i definicije fusnota u odnosu na engleski izvor, Markdown URLs, raw URLs, code fences, pokvarene bold/code artefakte, curenje srpske ćirilice i git diff --check.
Takođe zna da članci i stranice serija imaju različite oblike:
python bin/translation-qa.py --vault /path/to/blog --slug part-8-asking-before-telling --lang rs
python bin/translation-qa.py --vault /path/to/blog --kind series --slug health-dashboard
python bin/translation-qa.py --vault /path/to/blog --kind all --expect-draft false
Ta poslednja komanda je važna, jer stranica koju sam skoro zaboravio nije bila članak. Bila je to prevedena stranica serije.
Šta bih ponovo automatizovao
Definitivno bih ponovo automatizovao nacrt.
Automatizovao bih zaštitu code blocks, zaštitu inline koda, provere fusnota, provere linkova, provere skripti, run manifests, ponovne pokušaje, sažetke troškova i stanje pregleda. Sve bih to zadržao.
Ne bih pokušavao da isti pipeline bude odgovoran za finalni glas.
Korisnija lekcija je uža: koristite modele tamo gde su jeftini i korisni, a zatim gradite determinističke sisteme oko režima otkaza koje modeli zaista imaju.
Za moj blog to znači da kanonski izvor ostaje na engleskom. Ruski i srpski počinju kao nacrti koje generiše model. Alatni lanac hvata strukturna oštećenja. ChatGPT aplikacija i ja se bavimo prozom. Zatim alatni lanac sve proverava još jednom pre nego što se prevod označi kao pregledan.
Manje je magično od prevodioca za koji sam mislio da mi treba.
I to je prva verzija kojoj zaista verujem.