CoolFace
Modelpublic

fongios/Hollowfen-NPC-2B-fp16

sourceHugging Faceapache-2.0updated 4mo agoView on Hugging Face
0likes12downloads
Model Card

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)

NPCRoleVoice
Father LinPriest of Gengis alonewarm, devout, localist
PomeBakerfolksy, casual piety
The WandererCryptic outsidersparse, first-hand mystic
AdaArchivistprecise, dry, full-pantheon scholar

How to use it (it was trained on a specific prompt format)

Three things make it behave; skip them and quality drops:

  1. 1.Per-NPC system prompt + a canon facts block appended to it.
  2. 2.Plain ChatML with a closed empty `<think></think>` scaffold (Qwen3.5 is a reasoning model; this keeps replies direct, no <think> leakage).
  3. 3.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).

python
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:

json
{"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

BaseQwen/Qwen3.5-2B (hybrid linear-attention qwen3_5)
MethodLoRA (QLoRA on a 4-bit base) via mlx-lm, on Apple Silicon
v2 = model soupweight-average of two LoRA runs: a canon-strong run (more lore/distinction data, val-min) and a flow-strong run (added multi-turn data, val-min), merged 65/35 toward canon
LoRArank 8, scale 10, dropout 0.05, on mlp.* + self_attn.* (the full-attention layers); the SSM linear_attn.* blocks left untouched
Data~200 curated multi-turn dialogues (greetings, lore/worldbuilding, comfort, refusals, break-character probes, god-distinction drills, multi-turn flow), authored by a frontier model + hand-written gold, all in one consistent canon
Formatplain ChatML, no-think; system = persona + canon facts; completions {emotion, speech} JSON

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.