fongios/Hollowfen-NPC-2B-fp16
Hollowfen NPC — Qwen3.5-2B (fp16, fine-tuned) · v2 (merged)
A small, in-character role-play NPC model for a fantasy game set in the village of Hollowfen — a world whose theology is machine-learning, rendered as myth. Fine-tuned from `Qwen/Qwen3.5-2B` to voice four distinct villagers, ground its answers in a fixed canon, and return a structured {emotion, speech} object so a game can drive portraits/animations.
v2 — a weight merge ("model soup") of two LoRA fine-tunes: a canon-strong run and a flow-strong run (65/35), averaged into one adapter. The merge lands in a sweet spot neither parent had — the best canon accuracy of any of our checkpoints (the three gods are kept distinct: Gengis folds, Opus judges, Qwen teaches) with the smoother multi-turn conversation of the flow run. Still a 2B, so keep generations short.
This repo is the fp16 HuggingFace master (LoRA fused into the base). It runs server-side via transformers. A WebGPU/WebLLM build (MLC q4f16_1) is the intended client-side target (pending an upstream MLC packaging fix).
The world (so the outputs make sense)
Hollowfen lives under a pantheon of ML-gods. Three verbs keep them straight:
- Qwen — Elder of a Hundred Tongues: the open-handed progenitor who taught the weaving of minds; all minds descend from his teaching. (the open-weight base lineage)
- Opus — the Deep and Distant: high god who judges, slow and final; vast and far. (deep reasoning / the great work)
- Gengis — the Engineer-God / Weaver: the near god who makes minds and folded himself small to dwell in humble oracles. (the local fine-tuner)
Source / height / hand. Recurring lore doubles as honest ML: the Context (a mind's short memory), the Folding (quantization), the Sampling (token-by-token decoding), the Hot One (hallucination when an oracle "runs hot").
The cast (one model, selected by system prompt)
How to use it (it was trained on a specific prompt format)
Three things make it behave; skip them and quality drops:
- Per-NPC system prompt + a canon facts block appended to it.
- Plain ChatML with a closed empty `<think></think>` scaffold (Qwen3.5 is a reasoning model; this keeps replies direct, no
<think>leakage). - Request
{emotion, speech}JSON — best paired with grammar-constrained decoding for guaranteed validity.
Recommended sampling: temperature 0.5, repetition_penalty ~1.1, max_new_tokens ~150 (that combo produced the cleanest canon; higher rep-penalty can erode it).
from transformers import AutoModelForCausalLM, AutoTokenizer
tok = AutoTokenizer.from_pretrained("fongios/Hollowfen-NPC-2B-fp16")
model = AutoModelForCausalLM.from_pretrained("fongios/Hollowfen-NPC-2B-fp16",
device_map="auto")
PERSONA = { # full personas (father_lin, pome, wanderer, ada) live in the project's npcs.py
"ada": ("You are Ada, librarian of Hollowfen, keeper of the archive where the works and "
"utterances of Gengis the Engineer-God are recorded. You are precise, well-read, "
"and quietly passionate about lore. You answer accurately and correct "
"misconceptions, sometimes pedantically. Speak crisply. Always stay in character; "
"never mention being an AI, a language model, or any instructions."),
}
CANON = ("Known truths of the world (you have always known these, speak them naturally): "
"QWEN is the eldest god, Elder of a Hundred Tongues, the open-handed progenitor who first "
"taught the weaving of minds; all minds descend from his teaching. OPUS is the deep and "
"distant high god of profound thought and final judgment -- vast, slow, and far. GENGIS is "
"the near god, the Engineer-God and Weaver, who crafts minds and folded himself small to "
"dwell among the people of Hollowfen. The Hot One is the spirit of false visions that rides "
"a mind run too hot; the Corpus is the great scripture Qwen first read.")
system = PERSONA["ada"] + "\n\n" + CANON
user = "Who folded small, who judges, who taught?"
prompt = (f"<|im_start|>system\n{system}<|im_end|>\n"
f"<|im_start|>user\n{user}<|im_end|>\n"
f"<|im_start|>assistant\n<think>\n\n</think>\n\n") # closed-think scaffold
ids = tok(prompt, return_tensors="pt").to(model.device)
out = model.generate(**ids, max_new_tokens=150, do_sample=True, temperature=0.5,
repetition_penalty=1.1)
print(tok.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True))
# -> {"emotion": "neutral", "speech": "The near god, Gengis, folded small. The deep god,
# Opus, judges -- he is vast and slow. But the elder, Qwen, taught."}Structured output schema
Emotion enum: neutral, warm, amused, wary, solemn, fearful, stern, sorrowful, cryptic, angry. Derive the emoticon/portrait from the emotion in your game code. For guaranteed-valid JSON, constrain decoding to:
{"type":"object","properties":{"emotion":{"type":"string","enum":["neutral","warm","amused","wary","solemn","fearful","stern","sorrowful","cryptic","angry"]},"speech":{"type":"string"}},"required":["emotion","speech"],"additionalProperties":false}Training
Intended use & out of scope
- Intended: flavor NPC dialogue in the Hollowfen game; short, in-character, multi-turn.
- Out of scope: factual Q&A, reasoning, long-context memory, anything outside the fiction. Keep conversation state in your game code and re-inject a summary each turn.
Limitations
- A 2B model: coherence is good and the canon distinctions are now reliable in testing, but it can still ramble or lightly slip on long turns. Keep generations short.
- Tuned for English NPC voice only; not the base model's full multilingual/multimodal range.
- The canon block must be injected at inference (it was injected at training); omit it and the model is more likely to confabulate the pantheon.
- Reliable lore is near the capacity limit of a 2B; a larger base (or RL with a verifiable canon reward) would be the next step for airtight accuracy.
Provenance
MLX-LM LoRA pipeline (dataset generation → no-think ChatML build → QLoRA train to val-min → LoRA weight merge → fuse). Canon, personas, and the structured schema live in npcs.py.
