Stop sounding like everyone else's AI.
Draft LinkedIn posts and articles in your voice on your machine. Index your writing, measure your cadence, retrieve short rhythm references from your own pieces, and generate with a local quantized model (~5–7 GB). Your corpus never leaves your Mac.
Built by Telivity. Apache-2.0.
your writing ──► ingest ──► index-voice ──► build-style-profile ──► write [--channel post|article]
No fine-tune is required to get a draft. write runs the local base model with your retrieval index plus your measured style card (adapter=none).
The feed is drowning in AI writing that all sounds the same — “In today’s fast-paced world, we must leverage synergies…” A pasted style guide helps a little. It still isn’t you.
Cloud fine-tunes are a non-option for personal writing. Notes, emails, and posts are biometric-adjacent. Shipping them to a rented GPU so someone else’s stack can imitate you is a strange bargain.
PersonalityProtect keeps the corpus on disk, measures your cadence, retrieves short rhythm references from your own writing, and drafts locally with MLX on Apple Silicon. Treat outputs as drafts you still own.
- Ingest your LinkedIn export and/or local notes (stays on disk).
selectgates the corpus by length (--min-words, default 50) and an optional year cap (--through-year, default: current year).index-voicebuilds a local retrieval index.build-style-profilemeasures cadence from the selection (sentence length, short lines, post length band, banned filler).write --topic --pointsdrafts from the brief only; retrieved pieces are rhythm reference.
Two channels come out of step 5, and each takes its length from its own pieces:
--channel post(default) targets your long-post band, up to the LinkedIn ~3000-character limit (~550 words).--channel articleruns outline → sections → stitch. Total length comes from the p50/p75/p90 of yourlinkedin_articlepieces, split across the outline; the post ceiling never applies. Needs at least fivelinkedin_articlepieces both in the corpus and in the voice index.
Local LoRA training stays in the CLI as an experiment, not as the path to a first draft — see Advanced.
Requires Python 3.10+ and an Apple Silicon Mac: write runs on MLX and needs Metal.
git clone https://github.com/TelivityAI/personality-protect.git
cd personality-protect
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev,mlx]"
personality-protect init
personality-protect download --format mlx # ~6 GB, once
personality-protect ingest --linkedin ~/path/to/linkedin-export
personality-protect ingest --path ~/path/to/notes --source note
personality-protect select
personality-protect index-voice
personality-protect build-style-profile
# LinkedIn post (targets your long-post band, up to ~550 words / ~3k chars)
personality-protect write \
--topic "Contoso Ledger exceptions" \
--points "- Name one owner\n- Keep the rollout boring"
# Article (outline → sections → stitch)
personality-protect write \
--channel article \
--topic "Contoso Ledger guide" \
--points "- Name one owner\n- Cut exceptions\n- Keep rollbacks boring"
personality-protect statusThat is the whole path to a draft — no training step.
Optional extras:
pip install -e ".[models]" # Hugging Face download helper
pip install -e ".[gguf]" # llama.cpp GGUF (optional)
pip install -e ".[cuda]" # NVIDIA path (optional)Public docs use synthetic Contoso / synergy-slop text only. No personal corpus.
write (post + article) |
status |
Setup |
|---|---|---|
![]() |
![]() |
![]() |
personality-protect select
personality-protect index-voice
personality-protect build-style-profile
personality-protect write --topic "Contoso pricing" --points "Name one owner."
personality-protect statusOptional smoke tour — runs the write path with a stubbed model call (no download; synthetic Contoso only):
Smoke tour (demo) |
Mark |
|---|---|
![]() |
![]() |
personality-protect demoHard rule: personal writing stays on your machine.
Stays in ~/.personality-protect/ |
Never commit / never upload |
|---|---|
| Profiles, corpus index, voice index, style profile | Real LinkedIn / email / note exports |
Writer LoRA adapters under adapters/ |
Profile URLs, personal paths |
Downloaded weights under models/ / HF cache |
API keys, .env, tokens |
| Local eval receipts | Cloud train uploads |
Override the home directory with --home or PERSONALITY_PROTECT_HOME.
.gitignore blocks profiles, adapters, SFT, exports, weights, and secrets. Public git ships code + synthetic demo/eval fixtures only. Hugging Face is used only to download public quantized base weights.
This README uses synthetic examples only (e.g. Contoso, “leverage synergies”). Do not paste real posts, profile URLs, or personal before/after samples into docs or PRs.
write requires an Apple Silicon Mac. Drafting runs Qwen3.5-9B 4-bit through MLX/Metal; there is no cloud fallback.
| What | Size / note |
|---|---|
MLX 4-bit base (what write loads) |
~6 GB download, once |
| Peak RAM while writing | Memory-capped; 16 GB+ recommended |
GGUF Q4_K_M (optional, for filter) |
~5.6 GB download |
| Writer LoRA (optional) | Small (MBs) under the profile |
MLX applies a wired-memory cap so Metal does not jetsam-kill Python on mid-size Macs.
State lives in ~/.personality-protect/profiles/<name>/.
personality-protect init
personality-protect download --format mlx
personality-protect ingest --linkedin ~/path/to/linkedin-export.zip
personality-protect ingest --path ~/path/to/notes --source noteselect is required before build-style-profile (the style card reads selection.json). It is a length gate plus an optional year cap:
personality-protect select
personality-protect select --min-words 75 --include-undated
personality-protect select --through-year 2024 # deliberate narrowing only
personality-protect index-voice
personality-protect build-style-profileDefaults: ≥50 words, dates through the current year. Use --through-year when you intentionally want an older slice. Corpus gates: warn below 50 selected pieces; block below 20 unless --force. Holding pieces back from retrieval is separate — index-voice --holdout-id, scored by eval-write-holdout.
Length targets are per channel and never borrow across channels:
| Channel | Measured from | Aim | Ceiling |
|---|---|---|---|
| post | linkedin_post pieces |
p75 (floor 300) | p90, clamped to ~550 words (~3000 chars) |
| article | linkedin_article pieces |
median, clamped to 600–3000 words | p90, clamped to 3000 words |
The article aim is divided across the outline to get a per-section budget (clamped to 180–600 words), so a five-section article asks for five short sections rather than five posts. With no linkedin_article pieces in the corpus, the article aim falls back to a stated default of 1100 words instead of borrowing the post band.
personality-protect write --topic "…" --points "…"
personality-protect write --channel article --topic "…" --points "…"
personality-protect write --topic "…" --points "…" --json--topic and --points are the only content the draft may use; retrieved pieces supply rhythm, not facts. Every write above runs base weights (adapter=none).
On --channel article, each --points bullet becomes a section (2–8), retrieval is restricted to linkedin_article pieces so posts cannot become the rhythm reference, and sections that restate each other (including paraphrases) are dropped before stitching. Each section prompt sees only its own bullet — not the full brief dumped into every call — plus titles of the other sections so it does not rewrite them. Exemplar clips strip export chrome (duplicate titles, Created/Published lines) so the word budget is real prose. The style card for this channel uses article-only cadence when enough articles were measured, not the comment-dominated corpus card. Section prompts also use an article-specific system rule set: the BRIEF is the only fact source, allowed names/figures from the BRIEF are listed explicitly, and thin briefs lower the per-section word aim. A section that invents is repaired before it is dropped: the regenerate names the fabricated entities and figures it has to lose, and if they survive that, the sentences carrying them are cut and the section is re-checked. Only a section with nothing left is omitted, and the whole article is flagged for invention only when the stitch is empty or the stitched text still invents. The channel refuses to draft unless at least five linkedin_article pieces are in the corpus and five are in the voice index — a large carve that leaves retrieval empty is an error, not a silently thinner draft.
The post channel is scored by eval-write-holdout. The article channel has its own carve and eval:
personality-protect select-article-holdouts # report only
personality-protect select-article-holdouts --apply
personality-protect index-voice --from-carve # holdouts leave retrieval
PP_MLX_ALLOW=1 personality-protect eval-write-article --out receipt.jsonThe carve is deterministic (blake2b(piece_id) order), keeps previously carved ids pinned, and never drops the voice index below the five-article floor. Each holdout is reduced to a lossy brief — a topic plus 3–6 section bullets drawn one per segment of the piece, capped at 60 words and 10% of the source — so neither arm is handed the article back to paraphrase.
Two arms then write the same brief with the same outline, per-section budget, trim, and invent repair. The product arm gets retrieved exemplars and the measured style card; the control arm gets neither. Invention is judged against the visible brief both arms saw (not the full source article). Drafts are scored on distance to the holdout's own cadence axes, and a draft that parrots its exemplars, echoes the brief, or invents entities or figures is disqualified regardless of distance. Receipts carry ids, distances, and flags — never draft or corpus text.
The verdict needs all three of: the article arm wins the majority, the margin clears --alpha (default 0.10) on a one-sided sign test, and it is not disqualified more often than the control. When both arms are disqualified on every holdout, distance never decided anything, and the receipt says so (distance_ever_decided: false) rather than reporting it as a cadence loss.
personality-protect status
personality-protect api # loopback 127.0.0.1 onlyGlobal flags (most commands): --profile, --home, --json, plus branding --color, --logo, --logo-mode.
| Command | Purpose |
|---|---|
init |
Create profile under ~/.personality-protect/ |
download |
Prefetch quantized MLX or GGUF base |
ingest |
Index LinkedIn export and/or local paths |
dedupe-index |
Report pieces repeating another piece's text; --apply backs up then rewrites the index |
index-voice |
Build local voice retrieval index |
build-style-profile |
Build cadence / length / banned-filler style card |
write |
Draft a post or article (--channel post|article) |
eval-write-holdout |
Score post-channel writes on held-out pieces (local receipt) |
select-article-holdouts |
Deterministic article carve that respects the retrieval floor |
eval-write-article |
Score article-channel writes against a no-voice control (local receipt) |
status |
Show profile state |
demo |
Optional synthetic smoke tour of the write path (no download) |
api |
Loopback HTTP stub |
logo |
Print Telivity CLI mark |
build-writer-sft, train |
Optional LoRA experiments — see Advanced |
| Flag | Meaning |
|---|---|
--topic |
What the piece is about |
--points |
Facts/claims the draft may use |
--channel post|article |
Post (default) or article outline→sections→stitch |
--k |
Rhythm exemplars to retrieve |
--adapter / --no-adapter |
Default --no-adapter (base weights); --adapter needs a trained LoRA |
--json |
Machine-readable receipt |
| Flag | Meaning |
|---|---|
--holdout-id |
Piece id never indexed (repeatable) |
--save-raw |
Local prompts/drafts under the profile (never commit) |
--out PATH |
Contoso-safe aggregate receipt JSON |
| Flag | Meaning |
|---|---|
--holdout-id |
Article id to score (repeatable); defaults to the saved carve |
--k |
Article exemplars retrieved per section |
--alpha |
One-sided significance the run must reach (default 0.10) |
--save-raw |
Local prompts/drafts under the profile (never commit) |
--out PATH |
Contoso-safe receipt JSON |
| Flag | Meaning |
|---|---|
--apply |
Write the carve (default is report-only) |
--fraction |
Share of briefable articles to reserve |
--min / --max |
Carve size band (4–5; three cannot reach --alpha on a sign test) |
--keep-indexed |
Articles the carve must leave in retrieval (default 5) |
Nothing here is needed for a draft. These commands stay in the CLI for local experiments and receipts.
write defaults to base weights. The adapter path exists so a trained LoRA can be loaded, and --adapter errors out when no adapter is present:
personality-protect build-writer-sft
personality-protect select-writer-holdouts --apply
personality-protect index-voice --from-carve
personality-protect train --writer --backend mlx
personality-protect eval-writer-adapter --archive-on-fail
personality-protect write --adapter --topic "…" --points "…"build-writer-sft builds de-voiced brief→post pairs. train --writer uses a short writer recipe (3 epochs) and keeps per-chunk checkpoints under adapters/latest/checkpoints/. Keep an adapter only when eval-writer-adapter decides keep; otherwise archive it and stay on adapter=none. Training is not a prerequisite for write.
filter, compare, eval, and the translator-pair commands remain available. They score or rewrite existing text and are not part of the drafting path above. (select is part of the drafting path — see Select, index and style.)
scripts/beast_demo.sh drives the older select → train → compare → eval sequence, not write. Use it for train/compare runs only:
chmod +x scripts/beast_demo.sh
./scripts/beast_demo.sh --linkedin ~/path/to/linkedin-export
./scripts/beast_demo.sh --skip-download # synthetic smokeOperator checklist: docs/LAUNCH.md.
pip install -e ".[dev]"
pytest
ruff check src tests scriptsCI (.github/workflows/ci.yml) required checks: lint, test (3.11), test (3.12), sanitize, cli-smoke.
scripts/shot.py renders captured ANSI terminal bytes to PNG on a fixed character grid (so Rich box-drawing lines up). Needs pillow.
export PERSONALITY_PROTECT_HOME=/tmp/shots COLUMNS=94 TERM=xterm-256color
pip install pillow
script -qec "personality-protect --logo off demo" /dev/null \
| python3 scripts/shot.py docs/images/cli-demo.png "personality-protect demo"
script -qec "personality-protect --logo off status" /dev/null \
| python3 scripts/shot.py docs/images/cli-status.png "personality-protect status"Do not regenerate docs/images/cli-shipped.png off Apple Silicon — it shows a real write against MLX weights.
Apache-2.0. See LICENSE.




