Tag: Olaverse

  • PurpleMIST: decision models that speak eight African languages

    PurpleMIST: decision models that speak eight African languages

    Meet PurpleMIST-Flash: a 7.9B decision model that matches DeepSeek-V4-Pro, a model about 200 times its size,
    across eight African languages. Its probabilities are about five times closer to the truth.

    A decision model doesn’t write text. You give it a piece of state (a WhatsApp message, a support ticket, a
    call transcript, a form record) and a set of typed questions: which team should handle this?, is the customer
    asking for a refund?
    , how urgent is it? For every question it returns a probability for each option, in one
    forward pass, in milliseconds. It is the fastest way to triage, route, moderate and automate at scale. With
    TypeSafe’s Jev, more than a hundred open reproductions, and Ollama’s new /v1/systemone endpoint, it has become
    one of the most active corners of open AI.

    Until now, almost all of it worked only in English. Today we’re changing that with three releases:

    •PurpleMIST-Flash-1.0 (7.9B): our flagship decision model, for English and eight African languages.

    •PurpleMIST-Mini-1.0 (1.9B): the same interface in a model small enough for almost any GPU.

    •African Typed Decisions: to our knowledge, the first decision benchmark for African languages. It has 3,739 cases and 16,881 decisions in Amharic, Hausa, Igbo, Nigerian Pidgin, Somali, Swahili, Yorùbá and isiZulu, and an open leaderboard.

    Frontier accuracy at a fraction of the size

    On African Typed Decisions, PurpleMIST-Flash scores 0.668 accuracy, level with DeepSeek-V4-Pro (0.669), a
    1.6-trillion-parameter model. Where it pulls ahead is the part that matters for a decision model: how much you
    can trust each probability
    .

    ModelSizeEnglish8 African languagesKL ↓Brier ↓
    PurpleMIST-Flash-1.07.9B0.7200.6680.2140.115
    DeepSeek-V4-Pro (prompted)1.6T0.7200.6691.1250.199
    Qwen3.8-Flash (prompted)undisclosed0.7120.6430.8620.216
    OpenDecider-small4B0.6480.4770.3650.199
    laya-multilingual0.32B0.3480.3225.0530.545

    Other open decision models collapse outside English: OpenDecider-small loses 0.171 accuracy. PurpleMIST-Flash
    loses just 0.053.
     It leads every open decision model in every one of the eight languages.

    Strong in English too

    •LocalLLaMA/typed-decisions: 0.720 accuracy, zero-shot. Its probability quality, KL 0.189 and Brier 0.098, ranks 2nd among the published zero-shot models, ahead of Liquid AI d1 and TypeSafe Jev.

    •Jev Decision Index (37 public benchmarks, run with the maintainers’ own kit): ahead of 8 of the 13 models of its size. It is 4.8 points above their median, and above the median in language understanding (+6.4), knowledge and reasoning (+4.3), tools (+3.8) and retrieval (+1.4). We’ve submitted it for the index’s private evaluation.

    Probabilities you can act on

    A decision model is only as useful as its confidence. When it says 90%, it should be right about 90% of the time.
    Every PurpleMIST model is calibrated across domains: one temperature per question type, fitted on thousands of
    states it never saw in training, drawn evenly from eight different sources. The result is an expected
    calibration error of 0.049 across the eight African languages and 0.056 in English. So you can set a
    threshold once and trust it: auto-approve above 0.9, send to a human below.

    Fast at any length

    •Speed: a median of 63 ms per request on a single GPU, over 140,000 benchmark requests of every size.

    •Long inputs: up to 65,536 tokens per pass. Long contracts, transcripts and documents are read whole, never truncated.

    •Many questions: any number of them about a state share one pass, and any number of options per question works, because the model reads each option’s text rather than memorising labels.

    How it works

    PurpleMIST-Flash is built on the text model of Qwen3.5-9B with a purpose-built pointer head. The whole state
    and all its questions go in together, and each option is scored directly against the question it belongs to. It was trained on 300,000 real and synthetic decision states, including 86,134 decisions written natively in, or translated into, the eight African languages.

    Start in minutes

    import os, sys
    from huggingface_hub import hf_hub_download
    
    repo = "olaverse/PurpleMIST-Flash-1.0"
    sys.path.insert(0, os.path.dirname(hf_hub_download(repo, "purplemist.py")))
    from purplemist import Decider
    
    d = Decider.from_pretrained(repo, device="cuda")
    d.decide({"message": "Ẹ jọ̀ọ́, wọ́n ti yọ owó lẹ́ẹ̀mejì lórí káàdì mi. Mo fẹ́ kí ẹ dá owó mi padà."},
             {"team":   {"type": "choice", "instructions": "Which team should handle this?",
                         "criteria": {"billing": "payments and refunds", "delivery": "orders in transit"}},
              "refund": {"type": "noul", "instructions": "The customer is asking for money back."}})

    Or run it as a server. serve.py ships in each model repo and uses the same POST /v1/systemone request and answer format as Ollama’s decision endpoint, so code written for that API reads PurpleMIST’s answers the same way:

    hf download olaverse/PurpleMIST-Flash-1.0 serve.py purplemist.py --local-dir purplemist
    python purplemist/serve.py --model olaverse/PurpleMIST-Flash-1.0 --port 8000

    Flash runs on a single 24 GB GPU. PurpleMIST-Mini brings the same interface to 1.9B parameters. In English it beats models up to four times its size, including Bongard-mini (7.5B) and Jeff-Gemma4-E2B (4.6B), with a median latency of 46 ms. Both are open weights under Apache-2.0.

    Benchmark your model

    African Typed Decisions is open. Score your model on the all config, one request per case, and open a discussion with your numbers. We’ll add you to the leaderboard. If you build for Hausa, Yorùbá, Swahili or any of the other languages and want decision models that understand your users, we’d love to hear from you at olaverse.co.uk.

    Links: PurpleMIST-Flash-1.0 ·
    PurpleMIST-Mini-1.0 ·
    African Typed Decisions

    About the benchmark: references come from LLM teachers, and the African cases are machine-written and machine-checked. PurpleMIST was trained on separate data from the same pipeline. Full methodology is on the
    dataset card.

  • 608 languages in 37 MB: building African-first language identification

    608 languages in 37 MB: building African-first language identification

    Language identification sounds solved. It is the first step of almost every multilingual pipeline: before you
    can translate, filter a web crawl, route a support ticket or pick a speech model, you need to know what language
    the text is in. Open models like GlotLID and Meta’s fastText LID cover hundreds of languages, and on long,
    clean paragraphs they are very good.

    Real input is not long and clean. It is “Ẹ kú àárọ̀”, “how far”, a product review with an emoji, a WhatsApp
    message with no tone marks, a URL. And for African languages, the gaps are wide: Kinyarwanda, Xhosa, Wolof and
    Lingala are routinely confused with their neighbours, and Nigerian Pidgin is often read as English.

    So we built two models for 608 languages, African-first, designed for the input people actually type:

    •lid-lite-608: a 37 MB fastText model, about 6,800 texts per second on one CPU thread.

    •lid-neural-608: a 140M-parameter ModernBERT classifier fine-tuned from mmBERT-small, the most accurate model we tested on short and conversational text.

    Both are Apache-2.0 and trained only on data that allows commercial use.

    The results

    We scored every model on the same data, mapping each model’s language codes to ours so that naming
    differences never count as errors.

    lid-neural-608 vs lid-lite-608, GlotLID and Meta
    lid-neural-608lid-lite-608GlotLID v3Meta fastText LID-218
    Size140M params37 MB1,687 MB1,176 MB
    Held-out test, accuracy (240k samples, 608 languages)0.8670.8580.7830.280
    Short input (1-5 words), web-weighted, traffic mode0.9060.8290.8080.764
    Everyday phrases (“good morning”, “sign in”, …)0.9430.8380.8570.867
    Tatoeba (conversational, 34k)0.9300.9220.8680.557
    UDHR-LID (291 languages)0.9140.9100.9080.567
    FLORES+ devtest (202 languages)0.9420.9340.9570.874

    On the 20 major African languages we track, lid-lite-608 averages 0.839 F1 against GlotLID’s 0.757, with
    the biggest gains where it matters most: Kinyarwanda (0.758 vs 0.371), Xhosa (0.840 vs 0.602), Wolof (0.809 vs
    0.600) and Lingala (0.814 vs 0.642).

    African languages F1

    FLORES+ is the one benchmark where GlotLID leads. Its sentences are long, carefully translated news and wiki
    text, the kind of input every model handles well. The gap opens on short and conversational text: on the
    first 1-5 words of the same FLORES+ sentences, both of our models are ahead.

    One model, two ways to read it

    The most useful thing we added is not a bigger model but a second way of reading the one we have.

    A language identifier trained on balanced data treats all 608 languages as equally likely. That is exactly
    right when you are mining a web crawl for low-resource text: you want every Lingala sentence, and you want it
    labelled Lingala even when it is short. But it is wrong for a chat box. If a user types “Bonjour mon ami”,
    the overwhelmingly likely answer is French, not one of the dozens of small languages that share the same
    words.

    So both models ship with two modes:

    •coverage (default): every language equally likely. Best per-language accuracy, for corpus building.

    •traffic: each language’s score is shifted by how common it is in real text. Formally, we add 0.2 × log(real-world share / training share) to each language’s log-probability, using frequencies we measured on the web. Short and ambiguous input then leans towards the languages that actually dominate traffic.

    from olaverse import LIDLite608
    
    LIDLite608().predict("Bonjour mon ami")                  # 'dhv_Latn'  (coverage)
    LIDLite608(mode="traffic").predict("Bonjour mon ami")    # 'fra_Latn'  (traffic)

    Traffic mode adds 6.6 points of accuracy on 1-5 word input for lid-lite-608 and 5.5 for lid-neural-608, and it
    costs nothing: the same weights, one extra vector.

    The bug that taught us the most: “Good morning” was not English

    An early version of lid-lite scored well on every benchmark we had, and then we typed “Good morning” into it.
    It did not come back as English, and neither did most other everyday English phrases: precision for English on
    short phrases was 0.19.

    The cause was in the data, not the model. Web text in small languages is full of English: menus, cookie
    banners, “Read more”, “Sign in”, quoted headlines. Each of those English fragments was labelled with the
    language of the page it came from, so the model learned that short English phrases are evidence for hundreds
    of other languages.

    The fix had four parts:

    1.A foreign-text filter that removes boilerplate and lines in a different language from their page label before training.

    2.Rebalancing the major languages, so English, French, Spanish and the other high-traffic languages were not drowned out by the long tail.

    3.Tatoeba sentences, about 1.6M samples of short, conversational, human-written text in hundreds of languages.

    4.A probe test of 107 everyday phrases in 11 common languages (greetings, “sign in”, “add to cart”), removed from the training data and checked on every build.

    The probe test now sits next to the benchmarks in every evaluation: lid-neural-608 gets 0.943 of the phrases
    right in traffic mode, the best of all the models we tested. The lesson generalises: aggregate benchmarks
    can hide a failure that every user hits in their first minute.
     Build a small test of the obvious cases and
    never ship without it.

    Built for messy input

    Both models were trained on input that looks like real input:

    •Typos, missing diacritics, case changes and emoji were added to a share of the training samples, so “se daadaa ni” is recognised as Yorùbá as reliably as “ṣé dáadáa ni”.

    •Four length buckets per language, from 1-5 words to full paragraphs, because a model trained only on paragraphs fails on queries.

    •A noise class (zxx_Zxxx) for numbers, URLs, code and other non-language text, so a stray order number is not reported as a language. lid-lite-608 reaches 0.961 F1 on it, against 0.620 for GlotLID.

    •Data hygiene: we removed religious text (which dominates many low-resource corpora and skews the vocabulary), text in the wrong script for its label, and mislabelled documents.

    The held-out test was built from websites never seen in training, 100 samples per length bucket per
    language, so the scores reflect new sources rather than memorised ones.

    Which one to use

    lid-lite-608lid-neural-608
    Size37 MB140M parameters (~560 MB)
    Speed~6,800 texts/s on one CPU thread~1,100 texts/s on an A100
    Best forbulk filtering, CPU and edge deploymentuser input, short text, highest accuracy

    Or use both: run lid-lite-608 as a fast first pass over everything, and send only short or low-confidence
    inputs to lid-neural-608.

    Try it

    pip install "olaverse[lid]"            # lid-lite-608
    pip install "olaverse[deeplearning]"   # lid-neural-608
    from olaverse import LIDLite608, LIDNeural608
    
    LIDLite608().predict("Ẹ kú àárọ̀, ṣé dáadáa ni?")     # 'yor_Latn'
    
    lid = LIDNeural608(mode="traffic")
    lid.predict_batch(["Good morning", "Mo fẹ́ lọ sí ọjà", "Habari za asubuhi"])
    # ['eng_Latn', 'yor_Latn', 'swh_Latn']

    Both models also work with plain fasttext and transformers; the model cards have the code. Labels are ISO
    639-3 plus script (yor_Latn, srp_Cyrl), so the same language in two scripts is never confused.

    •Models: olaverse/lid-lite-608, olaverse/lid-neural-608

    •Library: olaverse on PyPI · docs

    If you work with a language we get wrong, we would like to hear about it: open a discussion on either model
    page with a few example sentences.

  • diacnet-2.0: how we fixed Yorùbá, added Arabic, and removed 90% of our Hausa errors with one function

    diacnet-2.0: how we fixed Yorùbá, added Arabic, and removed 90% of our Hausa errors with one function

    diacnet restores diacritics by rewriting text: a byte-level
    ByT5 model reads <yor> se eranko naa si gbo o? and writes ṣé ẹranko náà sì gbọ́ ọ?. Today we are releasing
    diacnet-2.0 (582M) and
    diacnet-mini-2.0 (300M): 11 languages including Arabic,
    Yorùbá error cut by 60%, and, with one small post-processing step, lower error than our character tagger
    diactag-2.0 on 8 of 10 languages.

    This post is mostly about the three things that went wrong on the way, because each one taught us something.

    1. More data made Yorùbá worse

    diacnet-1.1 was trained on far more Yorùbá text than 1.0, and it got worse at Yorùbá: its diacritic error rate
    rose from 0.155 to 0.201. The cause was measurable. Yorùbá is written with tone marks on almost every syllable,
    but most Yorùbá on the web leaves them out. 1.0 had trained on about 2,000 carefully marked passages
    (average mark density 0.565); 1.1’s much larger web corpus averaged 0.223. The model learned what it saw:
    leave the tones off.

    For 2.0 we attacked the data, not the model:

    •Filter the weak rows. Yorùbá training pairs were kept only if their mark density was at least 0.6× that of well-marked text.

    •Teacher-marked text. We took real Yorùbá, Igbo, Hausa and Arabic news and web text and had a large language model, DeepSeek-V4-Pro, re-mark it in full. A sentence was kept only if the teacher changed marks and nothing else. Where it lightly normalised a sentence (splitting a contraction, say), its version became the target and the input was rebuilt from it, so input and target always match letter for letter.

    •Meaning-dependent words. We generated sentences built around words whose marks depend on meaning (Vietnamese ranh: rảnh “free”, rành “skilled”; Yorùbá ogun: ogun “war”, ogún “twenty”), with an English gloss for each sense.

    The synthetic set came to about 37k pairs. Before choosing the teacher we
    compared several on the same sentences; the gap between two strong teachers was itself informative, at 7.4% of
    Yorùbá letters, 3.4% Igbo and 1.0% Hausa, a useful reminder that even the references a model learns from are
    not perfectly agreed upon.

    2. Our first base model barely learned anything new

    We trained the small model first, from google/byt5-small, and it worked. For the larger model we tried to save
    compute by continuing from diacnet-1.1 with a low learning rate (5e-5). It improved Yorùbá a little and
    learned almost no Arabic: a 0.34 error rate, three times worse than the small model.

    The fix was to stop being clever: we retrained base from google/byt5-base with the small model’s recipe
    (learning rate 2e-4, 2.5M real pairs plus the teacher set, one epoch). The retrained base is now the best model
    in the family on almost every language. Continued training at a cautious learning rate can preserve a model
    so well that it cannot learn a new script.

    3. The model was “wrong” because it was fixing typos

    When we scored diacnet the same strict way we score diactag, Igbo and Hausa looked terrible: about 0.09 and
    0.05 error rate against diactag’s 0.016 and 0.004. Reading the outputs showed why. Most errors were not wrong
    marks. In 4-7% of sentences diacnet had changed a letter, and strict scoring counts every markable letter in
    such a sentence as wrong.

    This was a feature misfiring. diacnet’s training inputs include deliberate typos (7% character noise), so it
    learned to fix spelling as well as add marks. On a benchmark, and in many real pipelines, that is unwanted.

    The fix is a tiny function that runs after generation: line the output up with the input, keep the model’s
    marks on every letter it left unchanged, and fall back to the input letter anywhere it changed, dropped or
    added one. The result is guaranteed to be your text with marks added.

    import difflib, unicodedata as ud
    
    _FOLD = str.maketrans("ɓɗƙƴđıłƁƊƘƳĐŁ", "bdkydilBDKYDL")   # letters with no combining form
    _LETTER = {"ٓ", "ٔ", "ٕ"}                  # Arabic madda / hamza are spelling
    
    def _units(text):
        units = []
        for c in ud.normalize("NFD", text):
            if units and ud.combining(c):
                if c in _LETTER:
                    units[-1][0] += c
                units[-1][1] += c
            else:
                units.append([c, c])
        return [(ud.normalize("NFC", b).translate(_FOLD), ud.normalize("NFC", f)) for b, f in units]
    
    def align(source, output):
        src, out = _units(source), _units(output)
        res = [f for _, f in src]
        sm = difflib.SequenceMatcher(None, [b for b, _ in src], [b for b, _ in out], autojunk=False)
        for a, b, n in sm.get_matching_blocks():
            res[a:a + n] = [f for _, f in out[b:b + n]]
        return "".join(res)
    diacnet-2.0: raw output vs aligned output
    diacnet-2.0RawAligned
    Igbo0.0930.018
    Hausa0.0480.004
    Yorùbá0.0780.070
    Arabic news (WikiNews)0.3440.154

    Hausa error falls by 90%, Igbo by 80%, and Arabic news by more than half, at no training cost. Users who do want
    typo correction can still ask for the raw output.

    Results

    Scored with exactly the code and letter definitions used for diactag-2.0, on 1,000 sentences per language
    from diacbench:

    diacbench: diacnet and diactag
    diacnet-1.1diacnet-mini-2.0diacnet-2.0diactag-2.0
    Yorùbá0.17430.08150.06980.0787
    Igbo0.02190.02020.01850.0162
    Hausa0.00630.00440.00410.0044
    Vietnamese0.03160.02070.01430.0141
    Polish0.00550.00240.00140.0035
    Spanish0.00640.00310.00270.0038
    French0.00280.00130.00120.0017
    Mean, 10 languages0.02610.01410.01170.0130

    (Aligned output, diacbench v1 references; full tables with Turkish, Portuguese and Italian on the model cards.)

    The two approaches now complement each other. diacnet-2.0 leads on Yorùbá and the European languages and
    offers meaning hints and typo correction; diactag-2.0 leads on Igbo, Vietnamese and modern standard Arabic, is
    15 times smaller, and runs in milliseconds on a CPU.

    Arabic is the newest and weakest language for diacnet: error 0.096 on Classical Arabic and 0.154 on modern
    news, behind diactag-2.0. Unlike most open Arabic diacritizers, diacnet was not trained on the Tashkeela
    corpus that the Classical test sets come from, and it offers the same two modes: full, and without
    grammatical case endings.

    Meaning hints: a nudge, not a switch

    You can tell diacnet what an ambiguous word means:

    d.restore("Chi ay chi that su ranh vao nhung buoi toi sau khi da cho con ngu say.",
              lang="vie", hints={"ranh": "free (time)"})
    # 'Chị ấy chỉ thật sự rảnh vào những buổi tối sau khi đã cho con ngủ say.'
    # without the hint: '... rành ...' ("skilled")

    We tested this properly on 580 held-out sentences whose ambiguous spellings never appeared in training. Hints
    lift Vietnamese accuracy on the ambiguous word from 0.63 to 0.90 and Turkish from 0.75 to 0.84. We also tried
    the opposite: give the hint for the word’s other meaning and see whether the output follows it. Usually it
    does not: diacnet switches between a quarter and a half of the time and otherwise trusts the sentence. Hints help when the
    sentence leaves the meaning open; they will not override a sentence that makes it clear. We think that is the
    right behaviour, but it is worth knowing.

    Also in 2.0

    •<auto>: leave the language out and the model infers it, at a cost of at most 0.0005 in error rate.

    •Partial input: marks already in the text are kept (99.9-100% in our tests) and the rest are filled in, so Yorùbá typed with underdots but no tones comes back fully marked.

    •Two sizes: diacnet-2.0 is more accurate everywhere (17% lower mean error); diacnet-mini-2.0 is half the size and within a hair of it on Igbo, Hausa, French, Spanish and Italian.

    Try it

    pip install "olaverse[deeplearning]"
    from olaverse.nlp import Diacritizer
    
    d = Diacritizer(model="diacnet-2.0", lang="yor", device="cuda")   # or "cpu" / "mps"
    d.restore("O so fun ara re pe oun ko ni isoro kankan.")
    # 'Ó sọ fún ara rẹ̀ pé òun kò ní ìṣòro kankan.'
    
    d.restore("وهذا قول مرغوب عنه .", lang="ara", case_endings=False)
    # 'وَهَذَا قَوْل مَرْغُوب عَنْه .'

    The library aligns the output by default (aligned=False returns the raw text) and handles chunking and
    batching. The model cards also show plain transformers usage with the alignment function.

    •Models: olaverse/diacnet-2.0, olaverse/diacnet-mini-2.0

    •Character tagger: olaverse/diactag-2.0

    •Benchmark: olaverse/diacbench

    •Library: olaverse

  • A diacritic model that cannot corrupt your text

    A diacritic model that cannot corrupt your text


    Diacritic restoration is easy to state. Given se eranko naa si gbo o, produce ṣé ẹranko náà sì gbọ́ ọ. Every character keeps its identity; some acquire marks.

    The standard approach treats it as translation. Bare text in, marked text out, generated token by token. That is what diacnet did — ByT5 fine-tuned on 7 million sentences.

    It works, mostly. But “mostly” hides something. On Hausa, only 94.7% of diacnet-1.1 outputs still stripped back to their input. The other 5.3% weren’t mis-accented — they were different text. Words dropped. Clauses rewritten. Punctuation invented. A model asked to add accents had quietly decided to edit.

    That is not a tuning problem. A generative decoder can emit any token at any position, so nothing in the architecture prevents it. You can penalise it in training and it gets rarer, but it never becomes impossible — and “rare” is not a property you can put in a contract.

    We wanted one where it is impossible.


    Reframing: classify, don’t generate

    The task has a hard structural constraint that generation throws away. The output has exactly as many characters as the input, in the same order, with the same base letters. Only the marks change.

    So don’t generate. Classify. For each character, predict which diacritics it carries, and copy the base character through.

     input      s      e      r      a      n      k      o
                │      │      │      │      │      │      │
                ▼      ▼      ▼      ▼      ▼      ▼      ▼
         ╔═══════════════════════════════════════════════════╗
         ║          character transformer encoder            ║
         ╚═══════════════════════════════════════════════════╝
                │      │      │      │      │      │      │
       SHAPE   DOT     ·      ·      ·      ·      ·      ·
       TONE     ·      ·      ·    ACUTE    ·      ·      ·
                │      │      │      │      │      │      │
                ▼      ▼      ▼      ▼      ▼      ▼      ▼
     output     ṣ      e      r      á      n      k      o
                └──────┴──────┴──────┴──────┴──────┴──────┘
                  base letters COPIED, never predicted
    

    The base character is not an output of the network. There is no mechanism by which the model can change it, so strip(output) == strip(input) holds by construction — for a trained model, an untrained one, or the int8 export.

    We test exactly that: the invariant suite runs against a randomly initialised model. If the guarantee only held for a good model, it would not be a guarantee.


    The model

    Twelve layers, 512 hidden, 37.6M parameters — against 580M for the seq2seq model it replaces.

          graphemes  [B,T]              language id  [B]
               │                              │
               ▼                              ▼
        ┌─────────────┐                ┌─────────────┐
        │  char emb   │                │  lang emb   │   11th slot =
        │   403×512   │                │    11×512   │   "unknown language"
        └──────┬──────┘                └──────┬──────┘
               │                              │
               └──────────────► + ◄───────────┘   additive: no <yor> token,
                                │                 no position spent
                                ▼
            ╔═══════════════════════════════════════╗
            ║          × 12  encoder block          ║
            ║  ┌─────────────────────────────────┐  ║   bidirectional —
            ║  │ RMSNorm → MHA(8 heads, RoPE)    │  ║   a diacritic depends on
            ║  │ + residual                      │  ║   what follows as much
            ║  │ RMSNorm → SwiGLU 512→1344→512   │  ║   as what precedes
            ║  │ + residual                      │  ║
            ║  └─────────────────────────────────┘  ║
            ╚═══════════════════╤═══════════════════╝
                                │  H  [B, T, 512]
            ┌──────────┬────────┴────────┬───────────┐
            ▼          ▼                 ▼           ▼
       ┌─────────┐┌─────────┐      ┌──────────┐┌──────────┐
       │  SHAPE  ││  TONE   │      │   MLM    ││   LID    │
       │ 512→15  ││ 512→8   │      │ 512→403  ││ 512→10   │
       │per char ││per char │      │ tied emb ││mean-pool │
       └────┬────┘└────┬────┘      └────┬─────┘└────┬─────┘
            │          │                │           │
            └────┬─────┘           training only  language
                 ▼
          legality mask  →  argmax  →  compose(base, shape, tone)
    

    Where the parameters go:

    componentshapeparams
    character embedding403 × 512206k
    language embedding11 × 5126k
    12 × encoder blockattn 4·512² + FFN 3·512·134437.4M
    SHAPE head512 × 157.7k
    TONE head512 × 84.1k
    LID head512 × 105.1k
    MLM headtied to embedding0

    The task heads together are 12k parameters — 0.03% of the model. Nearly everything is shared encoder, which is why ten languages cost roughly the same as one.

    loss = 1.0·CE(shape) + 1.0·CE(tone) + 0.3·CE(masked_char) + 0.05·CE(language)
    

    SHAPE and TONE are the task. MLM is auxiliary and dropped at inference — it forces lexical knowledge into the encoder, which is what tone disambiguation needs. Its target is always the fully marked character; masking ú and accepting u would teach the encoder that bare vowels are valid Yorùbá, the exact failure being fixed. LID is nearly free and makes the API restore(text) instead of restore(text, lang).

    Three choices that carry weight:

    RoPE, not learned positions. Documents are processed with a sliding window, so the model sees offsets at inference it never saw in training. Learned absolute positions generalise badly to that; rotary ones don’t.

    Language as an additive embedding. No <yor> prefix token — nothing to confuse with content, no position spent. During training the language embedding is dropped 12% of the time, giving a genuine “unknown language” mode, so being wrong about the language degrades gracefully instead of catastrophically.

    Trained from scratch. We considered starting from an existing encoder, but its tokenizer was trained on diacritized text while our inputs are stripped — every subword boundary would be wrong in the direction that matters most.


    Splitting shape from tone

    The naive version assigns each character one label from a flat set: acute, grave, underdot, circumflex-plus-acute. That works for Spanish and falls apart on Vietnamese, where stacked marks produce ~60 classes, most of them rare.

    Instead, factorize:

       ẹ́   →   e  +  DOT_BELOW   +  acute        (Yorùbá)
       ộ   →   o  +  CIRCUMFLEX  +  dot-below    (Vietnamese)
       ữ   →   u  +  HORN        +  tilde        (Vietnamese)
       ń   →   n  +     —        +  acute        (Polish)
    

    SHAPE is marks that change letter identity. TONE is the mark that changes pitch or stress. Two heads, two label sets, two loss terms.

    The subtlety — and the reason it works — is that which marks belong to which slot is language-dependent:

       U+0323  dot below      →  SHAPE in Yorùbá     (ẹ and e are different letters)
                              →  TONE  in Vietnamese (nặng, one of six tones)
    
       U+0303  tilde          →  SHAPE in Spanish    (ñ is a letter)
                              →  TONE  in Vietnamese (ngã)
    

    A flat label set cannot express that. It sees ọ and assigns one class, blind to the fact that the same codepoint plays different grammatical roles in the two languages.

    Factorized, Vietnamese collapses to 5 shapes × 6 tones, every component seen thousands of times. Across all ten languages the final space is 15 shapes and 8 tones.

    The bigger payoff is diagnostic. Yorùbá’s dominant failure is tone direction — the model knows a mark belongs there and picks the wrong one, writing pátápátá for pàtàpàtà. In a flat label space that is indistinguishable from any other error. Split, it gets its own head, its own confidence and its own metric:

    checkpointYorùbá DERtone portion
    10k steps0.130687%
    30k steps0.113881%
    120k steps0.083683%

    Four fifths of the remaining error, at every stage of training, is one failure mode. Aggregate DER would have shown a number going down and told us nothing about what to do next.


    Graphemes, not codepoints

    Yorùbá stacks marks: ọ̀ is o with an underdot and a grave. Unicode has no precomposed codepoint for that combination, so even after NFC normalisation it stays two codepoints. len("ọ̀rọ̀") is 5, not 3.

    Iterating over the string misaligns every label after the first stacked character. Everything here operates on graphemes — a base codepoint plus any combining marks that follow it. A small detail that silently determines whether the hardest language in the set trains at all.


    What the corpus was teaching us

    This is the finding that changed the result more than any architectural decision.

    Before training, we plotted diacritic density — the fraction of eligible characters actually carrying a mark — per language. Nine of ten gave a single hump. Yorùbá gave two:

      count
        │        ▁▄██▇▅▃▂▁
        │       ▂████████▃                       ▁▂▃▃▂▁
        │      ▃██████████▅▂                   ▁▃██████▃▁
        └──────┴───────────┴──────────────────┴──────────┴────► density
              0.10                                0.43
            89% of sentences                 11% of sentences
            UNDER-MARKED                        correct
    

    Correctly marked Yorùbá lands around 0.30–0.45 — nearly half its characters are vowels, most carry a high or low tone, plus underdots on ẹ, ọ and ṣ.

    Which means the large mode is the broken one. Eighty-nine per cent of our Yorùbá training data was under-marked: real words with their tone marks simply omitted. And we were using it as ground truth. Every one of those sentences was teaching the model that bare vowels are correct.

    That is diacnet-1.1‘s documented failure, sitting in a histogram. It also explains something we had noticed and shrugged at: diacnet-1.1 is worse than 1.0 on all three African languages. More data, trained longer, learned to omit tone more confidently.

    The fix is not to delete the data

    The obvious move is to drop the low-density mode. We didn’t, because under-marked Yorùbá is still Yorùbá — the words are real, the morphology is real. It is excellent data for learning the language. It is poisonous data for learning tone marks.

    Since we already had two objectives on one encoder, we could separate them. Sentences below the detected threshold keep their masked-character targets and contribute no diacritic supervision at all:

       density 0.458   ABOVE floor  →  23 diacritic labels,  8 MLM labels
       density 0.000   BELOW floor  →   0 diacritic labels,  8 MLM labels
                                        └── still teaches vocabulary
    

    The MLM head trains on all 1.9M Yorùbá sentences. The tagger heads train on the clean ~205k. Same corpus, two different notions of what counts as a label.

    Detecting it without eyeballing a threshold

    We wanted this automatic. The first attempt used Otsu’s method, the standard image-binarisation algorithm — and it reported every language as bimodal.

    Otsu returns a split for any distribution. On a single right-skewed hump — which is what density distributions look like — that split lands near the left edge, where “the peak below the cut” is two nearly-empty bins and every statistic derived from it is noise. Our test fixtures used symmetric Gaussians, which hid the failure completely.

    The fix was to find peaks directly and measure how deep the trough between them falls, requiring both sides to hold at least 5% of the mass so a tail cannot masquerade as a mode. Measured that way the answer is not close:

       yor   ████████████████████████  0.55   BIMODAL
       pol   ████                      0.10
       tur   ███                       0.07
       ita   ▊                         0.02
       ibo                             0.00
    

    Igbo and Hausa are correctly left alone, and that matters: standard Igbo orthography omits tone by convention, and Hausa rarely writes it at all. Their low density is a property of the writing system, not a defect. No threshold should touch them.


    What falls out of the architecture

    Per-character classification gives you several things that are awkward or impossible in a generative setup.

    Automatic language detection. A fourth head classifies the language from the mean-pooled encoder state. It costs almost nothing — DER 0.0132 with the language supplied, 0.0133 with it detected — and most integrations don’t reliably know the input language.

    Per-character confidence. The joint shape × tone distribution gives a calibrated probability per character (temperature-fitted on validation, T = 1.14). Threshold it, and characters below are left exactly as the user typed them:

    thresholdcoverageDER on committed
    0.00100.0%0.0132
    0.9097.1%0.0039
    0.9991.9%0.0008

    At 0.90: 97% of characters restored at 99.6% accuracy, the rest flagged. This is the number to put in front of a customer, because it answers the question they have. A wrong tone mark changes meaning; a missing one is merely incomplete. The threshold is per-request, so a CMS pre-fill and a legal pipeline sit at different points on the same curve.

    Legality masking. For each (language, base character) we record which (shape, tone) pairs were ever observed and mask the rest to −∞ at decode time. No acute on Turkish vowels, no Vietnamese horn on Polish. An interesting result: turning it off changes DER by zero to four decimal places. The model learned the orthographies well enough that the constraint never binds. We keep it as a guarantee for out-of-distribution input — it costs nothing.

    Protected spans. URLs, emails, @handles, inline code and CONSTANT_NAMES pass through untouched, and were excluded from the training loss. An accent inside a URL is never correct, and it is the error a reader notices first.

    Documents, not sentences. No sentence splitting. Overlapping windows are planned across the input and only the centre of each is trusted, so every character is predicted with context on both sides. The window plan is proved to partition the input exactly — 500 randomised cases in CI.


    Results

    diacbench, 1000 sentences per language.

    langDERshapetoneexactcompliance
    ita0.00020.00000.00020.9911.0000
    fra0.00120.00030.00090.9521.0000
    tur0.00160.00160.00000.9611.0000
    por0.00190.00060.00140.9251.0000
    pol0.00220.00180.00030.9351.0000
    spa0.00220.00010.00210.9171.0000
    hau0.00410.00400.00010.7411.0000
    ibo0.01220.01100.00130.4831.0000
    vie0.01640.00730.01240.6501.0000
    yor0.08360.02030.06950.0841.0000
    all0.01320.00550.00860.7641.0000

    Yorùbá 0.2006 → 0.0836, a 58% reduction. Hausa 0.0593 → 0.0041, 93% — though much of the Hausa gain is the compliance guarantee rather than better modelling, since a large share of the old error was corruption rather than wrong accents.

    37.6M parameters against 580M.

    Against frontier LLMs

    300 sentences per language, matching how the baselines were run.

    langdiactag-1.0Claude Sonnet 4.5GPT-4o-mini
    yor0.09330.19130.2811
    ibo0.01100.04270.1277
    hau0.00410.01780.1432
    vie0.01660.01070.0399
    fra0.00140.00520.0023

    Best on 7 of 10. Vietnamese and Portuguese genuinely lose.

    Those LLM numbers deserve a footnote, because they are the charitable ones. They come from a harness that checks whether the output still strips back to the input and throws it away when it doesn’t. Raw, against the floor of copying the input unchanged:

    langcopy-input floorClaude, raw
    hau0.02360.3509
    ibo0.11710.2261
    fra0.04710.1171

    On three languages, a frontier model makes the text worse than doing nothing at all. Hausa is already 97.6% correct if you leave it alone; Claude returns it 35% wrong.

    That fallback harness is our architecture, reimplemented externally and imperfectly. The difference is that here there is nothing to discard.


    Deployment

    backendchars/sp50sizeDER
    PyTorch CPU105591 ms150 MB0.0105
    ONNX fp32202286 ms150.9 MB0.0105
    ONNX int8244200 ms38.3 MB0.0108

    Three times faster and four times smaller for +0.03pp DER. Compliance stays 1.0000 under quantisation — the guarantee is architectural, not a property of a particular numeric precision.

    A 38MB artifact at 244 characters per second on one CPU core takes the GPU off the serving bill entirely.

    One honest caveat: p95 is 788–1099 ms across all backends while p50 varies 3×. The tail is length-driven — long sentences need more windows — not backend-driven. Quantisation doesn’t fix it.


    Reproducing it

    pip install olaverse[deeplearning]
    
    from olaverse.nlp import Diacritizer
    
    d = Diacritizer(model="diactag-1.0", lang="yo")
    d.restore("se eranko naa si gbo o?")
    # → 'ṣé ẹranko náà sì gbọ́ ọ?'
    

    Data. 7.0M sentences from a web + Wikipedia corpus, plus 131k from a Q&A passage set — the only non-encyclopedic register in the mix — gated on diacritic density and deduplicated against the eval splits by stripped-form hash.

    Two filters worth copying if you build something similar. Script filtering: all ten languages are Latin, but Hausa Wikipedia carries Ajami and Yorùbá articles carry Korean and Devanagari names; those contribute nothing but identity labels. Cross-language filtering: a Vietnamese name inside a Turkish sentence factorizes into composite classes that exist in no language, because Turkish declares no tone slot for those marks to occupy. Without both, the label space inflates from 15 shape classes to 99, most of them junk competing in the same softmax.

    Training. AdamW, lr 3e-4 with 2000 warmup steps and cosine decay to 5%, weight_decay 0.05, batch 128 × 2 accumulation = 256, seq len 256, bf16, 120k steps. About 13 hours on a single A100.

    Language balancing. Temperature sampling at α=0.5. At α=1 Yorùbá and Igbo would be 54% of the mix; at α=0 Hausa’s 400k sentences get seen 5× per Yorùbá epoch and overfit.

    Augmentation. partial_mark_prob=0.30 with keep range [0.05, 0.9], case perturbation (2% upper, 3% lower, 1% title), 3% protected-span injection, mlm_prob=0.15, lang_dropout=0.12.

    Evaluation. Report shape_DER and tone_DER separately, and compliance as a hard assertion rather than a metric. If compliance is not exactly 1.0, the architecture is broken and nothing else in the table means anything.


    What it still gets wrong

    Yorùbá. DER 0.0836, 83% of it tone direction. Sentence-level exact match is 0.084 — 92 of every 100 Yorùbá sentences contain at least one wrong mark. A harsher framing than DER, and a truer one.

    Igbo and Hausa tone numbers are not achievements. tone_DER of 0.0013 and 0.0001 looks superb and means little, because those orthographies barely write tone. Nothing to learn, nothing to get wrong.

    No typo correction. The architecture cannot insert or delete characters, so it cannot fix Ile → Ilé and teh → the in one pass. That is the price of the guarantee, and we think it is the right trade — a model that reliably does one thing beats one that sometimes does two. The per-character confidence is a natural trigger for a separate corrector.

    Dense input degrades. The Polish pangram Zażółć gęślą jaźń has density 0.565 against a Polish median of 0.068 — nine times denser than real Polish — and the model misses six characters, despite a Polish DER of 0.0022.

    Some errors are irreducible. Viaggio and Viaggiò are both valid Italian and the stripped form contains no information distinguishing them. Every language has a nonzero floor.


    Three things worth taking away

    Plot your data before you train. The bimodal Yorùbá distribution took ten minutes to find and changed the outcome more than any modelling decision. We nearly skipped it.

    Make your metric decompose along your failure modes. Aggregate DER would have gone down and taught us nothing. Splitting shape from tone told us, at every checkpoint, that four fifths of the problem was one thing — and that the other nine languages had already converged.

    Put your guarantees in the architecture, not the loss function. A constraint the model can violate is one you have to monitor, apologise for, and wrap in a fallback. A constraint it cannot violate is one you can sell.


    Weights, ONNX exports and label space: olaverse/diactag-1.0. Documentation: olaverse-labs.github.io/olaverse/models/diactag. Apache 2.0.

  • Introducing DiacNet-1.1: Multilingual Diacritic Restoration for 10 Languages

    Introducing DiacNet-1.1: Multilingual Diacritic Restoration for 10 Languages


    Overview

    Diacritics—such as accents, tone marks, and special characters—are essential for clarity, pronunciation, and meaning in many languages. Yet, they are often stripped away in digital text due to technical limitations, user input habits, or data processing errors. This loss can lead to ambiguity, mispronunciation, and even miscommunication.

    Today, we’re excited to introduce DiacNet-1.1, a state-of-the-art model designed to restore diacritics to text across 10 languages in a single, unified system. Developed by olaverse, DiacNet-1.1 is fine-tuned from Google’s ByT5-base (580M parameters) and trained on 7 million sentences of commercially licensed text. Whether you’re working with Yorùbá, Vietnamese, Polish, or Spanish, DiacNet-1.1 can help you recover the nuances of written language with remarkable accuracy.


    Supported Languages

    DiacNet-1.1 supports the following 10 languages:

    • Yorùbá (<yor>)
    • Igbo (<ibo>)
    • Hausa (<hau>)
    • Vietnamese (<vie>)
    • Polish (<pol>)
    • Turkish (<tur>)
    • Portuguese (<por>)
    • Spanish (<spa>)
    • French (<fra>)
    • Italian (<ita>)

    You can also use the <auto> tag to let the model infer the language automatically, with minimal impact on accuracy.


    How It Works

    DiacNet-1.1 is a sequence-to-sequence model that takes stripped text (text without diacritics) and predicts the fully diacritized version. The model is trained on a diverse corpus of web and encyclopedic text, ensuring robustness across different domains.

    Example Usage

    Here’s how you can use DiacNet-1.1 in just a few lines of Python:

    from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
    
    # Load the model and tokenizer
    tok = AutoTokenizer.from_pretrained("olaverse/diacnet-1.1")
    model = AutoModelForSeq2SeqLM.from_pretrained("olaverse/diacnet-1.1")
    
    # Restore diacritics to Yorùbá text
    text = "<yor> se eranko naa si gbo o?"
    inputs = tok(text, return_tensors="pt")
    out = model.generate(**inputs, max_new_tokens=512)
    print(tok.decode(out[0], skip_special_tokens=True))
    # Output: ṣé ẹranko náà sì gbọ́ ọ?

    Language Tagging

    Prefix your input text with the appropriate language tag to guide the model:

    # Vietnamese
    text = "<vie> Toi khong biet tieng Viet"
    # Output: Tôi không biết tiếng Việt
    
    # Polish
    text = "<pol> Lodz jest piekna"
    # Output: Łódź jest piękna
    
    # Auto-detect language
    text = "<auto> El nino esta en la casa"
    # Output: El niño está en la casa

    Performance

    DiacNet-1.1 was evaluated on diacbench, a benchmark of 1,000 held-out sentences per language, sourced independently from the training corpus. The model was tested using the following metrics:

    • DER (Diacritic Error Rate): Errors restricted to diacritic-eligible characters.
    • WER (Word Error Rate): Measures errors at the word level.
    • CER (Character Error Rate): Measures errors at the character level.
    • Exact Match: Percentage of sentences restored perfectly.

    Benchmark Results

    LanguageDERWERCERExact Match
    Italian0.00020.10%0.04%99.3%
    Portuguese0.00311.39%0.23%88.0%
    French0.00530.77%0.18%94.7%
    Polish0.00582.65%0.33%85.3%
    Turkish0.00681.50%0.35%91.7%
    Spanish0.00812.06%0.35%83.0%
    Vietnamese0.046011.98%3.06%36.0%
    Igbo0.05084.78%1.29%41.0%
    Hausa0.05932.53%0.51%59.7%
    Yorùbá0.200642.18%14.50%0.3%

    Note: The lower exact-match scores for Vietnamese and Yorùbá reflect the high density of diacritic marks in these languages. Even a single error can cause the entire sentence to fail the exact-match metric. DER is the most meaningful metric for these languages.


    Improvements Over DiacNet-1.0

    DiacNet-1.1 represents a significant leap forward from its predecessor, DiacNet-1.0, with improvements across multiple languages:

    LanguageDER (1.0 → 1.1)WER (1.0 → 1.1)Verdict
    Vietnamese0.1264 → 0.046021.98% → 11.98%1.1 — 2.7× better
    Turkish0.0447 → 0.006819.20% → 1.50%1.1 — 6.6× better
    Polish0.0357 → 0.005815.12% → 2.65%1.1 — 6.2× better
    Italian0.0015 → 0.00020.66% → 0.10%1.1 — 7.5× better
    Portuguese0.0072 → 0.00311.80% → 1.39%1.1 better
    Spanish0.0084 → 0.00812.19% → 2.06%~equal
    French0.0038 → 0.00531.46% → 0.77%Mixed
    Igbo0.0359 → 0.05088.34% → 4.78%Mixed
    Hausa0.0383 → 0.05938.58% → 2.53%Mixed
    Yorùbá0.1554 → 0.200627.69% → 42.18%1.0 better

    Key Observations

    1. Major Gains: DiacNet-1.1 achieves 2.7× to 7.5× better performance on Vietnamese, Turkish, Polish, and Italian.
    2. Mixed Results: For French, Igbo, and Hausa, the model shows trade-offs between DER and WER. For example, while DER increased slightly for Hausa, WER improved dramatically (8.58% → 2.53%), and exact-match scores jumped from 0% to 59.7%.
    3. Yorùbá Challenge: The model’s performance on Yorùbá regressed due to the lower quality of diacritic annotations in the larger training corpus. DiacNet-1.0 was trained on ~2,000 high-quality Yorùbá passages, while DiacNet-1.1’s corpus, though much larger, had a lower diacritic density. This highlights a critical trade-off: more data isn’t always better if annotation quality suffers.

    Priority for Next Release: Improving Yorùbá, Igbo, and Hausa performance by incorporating higher-quality annotated data.


    Comparison with General-Purpose LLMs

    We benchmarked DiacNet-1.1 against Claude Sonnet 4.5 and GPT-4o-mini on the same diacbench dataset. The results reveal interesting trade-offs between accuracy and reliability:

    Diacritic Error Rate (DER)

    SystemYorùbáIgboHausaVietnamesePolishTurkishPortugueseSpanishFrenchItalian
    Copy-Input (Floor)0.43580.11710.02360.30620.09150.11130.04760.04260.04710.0333
    DiacNet-1.00.15540.03590.03830.12640.03570.04470.00720.00840.00380.0015
    DiacNet-1.10.20060.05080.05930.04600.00580.00680.00310.00810.00530.0002
    Claude 4.5 (Strict)0.33530.22610.35090.02780.03430.02410.01720.02150.11710.0005
    Claude 4.5 (Fallback)0.19130.04270.01780.01070.00350.00190.00110.00400.00520.0005
    GPT-4o-mini (Strict)0.46070.50350.63680.08470.04300.02610.02800.05550.03410.0085
    GPT-4o-mini (Fallback)0.28110.12770.14320.03990.00700.00380.00240.00890.00230.0052

    Key Takeaways

    1. Strict vs. Fallback Scoring:
    • Strict scoring counts all errors, including cases where the LLM rewrites or translates the input.
    • Fallback scoring assumes a pipeline that detects and skips rewritten outputs, focusing only on diacritic restoration.
    1. DiacNet-1.1 vs. Claude (Strict): DiacNet-1.1 outperforms Claude on 8 out of 10 languages when scored strictly.
    2. DiacNet-1.1 vs. Claude (Fallback): With fallback scoring, Claude outperforms DiacNet-1.1 on 9 out of 10 languages. This highlights that while LLMs can achieve higher accuracy on compliant outputs, they fail to return usable text on 20-35% of inputs (e.g., 34% of Hausa, 26% of Yorùbá, 21% of Igbo).
    3. Reliability: DiacNet-1.1 never rewrites or alters base characters in 94.7–100% of cases, depending on the language. In contrast, LLMs alter 20–50% of inputs, making them unreliable for large-scale processing.

    Output Compliance

    SystemYorùbáIgboHausaVietnamesePolishTurkishPortugueseSpanishFrenchItalian
    DiacNet-1.1100%96.3%94.7%99.3%100%99.7%100%99.7%99.7%100%
    DiacNet-1.098.0%99.0%98.3%96.3%99.3%100%99.7%99.7%100%100%
    Claude 4.574.3%79.3%65.7%97.7%96.7%98.0%97.7%98.3%88.3%100%
    GPT-4o-mini68.0%57.3%49.3%93.7%96.0%94.7%97.7%97.3%96.7%99.7%

    Conclusion: If you need maximum accuracy on a few sentences and can tolerate retries, a frontier LLM like Claude may be the best choice. However, if you need to process millions of sentences reliably, deterministically, and offline, DiacNet-1.1 is the tool for the job.


    Training Details

    AspectDescription
    Base Modelgoogle/byt5-base (580M parameters, byte-level)
    Data7.02M sentences from FineWeb-2 (ODC-By) and Wikipedia (CC BY-SA), filtered for diacritic density, NFC-normalized, and deduplicated.
    ObjectiveSequence-to-sequence: <lang> stripped text → fully diacritized text.
    AugmentationPartial stripping (30%), character noise (7%), <auto> tag (12%), optional gloss conditioning (~6%).
    PrecisionBF16, max 512 bytes.
    Correction PassContinued training on high-density Yorùbá with 35% replay of other languages, learning rate 2e-5.

    Note: All training data is commercially licensed. No non-commercial sources (e.g., MENYO-20k, JW300) were used.


    Limitations

    While DiacNet-1.1 is a powerful tool, it has some limitations:

    1. Yorùbá Tone Marks: The model under-marks tones in Yorùbá due to under-annotated training data. Output should not be treated as publication-ready without review.
    2. Irreducible Errors: Stripping diacritics is lossy. For example, in Italian, both Viaggiò (3rd-person past) and Viaggio (1st-person present) reduce to Viaggio—no model (or human) can recover the original distinction.
    3. Proper Nouns: Names and places (e.g., Àlàmú vs. Àlámù) are unreliable due to insufficient context. Foreign names may also be over-nativized (e.g., Vietnamese Tom → Tôm).
    4. Gloss Conditioning: The model supports optional [g: word=meaning] hints, but their effect on diacbench was neutral to slightly negative. This feature is documented but not recommended for general use.
    5. Structural Guarantees: As a seq2seq model, DiacNet-1.1 can alter base characters. However, measured compliance on diacbench is 94.7–100%, so constrained decoding is advisable for high-stakes pipelines.
    6. Register Bias: The model was trained on web and encyclopedic text. Performance on conversational, poetic, or code-switched text is untested.
    7. Benchmark Limitations: The diacbench dataset contains a small number of defective reference sentences (~1%), which slightly caps measured scores.

    Intended Use Cases

    DiacNet-1.1 is ideal for:

    ✅ Restoring diacritics in scraped or user-typed text.
    ✅ Cleaning corpora for downstream NLP tasks.
    ✅ Assisting typing in languages where diacritics are commonly omitted.

    DiacNet-1.1 is not designed for:

    ❌ Text generation.
    ❌ Translation.
    ❌ Spelling correction.
    ❌ General-purpose language modeling.

    Best Practice: Use DiacNet-1.1 with human review for Yorùbá and any application where a wrong tone mark could change meaning.


    Citation

    If you use DiacNet-1.1 in your research or applications, please cite it as follows:

    @misc{diacnet11,
      author = {olaverse},
      title  = {diacnet-1.1: multilingual diacritic restoration},
      year   = {2026},
      url    = {https://huggingface.co/olaverse/diacnet-1.1}
    }

    DiacNet-1.1 is built on ByT5 (Xue et al.), FineWeb-2, and Wikipedia. Please attribute these sources per their respective licenses.


    Try It Yourself

    Ready to restore diacritics to your text? Head over to the DiacNet-1.1 Hugging Face page to explore the model, download it, and start using it in your projects!


    Questions or feedback? Open an issue on the Hugging Face repository or reach out to the olaverse team.

  • One Passage In, Real Questions Out: Introducing mist-qg-1.5b

    One Passage In, Real Questions Out: Introducing mist-qg-1.5b

    A compact, open-source question generator for 25 languages — and a data factory for building better retrieval systems.


    Every retrieval system, quiz app, and search engine shares a hidden hunger: it needs questions. Questions to train retrievers, questions to test rerankers, questions to turn static content into interactive learning. For English, that data exists in abundance. For most of the world’s languages, it barely exists at all.

    Today we’re releasing mist-qg-1.5b, a compact multilingual question generation model, open-sourced on Hugging Face under Apache 2.0. Give it a passage in any of 25 languages and it returns natural, search-style questions that the passage directly answers — the kind of thing a real user would actually type into a search box.

    25 languages, one small model

    Coverage spans the languages your users actually speak:

    • European: English, French, German, Spanish, Portuguese, Italian, Dutch, Russian, Polish
    • Asian & Middle Eastern: Hindi, Japanese, Korean, Vietnamese, Indonesian, Turkish
    • African: Yoruba, Igbo, Hausa, Swahili, Amharic, Zulu, Xhosa, Shona, Somali, Afrikaans

    At ~1.5 billion parameters (fine-tuned from Qwen2.5-1.5B-Instruct), it runs comfortably on a single modest GPU — no cluster required.

    Two jobs, one model

    mist-qg-1.5b is deliberately dual-use:

    1. A question-generation service. Wrap it behind an endpoint and you have instant question generation for education platforms, quiz builders, FAQ tooling, and content pipelines. The model returns strict JSON ({"questions": [...]}), so outputs slot straight into production systems — and for guaranteed structural validity, serve it with vLLM guided JSON decoding.

    2. A data factory. This is the quieter superpower. Every question it generates, paired with its source passage, is a (query, positive) training pair — exactly what you need to fine-tune retrievers and rerankers for RAG. For languages where such data has never existed at scale, this model can mint it.

    The interesting part: round-trip verification

    Question generation models often produce questions that look plausible but aren’t actually answerable from the source. We attacked this at the data level.

    Training data was distilled from the Aya collection using Qwen2.5-32B-Instruct — but every candidate question had to pass a test before entering the training set: embedded alongside its source passage and a pool of distractors, the question had to retrieve its own passage at rank one. Fail the round trip, and the question was discarded. Roughly 150k questions across ~50k passages survived this filter.

    We evaluate the same way. On 625 held-out passages never seen during training, the model achieves an overall round-trip keep-rate of 0.955 — meaning 95.5% of generated questions successfully retrieve their source passage from a distractor pool (scored with the larger Qwen3-Embedding-4B, a stricter judge than the one used in training).

    Honest numbers, including the weak spots

    Six languages — English, Spanish, Portuguese, Turkish, Indonesian, and Afrikaans — scored a perfect 1.000 on the held-out eval, and most high-resource languages cluster between 0.95 and 1.00.

    We want to be equally upfront about the other end: Amharic, Somali, and Shona sit at 0.58–0.70, and outputs in those three languages should be treated with lower confidence. Our analysis suggests this gap tracks fine-tuning data volume (~2,000 source passages per language) rather than a hard model-capacity ceiling, which makes it a natural target for the next data-scaling pass. We’re publishing these numbers because per-language transparency is the only way users can make informed decisions — and because we intend to close the gap.

    Try it in one minute

    pip install -U transformers
    
    import json, torch
    from transformers import AutoModelForCausalLM, AutoTokenizer
    
    model_id = "olaverse/mist-qg-1.5b"
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    model = AutoModelForCausalLM.from_pretrained(model_id, dtype=torch.bfloat16, device_map="auto")
    
    passage = "Tides are caused by the gravitational pull of the moon and, to a lesser extent, the sun, acting on Earth's oceans."
    
    messages = [
        {"role": "system", "content": "You write search-style questions that a passage directly answers."},
        {"role": "user", "content": f'''You are given a passage. Write 3 questions that the passage directly answers.
    
    Rules:
    - Each question must be answerable using ONLY this passage.
    - Vary the type: factual, yes/no, and a comparison or "why/how".
    - Natural, like a real user search query. Do NOT write "according to the passage".
    - Write the questions in English.
    
    Return ONLY JSON: {{"questions": ["...", "...", "..."]}}
    
    Passage: {passage}'''},
    ]
    
    input_ids = tokenizer.apply_chat_template(messages, tokenize=True,
        add_generation_prompt=True, return_tensors="pt").to(model.device)
    out = model.generate(input_ids, max_new_tokens=200, do_sample=False,
        pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id)
    text = tokenizer.decode(out[0][input_ids.shape[1]:], skip_special_tokens=True)
    print(json.loads(text[text.index("{"): text.rindex("}") + 1])["questions"])
    # ["What causes ocean tides?", "Does the sun affect tides?",
    #  "Which has a bigger effect on tides, the moon or the sun?"]
    

    Or skip the setup entirely and try the live demo Space.

    Links

    • Model: https://huggingface.co/olaverse/mist-qg-1.5b
    • Training dataset: https://huggingface.co/datasets/olaverse/qg-passages-multi
    • Demo: https://huggingface.co/spaces/olaverse/Mist_Question_Gen
    • GitHub: https://github.com/Olaverse-Labs

    If you use mist-qg-1.5b to build something — a quiz app, a retriever for a language nobody else supports, anything — we’d genuinely love to hear about it. Open a discussion on the model page or reach us on Twitter/X.


    Olaverse Lab builds open-source AI with first-class support for underserved languages. Follow us on Hugging Face and LinkedIn.

  • Introducing diacnet-1.0: One Model, Ten Languages, and an Honest Look at What It Can (and Can’t) Do Yet

    Introducing diacnet-1.0: One Model, Ten Languages, and an Honest Look at What It Can (and Can’t) Do Yet

    Diacritics carry real meaning. In Yorùbá, the same base letters can spell three different words depending on the tone marks: ogun (war), ògùn (medicine), ogún (twenty). In Vietnamese, Polish, and Turkish, dropping accents doesn’t just look wrong — it can make text ambiguous or outright unreadable. Yet diacritics are exactly what gets lost the moment text is typed on a non-native keyboard, scraped from the web, or transcribed casually.

    diacnet-1.0, built by Olaverse, is a single model that restores diacritics across 10 languages spanning West Africa, Europe, and Southeast Asia — Yorùbá, Vietnamese, Igbo, Hausa, Polish, Turkish, Portuguese, Spanish, French, and Italian — without needing a separate model or vocabulary per language.

    It’s live now on Hugging Face: olaverse/diacnet-1.0

    How it works

    diacnet is fine-tuned from google/byt5-small, a byte-level sequence-to-sequence model. Working at the byte level rather than the word level means the same mechanism handles Yorùbá tone marks, Vietnamese combining diacritics, and Polish or Turkish special characters — no per-language vocabulary needed.

    A language tag prefix (<yor>, <vie>, <pol>, etc.) tells the model which diacritic inventory to apply to a given input, so one joint model covers all 10 languages instead of ten separate ones.

    Training is fully self-supervised: clean, already-diacritized text is the target, and diacritics are deterministically stripped to create the training input. No manual annotation required.

    from transformers import AutoTokenizer, T5ForConditionalGeneration
    
    tok = AutoTokenizer.from_pretrained("olaverse/diacnet-1.0")
    model = T5ForConditionalGeneration.from_pretrained("olaverse/diacnet-1.0")
    
    text = "<yor> se eranko naa si gbo o?"
    inputs = tok(text, return_tensors="pt")
    output_ids = model.generate(**inputs, max_new_tokens=256)
    print(tok.decode(output_ids[0], skip_special_tokens=True))
    # ṣé ẹranko náà sì gbọ́ ọ?
    

    The benchmarks — tested on real external data

    Early validation numbers looked great: a median character error rate (CER) of about 0.02 across most languages. But that number came from a held-out slice of the same training distribution — machine-generated text, not the messy real-world input diacnet would actually see in the wild.

    So I re-ran evaluation on genuinely external test sets: Tatoeba for Spanish, French, Italian, Portuguese, Polish, Turkish, and Vietnamese; MasakhaNEWS for Igbo and Hausa; and the MENYO-20k test split for Yorùbá — roughly 1,000 diacritic-bearing sentences per language, stripped of diacritics as input, with the original text as ground truth.

    Word error rate (%, lower is better), diacnet-1.0 vs. doing nothing:

    LanguageCopy-input (floor)diacnet-1.0
    Italian14.160.62
    French18.191.45
    Portuguese19.471.61
    Spanish17.382.07
    Polish40.0113.18
    Turkish42.7619.49
    Vietnamese86.2521.60
    Igbo34.118.00
    Yorùbá87.3129.08
    Hausa7.808.27 ⚠️

    The good

    On 9 of 10 languages, diacnet beats the “do nothing” floor by a wide margin. Vietnamese goes from an unusable 86.3 WER to 21.6. Italian drops from 14.2 to 0.6. These are real, reproducible gains on text the model never saw during training.

    The honest middle: Yorùbá

    At 29.1 WER, Yorùbá is a big improvement over doing nothing (87.3) — but it’s still behind a dedicated Yorùbá-only specialist, Davlan/mt5_base_yoruba_adr, which scored 16.4 WER on an earlier run of the same test set, despite being roughly twice diacnet’s size.

    The gap isn’t really about model capacity. It’s about ambiguity: the same base letters in Yorùbá can spell multiple valid words depending on tone, and resolving that often requires semantic context a byte-level model trained mostly on machine-generated text hasn’t seen enough of. Closing this gap means more human-authored Yorùbá training data — not just a bigger model.

    The failure: Hausa

    diacnet-1.0 currently does not work for Hausa. It scores slightly worse than doing nothing at all (8.3 vs. 7.8 WER). With only about 15 Hausa examples in the training set, the model never learned Hausa’s hooked-letter inventory (ɓ, ɗ, ƙ) and mostly just passes text through unchanged.

    Don’t use diacnet-1.0 for Hausa diacritic restoration right now. Fixing this is the top priority for the next version.

    Why publish the bad numbers too

    It would have been easy to keep the old ~0.02 CER figure and call it a day — it’s not technically false, just measured on the wrong distribution. But a model that quietly fails on Hausa while claiming “near-perfect restoration” is worse than useless for anyone building on it in production. Anyone integrating this needs to know exactly where it helps and where it doesn’t.

    The evaluation code and test sets are open too — olaverse/diacbench — so these numbers are reproducible and other models can be compared on the same footing.

    What’s next: diacnet-1.1

    • Hausa — train on adequate Hausa data (Wura, MasakhaNEWS), targeting a clear win over the copy-input floor.
    • Yorùbá — incorporate human-authored training text (e.g. the MENYO-20k train split) to close the gap to the monolingual specialist.
    • Turkish/Vietnamese — remaining errors cluster in lexically ambiguous cases; larger-capacity variants are under evaluation.

    Try it, break it, help improve it

    diacnet-1.0 is released under Apache-2.0: huggingface.co/olaverse/diacnet-1.0

    If you have Hausa diacritic data, human-authored Yorùbá text, or want to stress-test the model on real-world messy input, get in touch — we’re actively looking for collaborators and data partners ahead of diacnet-1.1.