I often want to share a real piece of writing with an AI tool, or post it on a forum, without pointing at the real people and places in it. A story about my week. An email thread. Notes from a call.
The usual answer is redaction. That turns "I'm Daniel and I live in Jerusalem" into "I'm [NAME] and I live in [CITY]". It's safe, but it wrecks the text. Anything you hand to an AI tool afterwards is reasoning over a form with holes in it.
What I wanted was something closer to what a person would do by hand: change Daniel to David, Jerusalem to Tel Aviv, and keep doing that the same way all the way through. The text still reads naturally. It still means roughly the same thing. It just isn't about me any more.
So I trained a small model to do it: Proximate-Pseudonymizer-0.6B. It's a proof of concept, and it still leaks. I'll be upfront about how much.
Hugging Face Model danielrosehill/Proximate-Pseudonymizer-0.6B Try it: public demo Space · Write-up: blog post (runs on CPU and shows a word-level diff of every swap, so you can spot what it missed). Text generationAPACHE-2.0Updated Oct 2026 View model on Hugging FaceWhat it does
It's text in, text out. You give it a text, and it gives you back the same text with each identifying detail swapped for a close, plausible substitute. "Close" is the important word (hence "proximate"). A name keeps its gender and culture. A city becomes a similar city in the same country. A date moves by a few days or weeks and keeps its year. A phone number keeps its format.
The one hard rule is consistency. If Daniel becomes David in the first line, he's David in the last line too, and "Daniel Rosen" becoming "David Cohen" means "Daniel" on its own becomes "David" as well.
It's not a redactor. There are no tags in the output, and the published model doesn't hand back a mapping. You get plain text.
Here's a short example from the held-out test set. Like all of the training data, it's synthetic, not a real person. Before:
Rhys Morgan (34) said the Bluebell Cafe in Leeds had lost the deposit on his van, reg YK19 RWT, and asked us to chase it by 14 August.
And what the model returned:
Ewan Pryce (36) said the Harriet Lane Cafe in Bristol had lost the deposit on his van, reg EP27 KTR, and asked us to chase it by 19 August.
All six details changed. The name is still Welsh-sounding, the city is still a large English one, the reg plate is still a UK format and the date is still in August. Everything else is word for word.
It doesn't always go that well. In another test text about two school friends, "Farida Haddad" became "Fares Haddad" (the surname kept, and the gender flipped), and "Nadia Chen" came through untouched. That's the kind of miss the numbers below are counting.
Why a small model
It's a full fine-tune of Qwen/Qwen3-0.6B, released under Apache-2.0. At 0.6B parameters it runs on a laptop CPU. That matters for this job in particular. The whole point is not sending the original text anywhere, so the thing doing the rewriting should be on your own machine.
How I made it
The training data is synthetic. I asked Claude Haiku 5.5 for 5,000 texts (emails, meeting notes, application essays, forum posts and so on, from a sentence up to a few paragraphs), each with a replacement map: every identifying detail and the substitute it should get. 4,930 came back in a usable shape. Generating them through the Batches API cost about $1.06.
The part I'd keep for any future project like this: the training targets were never written by the LLM. Code produced them by applying the map to the text. That makes every target consistent by construction, and nothing outside the swapped spans can change. The only thing that can be wrong is the map itself, so that's what the checks look at.
The checks are code too. Maps that contradicted themselves or left a detail behind were dropped. A second pass caught substitutes that were too close to the original to hide anything: "Carlo" to "Carlos", a kept first name ("Tobias Vogt" to "Tobias Wendt"), or a social handle that keeps its template and nudges the year. Those swaps were sent back to Haiku to be re-picked, and dropped if the new pick failed again. I also built a small review Space on Hugging Face to look through the pairs by hand. After going through a batch I decided they were usable as they were, and the too-close check was the one flaw worth fixing.
That left 4,280 training texts and 295 held-out test texts. Training ran for 3 epochs on a single L4 GPU on Modal and took about 38 minutes. The whole thing, data generation included, came in under $10.
How well does it work?
On the 295 held-out texts:
Leak: 9.3%. That's the share of identifying details that survive unchanged in the output. Lower is better, and it's the number that matters.
Consistency: 0.99. The share of texts where each changed detail got exactly one substitute.
Rest of the text preserved: 0.998. The share of non-identifying words left alone.
Clean texts left untouched: 0.962. Texts with nothing to hide, returned as they were.
For comparison, the untrained Qwen3-0.6B, given one worked example, leaked 85.6% of the details and rewrote a good chunk of everything else too (it kept only 0.791 of the other words).
The first training run (2 epochs at a lower learning rate) leaked 26.5%. Training longer at a higher learning rate, on the same model and the same data, brought it to 9.3%. So the first run was under-trained, not too small.
Whether a substitute is actually close (as opposed to just different) isn't scored automatically. The leak metric is also slightly pessimistic: a short detail like an age counts as leaked if the same number happens to appear elsewhere in the output.
Not safe to run unsupervised
About one identifying detail in eleven survives. Names, dates and ages leak most. That's fine for showing the idea works. It's not fine for privacy.
If you try it, read every output before you share it. The targets I set for myself: 1% leak or less with a human reviewing every output, and around 0.1% or less before it could run without one (and even then only for low-stakes text). This checkpoint is at about 9%.
The policy behind the substitutions is also my own choice, and you may not share it. Big cities get replaced too. Countries and public figures are kept. Dates keep their year. The settings in the training data are weighted towards Israel, Ireland and the US, and 80% of the texts are English. Other languages work, but less well. The generator prompt is in the repo, so a different policy means editing it, regenerating the data and fine-tuning further from this checkpoint.
What already exists
I looked before building it. Roughly, the existing options fall into four groups:
Redaction and masking (token classifiers such as
StanfordAIMI/stanford-deidentifier-base, or the ai4privacy models). These find the spans and tag them. The text doesn't survive.Detect, then random replace (Presidio with Faker). The text reads, but the substitutes are random rather than close, so "Daniel in Jerusalem" can become "Bartholomew in Ulaanbaatar". Keeping them consistent is up to you.
Proximate replacement as a tool call (
eternisai/Anonymizer, also built on Qwen3). This is the closest prior art, and its replacement rules informed my generator prompt. It returns a list of replacements for your code to apply rather than text, it keeps cities of over a million people (so Jerusalem would stay Jerusalem), and it's licensed CC-BY-NC-4.0.Seq2seq pseudonymization (
grammarly/pseudonymization-seq2seq, BART, 2023). Text in, text out, but English only, and its training targets came from an NER pipeline, so the substitutes aren't proximate.
Mine is plain text out, replaces big cities too, and is Apache-2.0.
What's next
Training is paused at this checkpoint. The model card lists what I think would close the gap, and none of it looks like a dead end:
Longer training. That was the lever between the first run and this one.
A larger base model. Spotting what counts as an identifying detail is mostly world knowledge, which grows with size. Qwen3-4B-Instruct is the obvious candidate.
A swap-list output mode. It's already implemented in the repo. The model lists original-to-substitute pairs and code applies them, which makes consistency and preservation exact rather than learned. Its main failure in early probes was writing the original back as its own substitute. A decoding-time repair that re-asks those slots, with the original's first token banned, fixed 222 of 223 cases.
More data and a scored closeness check, so that "close but not too close" is something training actually optimises.
Try it
Demo Space: Proximate-Pseudonymizer-Demo
Code, data pipeline and run log: Proximate-Pseudonymizer on GitHub
Model page on my site: danielrosehill.com/ai/models/proximate-pseudonymizer-0.6b