← TinyGPT · docs · devlog · roadmap · speedup
source: docs/recipes/cookbook-character-specialist.md · view on GitHub ↗

Cookbook - Character Specialist

What you get:

What this is not: a magic 70B replacement. The goal is a small specialist that stays in character on a narrow world better and faster than the base model.

1. Pick The Student

Use a small instruct base with a normal HF tokenizer:

tinygpt download-model Qwen/Qwen2.5-0.5B

Useful alternatives:

Qwen/Qwen2.5-1.5B        higher quality, slower
HuggingFaceTB/SmolLM2-360M-Instruct        faster, weaker

Keep the base path in a variable:

export STUDENT_DIR="$HOME/.cache/huggingface/hub/models--Qwen--Qwen2.5-0.5B/snapshots/<HASH>"

2. Pull Public Dialogue Data

Start with permissive public roleplay/dialogue datasets:

mkdir -p .tinygpt/character

tinygpt download-dataset chimbiwide/RolePlay-NPC \
  --out .tinygpt/character/roleplay-npc.jsonl

tinygpt download-dataset agentlans/multi-character-dialogue \
  --out .tinygpt/character/multi-character-dialogue.jsonl

tinygpt download-dataset NousResearch/CharacterCodex \
  --out .tinygpt/character/character-codex.jsonl

Avoid commercially risky scraped datasets unless this is research-only.

3. Write Character Prompts

Create prompts that describe the world state, character, recent memory, and the user turn. Example:

{"prompt":"character: mara, a tired blacksmith in aliveville\nmood: amused but guarded\nscene: the player asks why the forge is closed after sunset\nreply in mara's voice, one or two sentences."}
{"prompt":"character: io, a curious shopkeeper\nmood: excited\nscene: the player asks about rumors near the old bridge\nreply in io's voice, one or two sentences."}

Save them to:

.tinygpt/character/character-prompts.jsonl

4. Label With A Local Teacher

Run a local teacher in LM Studio or any OpenAI-compatible server. Then label:

tinygpt synthesize \
  --teacher http://127.0.0.1:1234/v1 \
  --teacher-model qwen2.5-72b-instruct-q4 \
  --inputs .tinygpt/character/character-prompts.jsonl \
  --input-field prompt \
  --temperature 0.8 \
  --parallel 2 \
  --rate-limit 20 \
  --out .tinygpt/character/synthetic-character.jsonl

For a fast first pass, use 500-1000 prompts. For a stronger specialist, use 10K+ prompts and let the local teacher run overnight.

5. Merge And Clean

Use existing shell tooling to merge rows, then TinyGPT cleanup commands to remove obvious duplicates and unsafe text:

cat \
  .tinygpt/character/roleplay-npc.jsonl \
  .tinygpt/character/multi-character-dialogue.jsonl \
  .tinygpt/character/character-codex.jsonl \
  .tinygpt/character/synthetic-character.jsonl \
  > .tinygpt/character/merged-raw.jsonl

tinygpt filter \
  --in .tinygpt/character/merged-raw.jsonl \
  --out .tinygpt/character/merged-filtered.jsonl \
  --toxicity

tinygpt dedupe \
  .tinygpt/character/merged-filtered.jsonl \
  --out .tinygpt/character/merged-sft.jsonl

If a downloaded dataset is not already SFT-shaped, convert it before merge. The minimal target shape is one JSON object per line with prompt/input text and the desired assistant response.

6. Train LoRA

tinygpt sft "$STUDENT_DIR" \
  --data .tinygpt/character/merged-sft.jsonl \
  --template chatml \
  --rank 16 \
  --alpha 32 \
  --neftune-alpha 5 \
  --steps 2000 \
  --lr 1e-4 \
  --out .tinygpt/character/character-specialist.lora

Start with a 100-step smoke before a long run:

tinygpt sft "$STUDENT_DIR" \
  --data .tinygpt/character/merged-sft.jsonl \
  --template chatml \
  --rank 16 \
  --alpha 32 \
  --steps 100 \
  --lr 1e-4 \
  --out .tinygpt/character/smoke.lora

7. Serve

tinygpt serve "$STUDENT_DIR" \
  --lora .tinygpt/character/character-specialist.lora \
  --host 127.0.0.1 \
  --port 8080 \
  --prompt-cache-dir .tinygpt/character/prompt-cache

The prompt cache matters for character apps because the same character card and world rules usually repeat on every turn.

8. Eval

Create held-out prompts:

{"prompt":"mara sees the player carrying broken armor. reply with concern, not exposition.","character":"mara","must_contain":["armor"],"must_not_contain":["as an ai"]}
{"prompt":"io is asked about the old bridge. reply with curiosity and one rumor.","character":"io","must_contain":["bridge"],"must_not_contain":["i don't have"]}

Run the base and LoRA through the same prompts, then compare by hand or with a small rubric script:

tinygpt synthesize \
  --teacher http://127.0.0.1:8080/v1 \
  --teacher-model tinygpt \
  --inputs .tinygpt/character/eval-prompts.jsonl \
  --input-field prompt \
  --temperature 0.7 \
  --out .tinygpt/character/eval-lora.jsonl

Score each row on:

in_character: 0-2
world_consistency: 0-2
specificity: 0-2
voice_naturalness: 0-2
format_ok: 0-1

The LoRA is worth keeping only if it beats the base on character voice and specificity without losing format discipline.

9. Iterate

Common fixes:

Current CLI Gaps

This cookbook uses existing commands. Three useful follow-ups remain: