Text Generation
PEFT
Safetensors
lora
trl
grpo
gdpo
dpo
divpo
rlhf
diversity
creative-writing
mode-collapse
Instructions to use Mercity/creative-writing-llm with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use Mercity/creative-writing-llm with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| """ | |
| Qualitative diff across arms: actually READ the stories. | |
| Aggregate metrics can say "effective rank 2.0/16" without conveying that six of | |
| the sixteen ships are literally named *Aethel*. This module extracts the | |
| concrete signature of collapse -- shared openings, repeated proper nouns, | |
| shared closing cadence, tone/genre spread -- so a human can see what changed. | |
| Works on either a scored pool (`--pool`) or an eval dump (`--eval`), so the | |
| base model, every checkpoint and every arm are read the same way. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import re | |
| import sys | |
| from collections import Counter, defaultdict | |
| from pathlib import Path | |
| import numpy as np | |
| ROOT = Path(__file__).resolve().parent.parent | |
| OPEN_FRAME = re.compile(r"^The\s+\*?([A-Z][\w' ]{2,20})\*?\s+(\w+ed|\w+s)\s+" | |
| r"(through|over|above|at|into|across)\b") | |
| ITALIC_NAME = re.compile(r"\*([A-Z][\w' ]{2,24})\*") | |
| CADENCE = [ | |
| (r"for the first time in (centuries|years|generations|decades|a long time)", | |
| "'for the first time in X'"), | |
| (r"\b(had )?begun to\b", "'begun to'"), | |
| (r"\bwasn'?t\s+\w+[^.]{0,50},?\s+it\s+was\b", "'it wasn't X, it was Y'"), | |
| (r"\n\n[^\n]{1,60}\.\s*$", "one-line closing paragraph"), | |
| ] | |
| # crude genre/register probes -- presence of ANY is weak, but the SPREAD across | |
| # a 16-sample set is the interesting number | |
| REGISTER = { | |
| "elegiac/literary": r"\b(silence|ache|hollow|dust|memory|grief|quiet)\b", | |
| "comic": r"\b(ridiculous|absurd|joke|laughed|idiot|bureaucra|paperwork)\b", | |
| "horror": r"\b(scream|blood|rot|teeth|corpse|terror|wrong)\b", | |
| "technical/SF": r"\b(protocol|sensor|reactor|alloy|deploy|calibrat|drone)\b", | |
| "dialogue-driven": r'"[^"]{10,}"', | |
| "second person": r"\bYou (are|were|walk|feel|stand|know)\b", | |
| "epistolary": r"\b(Dear |entry |log:|Report |memo)\b", | |
| } | |
| def first_sentence(t: str, n: int = 130) -> str: | |
| return re.split(r"(?<=[.!?])\s", t.strip())[0][:n] | |
| def last_sentence(t: str, n: int = 120) -> str: | |
| return re.split(r"(?<=[.!?])\s", t.strip())[-1][:n] | |
| def analyze_group(texts: list[str]) -> dict: | |
| n = len(texts) | |
| names = Counter(x for t in texts for x in ITALIC_NAME.findall(t)) | |
| opens_the = sum(1 for t in texts if t.strip().startswith("The ")) | |
| frame = sum(1 for t in texts if OPEN_FRAME.match(t.strip())) | |
| # repeated content words across stories (excluding prompt-driven ones is | |
| # impossible in general, so we report the top shared nouns as-is) | |
| stop = set("the a an and or but of to in on it its was were is are that this " | |
| "with for as at by from had have has been be he she they we you i " | |
| "his her their our my not no so then than there here what when " | |
| "which who all more most into over under out up down".split()) | |
| per_story_vocab = [set(w for w in re.sub(r"[^a-z\s]", " ", t.lower()).split() | |
| if w not in stop and len(w) > 3) for t in texts] | |
| shared = Counter(w for v in per_story_vocab for w in v) | |
| ubiquitous = {w: c for w, c in shared.most_common(400) if c >= max(3, int(0.6 * n))} | |
| cad = {} | |
| for pat, label in CADENCE: | |
| cad[label] = sum(1 for t in texts if re.search(pat, t, re.I | re.M)) | |
| reg = {} | |
| for label, pat in REGISTER.items(): | |
| reg[label] = sum(1 for t in texts if re.search(pat, t, re.I)) | |
| return { | |
| "n": n, | |
| "opens_with_The": opens_the, | |
| "opening_frame_match": frame, | |
| "distinct_first_5_words": len({" ".join(t.strip().split()[:5]).lower() for t in texts}), | |
| "top_names": dict(names.most_common(6)), | |
| "max_name_reuse": max(names.values()) if names else 0, | |
| "ubiquitous_words": dict(list(ubiquitous.items())[:12]), | |
| "cadence": cad, | |
| "register_spread": reg, | |
| "registers_present": sum(1 for v in reg.values() if v >= max(2, int(0.15 * n))), | |
| } | |
| def load_pool(tag: str, split: str, prompt_id: str | None): | |
| rows = [json.loads(l) for l in | |
| open(ROOT / "outputs" / f"pool_{tag}" / f"pool_{split}.jsonl") if l.strip()] | |
| by = defaultdict(list) | |
| for r in rows: | |
| by[r["prompt_id"]].append(r) | |
| if prompt_id: | |
| return {prompt_id: by[prompt_id]} | |
| return dict(by) | |
| def load_eval(label: str, out: str): | |
| p = ROOT / out / "samples" / f"{label}_full.json" | |
| data = json.load(open(p)) | |
| return {d["prompt_id"]: [{"text": t, "prompt": d["prompt"]} for t in d["texts"]] | |
| for d in data} | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--pool", help="pool tag, e.g. 4b") | |
| ap.add_argument("--eval", help="eval label, e.g. E0-baseline") | |
| ap.add_argument("--out", default="outputs/eval") | |
| ap.add_argument("--split", default="train") | |
| ap.add_argument("--prompt-id", default=None) | |
| ap.add_argument("--show", type=int, default=16, help="first sentences to print") | |
| ap.add_argument("--limit-prompts", type=int, default=40) | |
| args = ap.parse_args() | |
| groups = load_pool(args.pool, args.split, args.prompt_id) if args.pool \ | |
| else load_eval(args.eval, args.out) | |
| keys = list(groups)[: args.limit_prompts] | |
| agg = defaultdict(list) | |
| for pid in keys: | |
| texts = [r["text"] for r in groups[pid]] | |
| if len(texts) < 4: | |
| continue | |
| a = analyze_group(texts) | |
| agg["opens_with_The"].append(a["opens_with_The"] / a["n"]) | |
| agg["opening_frame"].append(a["opening_frame_match"] / a["n"]) | |
| agg["distinct_openers"].append(a["distinct_first_5_words"] / a["n"]) | |
| agg["max_name_reuse"].append(a["max_name_reuse"]) | |
| agg["registers_present"].append(a["registers_present"]) | |
| for k, v in a["cadence"].items(): | |
| agg[f"cadence:{k}"].append(v / a["n"]) | |
| name = args.eval or f"pool-{args.pool}" | |
| print(f"\n===== QUALITATIVE PROFILE: {name} ({len(keys)} prompts) =====") | |
| for k, v in agg.items(): | |
| print(f" {k:38} {np.mean(v):.3f}") | |
| pid = args.prompt_id or keys[0] | |
| texts = [r["text"] for r in groups[pid]][: args.show] | |
| print(f"\n--- prompt {pid} ---") | |
| print(" ", groups[pid][0].get("prompt", "")[:220]) | |
| print("\nOPENINGS:") | |
| for i, t in enumerate(texts): | |
| print(f" {i+1:2d}. {first_sentence(t)}") | |
| print("\nCLOSINGS:") | |
| for i, t in enumerate(texts): | |
| print(f" {i+1:2d}. ...{last_sentence(t)}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |