teencode-normalizer v3

Encoder-decoder Transformer that rewrites Vietnamese teencode (abbreviations, missing tones, slang spellings, short pronouns) into standard Vietnamese. Small enough (~20.8 MB INT8) to run on a Raspberry Pi 4 with ONNX Runtime.

Code (Rust data prep / inference / HTTP server, Python training, Pi Docker, Colab): https://github.com/femboyvn/TFVN-TTS-standardlization

Files

Path Content
onnx/encoder.int8.onnx, onnx/decoder.int8.onnx INT8 dynamic-quantized ONNX (no KV cache)
onnx/tokenizer.json shared BPE tokenizer, 8,000 tokens, NFC
onnx/output_vocab.txt, onnx/guard.json word-alignment guard data/settings used by the runner
onnx/config.json export config
model.safetensors PyTorch weights (FP16, weights only, step 13,000)
config.json model hyperparameters
inference_onnx.py minimal Python ONNX Runtime example (greedy)

Model details

  • Transformer seq2seq, pre-LayerNorm: 8 encoder + 4 decoder layers, d_model 256, 8 heads, d_ff 1536, dropout 0.1, ~19.94M parameters.
  • Shared BPE tokenizer, 8k vocab, NFC normalization; max length 192 tokens. Special ids: PAD 0, BOS 1, EOS 2, UNK 3.
  • Trained with PyTorch (BF16 autocast, AdamW, cosine schedule); grown from a smaller v2 model with function-preserving initialization. Selected checkpoint: step 13,000 (best combined dev CER).
  • Recommended decoding: beam 4 + word-alignment guard (guard.json).

Intended use

  • Normalizing informal Vietnamese chat / social-media text before search, NLP pipelines, TTS or display.
  • On-device / edge use (Raspberry Pi 4 class hardware).

Out of scope: translation, content moderation, free text generation. The model rewrites; it does not filter.

Evaluation

Character error rate (lower is better), beam 4 + guard:

Test set v2 v3 INT8 v3 BF16 v3 FP32
Original teencode (999) 2.92% 2.08% 2.05% 2.03%
Untoned (986) 12.38% 11.19% 11.37% 11.31%
Context (803, in-distribution) 11.93% 0.35% 0.23% 0.22%
Hard (1,251, eval-only) 10.96% 4.95% 4.84% 4.85%
Hard: ambiguous abbreviations 13.59% 8.09% 7.94% 7.94%
Hard: pronoun pairs 9.45% 2.34% 2.22% 2.24%
Hard: untoned context 10.04% 6.20% 6.22% 6.22%

The context test set comes from the same generator as the context training data, so its score is optimistic. The hard set has no overlap with the training data and is the fairer measure.

Examples (v3 INT8, beam 4 + guard)

Input Output
c oi, e nho c qua chị ơi, em nhớ chị quá
c oi, c muon hoi cai nay chú ơi, cháu muốn hỏi cái này
co oi, c xin phep nghi hoc cô ơi, cháu xin phép nghỉ học
mua toi goi cc cao cap voi mua tôi gói claude code cao cấp với
ong oi, t mua them cc ông ơi, tôi mua thêm cái
b oi, c ve roi nha bạn ơi, cháu về rồi nha
thang lam ML thằng làm ML

How to run

Rust runner (recommended: beam 4 + guard)

git clone https://github.com/femboyvn/TFVN-TTS-standardlization && cd TFVN-TTS-standardlization
huggingface-cli download tfvn/TTS-standardlization --include "onnx/*" --local-dir hf
cargo build --release
export ORT_DYLIB_PATH=/path/to/libonnxruntime.so   # ARM64 build on the Pi
./target/release/step_4_inference --model-dir hf/onnx "c oi, e nho c qua"

The GitHub repo also has a Raspberry Pi Docker image with an HTTP endpoint (POST /normalize).

Python (ONNX Runtime, greedy, no guard)

pip install onnxruntime tokenizers numpy huggingface_hub
huggingface-cli download tfvn/TTS-standardlization --local-dir teencode-v3
cd teencode-v3 && python inference_onnx.py "c oi, c muon hoi cai nay"
# chú ơi, cháu muốn hỏi cái này

Core of inference_onnx.py:

import numpy as np, onnxruntime as ort
from tokenizers import Tokenizer
BOS, EOS = 1, 2
enc = ort.InferenceSession("onnx/encoder.int8.onnx")
dec = ort.InferenceSession("onnx/decoder.int8.onnx")
tok = Tokenizer.from_file("onnx/tokenizer.json")

def normalize(text):
    src = np.array([[BOS] + tok.encode(text, add_special_tokens=False).ids[:190] + [EOS]], dtype=np.int64)
    mem = enc.run(None, {"src_ids": src})[0]
    ys = [BOS]
    for _ in range(min(192, src.shape[1] + 24) - 1):
        logits = dec.run(None, {"tgt_ids": np.array([ys], dtype=np.int64), "memory": mem, "src_ids": src})[0]
        nxt = int(logits.reshape(-1, logits.shape[-1])[-1].argmax())
        if nxt == EOS: break
        ys.append(nxt)
    return tok.decode(ys[1:])

Greedy decoding without the guard is simpler but slightly less accurate than the Rust runner (it can occasionally produce a malformed word).

PyTorch weights

model.safetensors loads into the Seq2Seq class in scripts/step_2_train.py from the GitHub repo:

import json, step_2_train as S
from safetensors.torch import load_file
cfg = {k: v for k, v in json.load(open("config.json")).items() if k in S.CFG}
m = S.Seq2Seq(cfg); m.load_state_dict({k: v.float() for k, v in load_file("model.safetensors").items()})

Use these to fine-tune further (--init-from) or to re-export ONNX.

Limitations and risks

  • Vulgar output is intended: vulgar slang in the input is normalized to the full vulgar words; the model does not censor.
  • Ambiguous abbreviations can be resolved wrongly, e.g. "cc" sometimes becomes "cái" instead of "claude code".
  • "ml" depends on capitalization: "ML" stays machine learning, lowercase "ml" tends to get the vulgar reading; a capitalized word like "Lam" may be read as a name (Lâm).
  • Brand names are output in lowercase.
  • Fully untoned input remains hard (~11% CER).
  • Trained only on synthetic data; real-world distribution shift is likely. The training data is not released.
  • Raspberry Pi 4 latency (0.25–0.55 s per sentence, INT8) is an estimate and has not been measured on a Pi.

License

MIT (see LICENSE).

Downloads last month
387
Safetensors
Model size
19.9M params
Tensor type
F16
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Evaluation results

  • CER original teencode test (999 rows on Synthetic Vietnamese teencode pairs (held-out test sets)
    self-reported
    2.080
  • CER untoned test (986 rows on Synthetic Vietnamese teencode pairs (held-out test sets)
    self-reported
    11.190
  • CER hard test (1251 rows on Synthetic Vietnamese teencode pairs (held-out test sets)
    self-reported
    4.950
  • CER context test (803 rows, INT8; in-distribution, optimistic) on Synthetic Vietnamese teencode pairs (held-out test sets)
    self-reported
    0.350