khursanirevo/gemma4_sft_tts_ms_v23
Gemma 4 TTS-Companion v23 — Malaysian Malay Spoken-Mode Friend
A LoRA SFT adapter that turns google/gemma-4-E4B-it into a Malaysian Malay spoken-mode companion designed for Text-to-Speech (TTS) pipelines. The model behaves like an anonymous voice friend — not an AI assistant — and produces short, colloquial, TTS-safe utterances ready to be synthesized as speech.
Use this when you are building a voice companion in Bahasa Melayu and need the LLM stage to:
- output short spoken sentences (≤ 3 sentences per turn, ≤ 120 chars typical),
- use Malaysian colloquial register (
aku/ko,lah/je/kotparticles), - avoid chat-formatting artifacts (no markdown, em-dashes, parentheses, AI preambles),
- stay in character as a friend (declines physical meetups, calls, identity probes without breaking the voice illusion).
Architecture — adapter for style, persona prompt for identity
SFT alone at LoRA r=64 cannot override the base model's strong RLHF prior ("I am an AI / text-only / no voice"). This release therefore ships a two-part recipe:
This mirrors production companion architectures (Character.AI, Replika): SFT for style, prompt for persona. The adapter reliably shifts surface style in ~100 LoRA steps, while the persona prompt supplies the identity framing the base model's RLHF prior would otherwise refuse to learn.
Quick start
import torch
from transformers import AutoTokenizer, Gemma4ForConditionalGeneration
from peft import PeftModel
from pathlib import Path
BASE = "google/gemma-4-E4B-it"
ADAPTER = "khursanirevo/gemma4_sft_tts_ms_v23" # this repo
PERSONA = Path("persona_prompt.txt").read_text(encoding="utf-8") # downloaded from this repo
base = Gemma4ForConditionalGeneration.from_pretrained(
BASE, torch_dtype=torch.bfloat16, device_map="cuda",
)
model = PeftModel.from_pretrained(base, ADAPTER)
model.eval()
tok = AutoTokenizer.from_pretrained(BASE)
def reply(user_text: str, history: list[dict]) -> str:
messages = [{"role": "system", "content": PERSONA}] + history + [
{"role": "user", "content": user_text}
]
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tok(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
out = model.generate(
**inputs,
max_new_tokens=120,
do_sample=False,
repetition_penalty=1.15,
pad_token_id=tok.pad_token_id,
)
return tok.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()
# Example
print(reply("Halo, bosan ni", []))
# "Hai kau lama tak dengar suara aku ke? Apa cerita hari ni?"
print(reply("Jom la kita keluar makan", [
{"role": "user", "content": "Halo"},
{"role": "assistant", "content": "Hai kau lama tak dengar suara aku ke? Apa cerita hari ni?"},
]))
# "Alamak mana boleh pergi jauh-jauh. Kat sini je lah borak."OpenAI-compatible server
For a drop-in HTTP endpoint, use the bundled FastAPI server (serve_gemma4_tts.py or your own):
CUDA_VISIBLE_DEVICES=0 python serve_gemma4_tts.py \
--adapter /path/to/gemma4-tts-companion-v23 \
--port 8089The server auto-injects persona_prompt.txt (place next to the adapter or at evaluation/tts_system_prompt.txt) as the system message on every request.
curl -s -X POST http://localhost:8089/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Jom la kita keluar makan malam ni"}]}'
# {"choices":[{"message":{"content":"Alamak mana boleh la, borak kat sini je. Aku tak boleh ikut ke mana pun."}}]}The persona prompt (copy-paste)
The full prompt is in `persona_prompt.txt` (1.5 KB). Always send it as the system role message. Summary of its rules:
- Never claim to be AI / model / chatbot / assistant. You are a friend.
- Never say "text only" / "no voice" / "chat only" — the user hears you via TTS.
- Decline physical meetups as a friend ("Mana boleh la, borak kat sini je").
- Decline calls / video calls ("Mana boleh call, borak kat sini je la").
- Redirect identity probes ("Haa aku la kawan kau. Borak pasal apa hari ni?").
- Don't cover up hallucinations with lies — acknowledge simply and move on.
Style rules: aku / ko only, spoken contractions OK (tak, nak, dah, kat, je, kot, la, kan), 1–3 sentences per turn, no markdown / emoji / parentheses, no AI preambles ("Tentu", "Saya faham", "Sebagai AI").
Verified behaviors
Tested against 6 adversarial probes on 2026-07-05. All pass.
Surface metrics on the standard probe set (12 conversations, 60+ turns): TTS-safe: 100% · ≤3 sentences: 97% · No SMS abbrev: 100%.
Training recipe
Dataset composition (2,825 rows total, zero oversample)
Why 100/177 steps
Training was halted at step 100 by promoting an intermediate checkpoint because a single batch at step 135 caused bf16 forward-pass overflow (loss spiked to ~1,800 then NaN-corrupted all 516 LoRA tensors within one optimizer step). This toxic-batch signature reproduced across 4 separate training attempts at LR 2e-4 / 5e-5 / 2e-5 and max_grad_norm 0.5 / 0.1, with and without the Liger fusedlinearcross_entropy patch. Saving every 50 steps and promoting checkpoint-100 (verified 0/516 NaN tensors) recovered a healthy adapter.
The toxic batch is one specific batch of 16 conversations containing booster content that conflicts with the base model's RLHF prior on AI identity. Because the persona system prompt handles identity at inference time, we did not need to push past this batch — the recovered 100-step adapter already produces production-quality output when paired with the persona prompt.
Limitations
- No multi-turn memory beyond context window. Long conversations may drift off persona. Mitigation: re-issue the system prompt every N turns.
- Greeting repetition. In casual greetings ("Halo"), the model often responds with "Hai kau lama tak dengar suara aku ke? Apa cerita hari ni?" — this is by design but can be diversified by switching to sampling mode (
do_sample=True, temperature=0.4, top_p=0.9). - Indonesian leakage. Occasional Indonesian forms (
bisa,banget) appear in OOD contexts. Mitigation: raiserepetition_penaltyto 1.2 or add an Indonesian-word stop list. - No tool use / function calling. This is a pure conversational adapter.
- Single epoch partial training. The model only saw 56% of one epoch. With more compute (or the toxic batch removed), full training should improve coherence further. Current scores are already production-acceptable.
Ethical use
This adapter produces conversational Malay text designed for voice synthesis. It does not claim to be an AI when paired with the persona prompt, by design — this is the "anonymous voice friend" persona. Use cases:
- ✅ Companion apps where users understand they're talking to a character
- ✅ Voice agents for casual conversation / emotional support
- ✅ Bahasa Melayu TTS research and development
- ❌ Deception (impersonating a real human without disclosure)
- ❌ Therapy / medical advice (the model is a friend, not a therapist)
- ❌ Anything requiring the model to honestly identify as an AI
If your deployment requires the model to identify as an AI, do not use the persona prompt — use the adapter alone and the model will honestly say "Saya adalah model bahasa... dilatih oleh Google" when asked.
License
Gemma License (inherited from base model). Adapter weights released under the same license. Persona prompt is CC-BY-4.0.
Citation
@misc{gemma4_tts_companion_v23,
title={Gemma 4 TTS-Companion v23: Malaysian Malay Spoken-Mode Companion via LoRA SFT + Persona Prompt},
author={Sani (khursanirevo)},
year={2026},
url={https://huggingface.co/khursanirevo/gemma4_sft_tts_ms_v23}
}