Downloads · 30 days
119
18% of all-time downloads
chartreuse-verte/prose-rewriter-4b-v1.6
prose-rewriter-4b-v1.6 is a text generation model from chartreuse-verte. Use it when you need the model to write or continue text. It is set up for transformers. The card lists the license as agpl-3.0.
A paragraph-level prose rewriter: it takes prose written by a large model and re-renders it to be more human, preserving the semantics it was given.
Downloads · 30 days
119
18% of all-time downloads
All-time downloads
664
Public
Parameters
4.4B
16.2 GB on disk
Likes
1
Public
Click a slice to open those files.
.safetensors8.8 GB · 54%
From the Hugging Face model README
A paragraph-level prose rewriter: it takes prose written by a large model and re-renders it to be more human, preserving the semantics it was given.
Qwen/Qwen3-4B-Base with a rank-32 LoRA merged in at strength 1.30.
Successor to prose-rewriter-4b-v1.4.
The version jumps 1.4 to 1.6 to line up with prose-rewriter-1.7b-v1.6: both are trained on the same pool. There is no 4B v1.5.
Better data: cleaned up badly formatted, annotated, unbalanced quotes, miscapitalization, etc. Added contemporary novels to the training pool and a gate to reduce invention.
This release invents less and truncates more. See Evaluation.
| Path | Format | Use with |
|---|---|---|
/ | safetensors bf16, qwen3 arch | transformers |
GGUF/prose-rewriter-4b-v1.6-Q8_0.gguf | GGUF Q8_0, 4.69 GB | llama.cpp / llama-cpp-python |
GGUF/prose-rewriter-4b-v1.6-Q4_K_M.gguf | GGUF Q4_K_M, 2.72 GB | llama.cpp / llama-cpp-python |
The quants carry the chat template and stop on <|im_end|>, and the adapted
output head is kept separate from the token embeddings in both — Q8_0 stores it
at Q8_0, Q4_K_M at Q6_K.
<|im_start|>source
{paragraph}<|im_end|>
<|im_start|>edit
match<|im_end|>
<|im_start|>rewrite
The chat template in this repo builds exactly that string, byte for byte, from two roles:
messages = [
{"role": "source", "content": paragraph},
{"role": "edit", "content": "match"},
]
tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
It is not a chat model. The template rejects an edit value outside the three
modes rather than quietly building a prompt the weights have never seen. Any
other role is treated as the source paragraph, so a runtime that probes the
template with a user message still gets a valid prompt.
edit block is mandatoryedit names which of three length transforms is being asked for. The values
describe the input, not the instruction. They say what kind of text you are
handing over:
edit | what it says about the input | what the model does |
|---|---|---|
match | the source is about the length it should be | rewrite in place |
inflate | the source is padded relative to what it should be | cut |
compress | the source is flattened and too short | open it back out |
match is the setting for "rewrite it, do not trim it". It's strongly recommended
you use this mode.
Sending no block is the worst thing you can do to this checkpoint. It was trained with the block, so omitting it collapses the model onto its deletion-heaviest mode.
Sampled at temperature=0.9, top_p=0.9. Temperature 0.9 has been tested
internally to be the most optimal value. It's recommended you use this.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
repo = "chartreuse-verte/prose-rewriter-4b-v1.6"
tok = AutoTokenizer.from_pretrained(repo)
model = AutoModelForCausalLM.from_pretrained(repo, dtype=torch.bfloat16, device_map="cuda").eval()
def rewrite(paragraph, mode="match"):
text = tok.apply_chat_template(
[{"role": "source", "content": paragraph},
{"role": "edit", "content": mode}],
tokenize=False, add_generation_prompt=True,
)
ids = tok(text, return_tensors="pt", add_special_tokens=False).input_ids.to(model.device)
out = model.generate(ids, max_new_tokens=512, do_sample=True, temperature=0.9, top_p=0.9)
return tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True).strip()
Same thing under llama.cpp. The roles are source and edit, which no chat API
models, so build the string yourself; <|im_end|> stops it:
llama-cli -m GGUF/prose-rewriter-4b-v1.6-Q8_0.gguf -no-cnv -n 512 --temp 0.9 --top-p 0.9 \
-p '<|im_start|>source
{paragraph}<|im_end|>
<|im_start|>edit
match<|im_end|>
<|im_start|>rewrite
'
The training pool's median input is 42 words and most of it is under 70, so serve it on anything from a full sentence up.
The practical floor is about 15 words. Below it the failure mode is padding and fabrication rather than gibberish: the model stretches the line toward its learned length and adds material the input never supported. Below 80 bytes, pass the text through unchanged.
Both releases measured as they ship — v1.4 baked at strength 1.20, v1.6 at
1.30 — on 365 held-out paragraphs of LLM-written prose that neither model saw in
training, both sent the identical prompt at temperature=0.9, top_p=0.9, three
swipes each. 1,095 generations an arm.
| v1.4 | v1.6 | |
|---|---|---|
| words changed | 36.3% | 37.3% |
| passed through unchanged | 5.6% | 3.8% |
| near-verbatim outputs | 2.5% | 1.1% |
| below the training edit floor | 53.2% | 50.1% |
| sentence count moved | 75.6% | 77.2% |
| sentence-length variety vs input | +0.111 | +0.127 |
| words kept from the input | 0.683 | 0.670 |
| length preserved | 0.883 | 0.864 |
| truncated below 0.75x | 13.2% | 17.5% |
| repeated 3-grams | 0.004 | 0.004 |
Unsupported content — a sentence in the rewrite the input does not entail, scored by NLI with the input alone as the premise:
| v1.4 | v1.6 | |
|---|---|---|
| rewrites with an unsupported sentence | 10.3% | 8.6% |
| unsupported sentences, all rewrites | 9.8% | 8.0% |
| coverage of the input (reverse entailment) | 0.545 | 0.519 |
The register numbers on the same paragraphs, unpaired and against the input:
| input | v1.4 | v1.6 | human corpus | |
|---|---|---|---|---|
| banned constructions /1k | 7.52 | 2.50 | 2.64 | 0.00 |
| slop lexicon density | 0.081 | 0.051 | 0.048 | 0.018 |
| purple score | 0.498 | 0.322 | 0.319 | — |
Every number above is measured at the strength the release is baked at, which is
not the same as measuring the adapter. LoRA strength is spent at merge time
(W + (B @ A) * 2.60 here), and the metrics move with it: read at the adapter's
natural 1.0 this same checkpoint is a materially different artifact.
Corrupt forward, train backward. The human paragraph is the target; an on-policy LLM manufactures the input by slop-ifying it.
The target side is human prose: r/WritingPrompts
(Mollymo/Human-to-AI-writing),
AO3
(midwestern-simulation-active/ao3_random_subset),
a scrape of bluemoonroleplaying.com — the only human writing in the pool
already in the deployment's own register — and a sliver of fanfiction.net
(atom-in-the-universe/fanfics-10k-10k).
Professionally edited fiction. Every source above is amateur archive prose. Published contemporary literary fiction now supplies about a fifth of the target side — the first professionally edited register in the pool, added because it is the one class that clears the corruptor's own output by a margin wide enough to register on vocabulary breadth.
The input side is manufactured from those targets, weighted and share-capped so that no single generator's tics dominate:
| pool axis | composition |
|---|---|
| rows | 28,985 over 22,751 distinct target paragraphs |
| corruption band | medium 36%, heavy 33%, light 28%, identity/no-op 3% |
len_mode | match 56%, inflate 34%, compress 7% |
| kind | prose 94%, dialogue 6%, structural no-ops 1% |
| target source | r/WritingPrompts 37%, AO3 29%, published fiction 20%, roleplay forum 13%, fanfiction.net 1% |
Pairs pass invariant gates before they reach the GPU: POV, tense, who is in the scene, grammatical correctness on the target side, content recall stratified by target length, and NLI entailment both ways. Two further screens shape what reaches training — a floor on how much a pair actually changes, and a floor on the sentence-length variety of the target, applied at a higher threshold for long paragraphs than short ones.
Loss on the target paragraph only. Everything before rewrite is masked.
| LoRA | r=32, alpha=64, dropout 0.05 |
| target modules | q, k, v, o, gate, up, down, and lm_head |
| trainable | 71,004,160 params (1.73%) |
| schedule | 2 epochs, lr 1e-4 cosine, batch 8 × accum 4, seq 2048 |
| steps | 1,774 on one RTX 3090 |
Merged at strength 1.30. Rank 32 with alpha 64 is a LoRA scaling of 2.0, so the
effective scaling is 2.60: W + (B @ A) * 2.60. Merged in float32, stored
bfloat16.
lm_head is adapted, and Qwen3-4B-Base ties lm_head.weight to
embed_tokens.weight. This checkpoint is untied: the merged output head is
stored separately and the input embeddings are bit-identical to the base
model's, which is what training assumed. config.json says
tie_word_embeddings: false and it means it. Do not re-tie it, and if you
convert to another format, check that the head survived.
The weights in this repository are released under the GNU Affero General
Public License, version 3. The full text is in LICENSE.
This is a derivative of Qwen/Qwen3-4B-Base,
which is licensed under Apache License 2.0. That license is preserved and
its terms continue to apply to the base weights this model was built from; the
AGPL covers the combined work as distributed here. Apache-2.0 is one-way
compatible with AGPLv3, which is what makes this combination possible.
If you run a modified version of this model as a network service, AGPL section 13 requires you to offer the corresponding source of your modifications to its users.