From ad831bb095b075e26e3408edd3c600b4c10552cd Mon Sep 17 00:00:00 2001 From: Dusan Milicevic Date: Thu, 30 Jul 2026 10:37:27 -0500 Subject: [PATCH 1/3] Add long-post budgets, article channel, and writer LoRA path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post length targets now use linkedin_post p75/p90 (LinkedIn ~3k-char ceiling). write --channel article outlines, drafts sections, and stitches. build-writer-sft / train --writer / write --adapter wire a brief→post LoRA without making the translator the product path. README matches. Co-authored-by: Cursor --- README.md | 141 +++++++------- src/personality_protect/cli.py | 63 ++++-- src/personality_protect/style_profile.py | 78 ++++++-- src/personality_protect/train.py | 20 +- src/personality_protect/voice_index.py | 11 +- src/personality_protect/write.py | 60 +++++- src/personality_protect/write_article.py | 232 +++++++++++++++++++++++ src/personality_protect/writer_sft.py | 139 ++++++++++++++ tests/test_style_profile.py | 31 +++ tests/test_write.py | 6 +- tests/test_write_article.py | 115 +++++++++++ tests/test_writer_sft.py | 84 ++++++++ 12 files changed, 863 insertions(+), 117 deletions(-) create mode 100644 src/personality_protect/write_article.py create mode 100644 src/personality_protect/writer_sft.py create mode 100644 tests/test_write_article.py create mode 100644 tests/test_writer_sft.py diff --git a/README.md b/README.md index 6e47f9a..e06db79 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ **Stop sounding like everyone else's AI.** -Draft LinkedIn posts in *your* voice on your machine. Index your writing, retrieve a few of your own posts for rhythm, and generate from a quantized local model (~5–7 GB). Your corpus never leaves this Mac. +Draft LinkedIn posts and articles in *your* voice on your machine. Index your writing, measure cadence, retrieve short rhythm references, and generate with a local quantized model (~5–7 GB). Optional writer LoRA deepens voice after it beats RAG-alone on holdouts. Your corpus never leaves this Mac. Built by [Telivity](https://telivity.com). Apache-2.0. ```text your writing ──► ingest ──► index-voice ──► build-style-profile ──► write + └── optional: train --writer ──► write --adapter ```

@@ -22,7 +23,22 @@ The feed is drowning in AI writing that all sounds the same — *“In today’s 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 posts, and drafts locally with MLX on Apple Silicon. Treat outputs as drafts you still own. +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. + +--- + +## How you get voice + +1. **Ingest** your LinkedIn export and/or local notes (stays on disk). +2. **`index-voice`** builds a local retrieval index. +3. **`build-style-profile`** measures cadence (sentence length, short lines, post length band, banned filler). +4. **`write --topic --points`** drafts from the brief only; retrieved posts are rhythm reference. + +Optional: + +- **`train --writer`** trains a brief→post LoRA from your posts (holdouts excluded). +- **`write --adapter`** loads that LoRA when `adapters.safetensors` is present. +- **`write --channel article`** runs outline → sections → stitch (needs enough `linkedin_article` pieces). --- @@ -40,19 +56,34 @@ pip install -e ".[dev,mlx]" personality-protect init personality-protect download --format mlx # ~6 GB, once -# Your exports stay on this machine personality-protect ingest --linkedin ~/path/to/linkedin-export personality-protect ingest --path ~/path/to/notes --source note 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. Keep the rollout boring." + --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 status ``` +Optional writer LoRA: + +```bash +personality-protect build-writer-sft +personality-protect train --writer --backend mlx +personality-protect write --adapter --topic "…" --points "…" +``` + Optional extras: ```bash @@ -93,8 +124,9 @@ personality-protect demo | Stays in `~/.personality-protect/` | Never commit / never upload | | --- | --- | | Profiles, corpus index, voice index, style profile | Real LinkedIn / email / note exports | -| Downloaded weights under `models/` / HF cache | Profile URLs, personal paths | -| Local eval receipts | API keys, `.env`, tokens | +| 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`. @@ -112,7 +144,8 @@ This README uses synthetic examples only (e.g. Contoso, “leverage synergies” | --- | --- | | MLX 4-bit base (default write) | ~6 GB download | | GGUF Q4_K_M (optional) | ~5.6 GB download | -| Peak RAM while writing | Memory-capped; typically comfortable on 16 GB+ | +| Writer LoRA | Small (MBs) under the profile | +| Peak RAM while writing / training | Memory-capped; 16 GB+ recommended | MLX applies a wired-memory cap so Metal does not jetsam-kill Python on mid-size Macs. @@ -122,34 +155,13 @@ MLX applies a wired-memory cap so Metal does not jetsam-kill Python on mid-size State lives in `~/.personality-protect/profiles//`. -### Init +### Init / download / ingest ```bash personality-protect init -personality-protect init --profile work -``` - -### Download - -```bash -personality-protect download --format mlx # → Hugging Face cache, ~6 GB -personality-protect download --format gguf # → ~/.personality-protect/models/*.gguf -``` - -### Ingest - -LinkedIn export (folder or `.zip`) — CSV/HTML read in place; zips unpack only into the profile cache: - -```bash -personality-protect ingest --linkedin ~/path/to/linkedin-export +personality-protect download --format mlx personality-protect ingest --linkedin ~/path/to/linkedin-export.zip -``` - -Local docs / notes / mail archives (read in place — **no mandatory copy**): - -```bash personality-protect ingest --path ~/path/to/notes --source note -personality-protect ingest --path ~/path/to/mail-archive --source email ``` ### Index and style @@ -159,32 +171,33 @@ personality-protect index-voice personality-protect build-style-profile ``` -`index-voice` builds a local retrieval index from your corpus. `build-style-profile` measures cadence targets (sentence length, short lines, typical post length, banned filler) used by `write`. +Post length targets come from `linkedin_post` pieces (p75/p90), clamped to the LinkedIn ~3000-character band (~550 words). ### Write ```bash -personality-protect write \ - --topic "Contoso Ledger exceptions" \ - --points "Name one owner. Keep the rollout boring." +personality-protect write --topic "…" --points "…" +personality-protect write --channel article --topic "…" --points "…" +personality-protect write --adapter --topic "…" --points "…" personality-protect write --topic "…" --points "…" --json ``` -`--topic` and `--points` are the only content the draft may use. Retrieved posts are rhythm reference only — facts come from the brief. +`--topic` and `--points` are the only content the draft may use. Article channel requires at least five `linkedin_article` pieces in the corpus. -### Status +### Writer LoRA ```bash -personality-protect status +personality-protect build-writer-sft +personality-protect train --writer --backend mlx ``` -### Local API stub +Keep the adapter only if it improves holdout drafts vs RAG-alone (`eval-write-holdout`). Delete it if it does not. -Loopback only (`127.0.0.1`). Refuses non-local binds. Future browser-extension hook. +### Status / API ```bash -personality-protect api -# GET http://127.0.0.1:8765/health +personality-protect status +personality-protect api # loopback 127.0.0.1 only ``` --- @@ -199,40 +212,28 @@ Global flags (most commands): `--profile`, `--home`, `--json`, plus branding `-- | `download` | Prefetch quantized MLX or GGUF base | | `ingest` | Index LinkedIn export and/or local paths | | `index-voice` | Build local voice retrieval index | -| `build-style-profile` | Build cadence / banned-filler style card | -| `write` | Draft a post from topic + points | +| `build-style-profile` | Build cadence / length / banned-filler style card | +| `build-writer-sft` | Build brief→post SFT for writer LoRA | +| `write` | Draft a post or article (`--channel`, optional `--adapter`) | +| `train` | Local LoRA train (`--writer` for writer LoRA) | | `eval-write-holdout` | Score write quality on held-out pieces (local receipt) | | `status` | Show profile state | | `demo` | Optional synthetic smoke tour (no download) | | `api` | Loopback HTTP stub | | `logo` | Print Telivity CLI mark | -### Important flags - -**`download`** - -| Flag | Meaning | -| --- | --- | -| `--format mlx\|gguf` | Which quantized artifact to fetch | - -**`ingest`** +### `write` flags | Flag | Meaning | | --- | --- | -| `--linkedin PATH` | LinkedIn export folder or `.zip` | -| `--path PATH` | Local docs/notes/mail (repeatable) | -| `--source NAME` | Label for `--path` sources | - -**`write`** - -| Flag | Meaning | -| --- | --- | -| `--topic` | What the post is about | +| `--topic` | What the piece is about | | `--points` | Facts/claims the draft may use | -| `--k` | How many rhythm exemplars to retrieve | +| `--channel post\|article` | Post (default) or article outline→sections→stitch | +| `--k` | Rhythm exemplars to retrieve | +| `--adapter` / `--no-adapter` | Load writer LoRA when present (default off) | | `--json` | Machine-readable receipt | -**`eval-write-holdout`** +### `eval-write-holdout` flags | Flag | Meaning | | --- | --- | @@ -244,16 +245,7 @@ Global flags (most commands): `--profile`, `--home`, `--json`, plus branding `-- ## Advanced (optional) -These commands are available for experimentation. The shipped path above does not require them. - -```bash -personality-protect select -personality-protect train --backend mlx -personality-protect filter --text "…" -personality-protect compare --synthetic slop_branding -``` - -See `personality-protect train --help` and `filter --help` for flags. Adapters, when used, stay under `~/.personality-protect/profiles//adapters/`. +Other experiment commands (`select`, `filter`, `compare`, translator pairs) remain in the CLI. The shipped path above does not require them. --- @@ -264,8 +256,7 @@ Operator checklist: [docs/LAUNCH.md](docs/LAUNCH.md). ```bash chmod +x scripts/beast_demo.sh ./scripts/beast_demo.sh --linkedin ~/path/to/linkedin-export -# synthetic smoke (no personal data, no multi-GB download): -./scripts/beast_demo.sh --skip-download +./scripts/beast_demo.sh --skip-download # synthetic smoke ``` --- diff --git a/src/personality_protect/cli.py b/src/personality_protect/cli.py index 21a978b..0910dec 100644 --- a/src/personality_protect/cli.py +++ b/src/personality_protect/cli.py @@ -504,11 +504,42 @@ def build_style_profile_cmd( console.print(f"Saved: {out_path}") +@app.command("build-writer-sft") +def build_writer_sft_cmd( + ctx: typer.Context, + profile: str = typer.Option(DEFAULT_PROFILE, "--profile"), + home: Optional[Path] = typer.Option(None, "--home"), + as_json: bool = typer.Option(False, "--json"), +) -> None: + """Build brief→post SFT JSONL for the writer LoRA (holdouts excluded).""" + from personality_protect.writer_sft import run_build_writer_sft + + _banner_from_ctx(ctx, json_mode=as_json) + paths = get_paths(profile, home=home) + try: + receipt = run_build_writer_sft(paths) + except (FileNotFoundError, ValueError) as exc: + console.print(f"[red]{exc}[/red]") + raise typer.Exit(1) from exc + if as_json: + typer.echo(json.dumps(receipt, indent=2, ensure_ascii=False)) + return + console.print( + f"writer SFT: {receipt['examples']} examples " + f"(skipped {receipt['skipped']}) → {receipt['path']}" + ) + + @app.command("write") def write_cmd( ctx: typer.Context, topic: str = typer.Option(..., "--topic", help="What the post is about."), points: str = typer.Option(..., "--points", help="Facts/claims the post may use."), + channel: str = typer.Option( + "post", + "--channel", + help="post (default LinkedIn post) or article (outline→sections→stitch).", + ), k: int = typer.Option( DEFAULT_WRITE_K, "--k", @@ -517,38 +548,27 @@ def write_cmd( max_tokens: int = typer.Option( DEFAULT_WRITE_MAX_TOKENS, "--max-tokens", - help="Generation budget for the draft.", + help="Generation budget for the draft (per section when --channel article).", ), no_adapter: bool = typer.Option( True, "--no-adapter/--adapter", - help="RAG write runs on base weights only; --adapter is unsupported.", + help="Default: base weights. --adapter loads a local writer LoRA when present.", ), out: Optional[Path] = typer.Option(None, "--out", help="Write draft to file."), profile: str = typer.Option(DEFAULT_PROFILE, "--profile"), home: Optional[Path] = typer.Option(None, "--home"), as_json: bool = typer.Option(False, "--json"), ) -> None: - """Draft a post from retrieved exemplars on base weights (no adapter).""" + """Draft a post or article from retrieved exemplars (optional writer LoRA).""" _banner_from_ctx(ctx, json_mode=as_json) - if not no_adapter: - console.print( - "[red]--adapter is not supported: the RAG write path always runs " - "base weights with adapter=none.[/red]" - ) - raise typer.Exit(2) paths = get_paths(profile, home=home) try: - config = load_config(paths) + load_config(paths) except FileNotFoundError as exc: console.print(f"[red]{exc}[/red]") raise typer.Exit(1) from exc - if config.write_adapter is not None and not as_json: - console.print( - f"[yellow]Ignoring profile write_adapter={config.write_adapter} — " - "write is RAG-only.[/yellow]" - ) try: result = run_write( @@ -557,6 +577,8 @@ def write_cmd( paths, k=k, max_tokens=max_tokens, + channel=channel, + use_adapter=not no_adapter, ) except ValueError as exc: console.print(f"[red]{exc}[/red]") @@ -818,6 +840,11 @@ def train_cmd( "Voice-pair mode: translator SFT only; skips leave_alone/identity minting." ), ), + writer: bool = typer.Option( + False, + "--writer", + help="Build brief→post writer SFT and train a writer LoRA (RAG write path).", + ), profile: str = typer.Option(DEFAULT_PROFILE, "--profile"), home: Optional[Path] = typer.Option(None, "--home"), as_json: bool = typer.Option(False, "--json"), @@ -829,6 +856,9 @@ def train_cmd( console.print(f"[red]Unknown backend: {backend}[/red]") raise typer.Exit(2) + if pairs is not None and writer: + console.print("[red]Pass only one of --writer or --pairs.[/red]") + raise typer.Exit(2) if pairs is not None and not pairs.is_file(): console.print(f"[red]pairs file not found: {pairs}[/red]") raise typer.Exit(2) @@ -1055,8 +1085,9 @@ def on_progress(info: dict) -> None: force_retrain=force_retrain, progress_callback=callback, pairs=pairs, + writer=writer, ) - except (FileNotFoundError, RuntimeError, MockFallbackError) as exc: + except (FileNotFoundError, RuntimeError, MockFallbackError, ValueError) as exc: console.print(f"[red]{exc}[/red]") raise typer.Exit(1) from exc finally: diff --git a/src/personality_protect/style_profile.py b/src/personality_protect/style_profile.py index bb3b67a..bc465d1 100644 --- a/src/personality_protect/style_profile.py +++ b/src/personality_protect/style_profile.py @@ -194,6 +194,47 @@ def corpus_style_stats(texts: Iterable[str]) -> dict[str, Any]: } +# Short posts and comments pull medians down; length targets come from posts +# that are already post-shaped. LinkedIn's ~3000 character limit is ~480–550 +# words — that is the hard ceiling for the post channel. +_POST_LENGTH_SOURCES = frozenset({"linkedin_post"}) +_MIN_LENGTH_SAMPLE_WORDS = 80 +LINKEDIN_POST_WORD_CEILING = 550 +DEFAULT_DRAFT_WORD_TARGET = 500 +DEFAULT_DRAFT_WORD_FLOOR = 300 + + +def post_length_stats(pieces: Iterable[Piece]) -> dict[str, float]: + """Word-length percentiles from post-shaped pieces only. + + Falls back to all non-empty pieces when no linkedin_post rows qualify, so + Contoso fixtures and note-only corpora still get a length card. + """ + piece_list = list(pieces) + posts = [ + p + for p in piece_list + if p.source in _POST_LENGTH_SOURCES + and len(_word_tokens(p.text or "")) >= _MIN_LENGTH_SAMPLE_WORDS + ] + if not posts: + posts = [p for p in piece_list if (p.text or "").strip()] + lengths = [float(len(_word_tokens(p.text or ""))) for p in posts] + if not lengths: + return { + "median_post_words": 0.0, + "post_words_p75": 0.0, + "post_words_p90": 0.0, + "post_length_samples": 0.0, + } + return { + "median_post_words": round(_median(lengths), 1), + "post_words_p75": round(_percentile(lengths, 0.75), 1), + "post_words_p90": round(_percentile(lengths, 0.90), 1), + "post_length_samples": float(len(lengths)), + } + + def build_style_profile( pieces: Iterable[Piece], *, @@ -204,8 +245,7 @@ def build_style_profile( stats = corpus_style_stats(p.text for p in piece_list) banned = list(banned_phrases) if banned_phrases is not None else list(BANNED_AI_FILLER) texts = [p.text for p in piece_list if (p.text or "").strip()] - lengths = [len(_word_tokens(text)) for text in texts] - stats["median_post_words"] = round(_median([float(n) for n in lengths]), 1) + stats.update(post_length_stats(piece_list)) stats.update(sentence_length_spread(texts)) stats["multi_sentence_paragraph_ratio"] = multi_sentence_paragraph_ratio(texts) return { @@ -217,19 +257,31 @@ def build_style_profile( } -# Slight headroom over the author's median so a legitimately long post is kept -# whole, while a runaway generation gets cut. The prompt states this same -# ceiling, so the instruction and the edit cannot drift apart. -_LENGTH_HEADROOM = 1.15 -DEFAULT_DRAFT_WORD_TARGET = 300 +def draft_word_target(profile: dict[str, Any]) -> int: + """Word ceiling for a finished post draft. + + Prefer the author's long-post band (p90, else p75, else median) so short + comments in the selection cannot shrink drafts to stub length. Clamp to the + LinkedIn character budget in words. + """ + stats = profile.get("stats") or {} + for key in ("post_words_p90", "post_words_p75", "median_post_words"): + value = float(stats.get(key) or 0) + if value > 0: + target = int(round(max(value, DEFAULT_DRAFT_WORD_FLOOR))) + return min(LINKEDIN_POST_WORD_CEILING, target) + return DEFAULT_DRAFT_WORD_TARGET -def draft_word_target(profile: dict[str, Any]) -> int: - """Word ceiling for a finished draft, measured from the author's posts.""" - median = float((profile.get("stats") or {}).get("median_post_words") or 0) - if median <= 0: - return DEFAULT_DRAFT_WORD_TARGET - return int(round(median * _LENGTH_HEADROOM)) +def draft_word_aim(profile: dict[str, Any]) -> int: + """Typical finished length stated in the prompt (below the hard ceiling).""" + stats = profile.get("stats") or {} + for key in ("post_words_p75", "post_words_p90", "median_post_words"): + value = float(stats.get(key) or 0) + if value > 0: + aim = int(round(max(value, DEFAULT_DRAFT_WORD_FLOOR))) + return min(draft_word_target(profile), aim) + return min(DEFAULT_DRAFT_WORD_TARGET, draft_word_target(profile)) def style_directives(profile: dict[str, Any]) -> list[str]: diff --git a/src/personality_protect/train.py b/src/personality_protect/train.py index 4cc9296..83876ad 100644 --- a/src/personality_protect/train.py +++ b/src/personality_protect/train.py @@ -238,19 +238,37 @@ def run_train( force_retrain: bool = False, progress_callback: ProgressCallback | None = None, pairs: Path | None = None, + writer: bool = False, ) -> TrainResult: config = load_config(paths) + if writer and pairs is not None: + raise ValueError("Pass only one of --writer or --pairs") voice_pair_mode = pairs is not None if voice_pair_mode: # Gated flatten→author pairs are the data floor; skip selected-piece gate. corpus_note = None + elif writer: + corpus_note = None else: n_selected = len(selected_pieces(paths)) corpus_note = check_corpus_size( n_selected, force=force or sft_only, smoke=smoke or mock ) - if voice_pair_mode: + if writer: + from personality_protect.writer_sft import run_build_writer_sft, writer_sft_path + + receipt = run_build_writer_sft(paths) + sft_path = writer_sft_path(paths) + # Train pipeline expects train.jsonl under sft/; copy writer rows there. + paths.sft_jsonl.write_text(sft_path.read_text(encoding="utf-8"), encoding="utf-8") + sft_path = paths.sft_jsonl + n = int(receipt["examples"]) + if n < 1: + raise FileNotFoundError( + "Writer SFT produced 0 examples. Need longer linkedin_post pieces." + ) + elif voice_pair_mode: assert pairs is not None # for type checkers sft_path, n = build_sft_from_pairs( pairs, diff --git a/src/personality_protect/voice_index.py b/src/personality_protect/voice_index.py index 2dd2a03..5cebdbb 100644 --- a/src/personality_protect/voice_index.py +++ b/src/personality_protect/voice_index.py @@ -137,17 +137,26 @@ def retrieve( *, profile: str = DEFAULT_PROFILE, home: Path | None = None, + sources: Iterable[str] | None = None, ) -> list[dict[str, Any]]: - """Return up to k indexed exemplars ranked by similarity to the brief.""" + """Return up to k indexed exemplars ranked by similarity to the brief. + + ``sources`` limits matches to those piece sources (e.g. article-only + retrieval). When the filter yields fewer than ``k`` rows, the shortlist is + returned as-is rather than falling back to other sources. + """ if k <= 0: return [] paths = get_paths(profile, home=home) embedder, rows = _load_voice_index(paths) query = embedder.embed(brief) + allowed = {str(source) for source in sources} if sources is not None else None scored: list[tuple[float, dict[str, Any]]] = [] for row in rows: piece = dict(row["piece"]) + if allowed is not None and str(piece.get("source") or "") not in allowed: + continue score = cosine_similarity(query, row["vector"]) scored.append((score, piece)) scored.sort(key=lambda item: (-item[0], str(item[1]["id"]))) diff --git a/src/personality_protect/write.py b/src/personality_protect/write.py index 539ed64..73e1c38 100644 --- a/src/personality_protect/write.py +++ b/src/personality_protect/write.py @@ -37,7 +37,9 @@ DEFAULT_WRITE_K = 2 MIN_WRITE_K = 0 MAX_WRITE_K = 5 -DEFAULT_WRITE_MAX_TOKENS = 768 +# Long LinkedIn posts (~500 words) need headroom past the trim target so the +# model can finish the last paragraph before draft_trim cuts. +DEFAULT_WRITE_MAX_TOKENS = 1536 MAX_WRITE_ATTEMPTS = 2 MAX_EXEMPLAR_WORDS = 60 @@ -47,17 +49,28 @@ PromptSink = MutableSequence[str] +def resolve_writer_adapter(paths: ProfilePaths) -> str | None: + """Return an adapter directory when a writer LoRA is present on disk.""" + adapters = paths.adapters_dir + weights = adapters / "adapters.safetensors" + if weights.is_file(): + return str(adapters) + return None + + def mlx_generate_no_adapter( messages: Sequence[Message], *, base_model: str, max_tokens: int = DEFAULT_WRITE_MAX_TOKENS, + adapter_path: str | None = None, prompt_sink: PromptSink | None = None, ) -> str: - """Generate from MLX base weights with ``adapter_path=None``. + """Generate from MLX weights, optionally with a writer LoRA. - The write path is RAG-only: a LoRA adapter is never loaded, even when one - exists on disk. Tests inject ``generate_fn`` instead of calling this. + ``adapter_path=None`` keeps the Camp A RAG default (base only). When a + gated writer adapter directory is supplied, MLX loads it for the call. + Tests inject ``generate_fn`` instead of calling this. ``messages`` are rendered through the tokenizer's chat template. Skipping that step makes the instruct model continue the prompt as a document and @@ -75,7 +88,7 @@ def mlx_generate_no_adapter( try: from mlx_lm import generate, load - model, tokenizer = load(base_model, adapter_path=None) + model, tokenizer = load(base_model, adapter_path=adapter_path) prompt = render_chat_prompt( tokenizer, messages, @@ -159,10 +172,32 @@ def run_write( *, k: int = DEFAULT_WRITE_K, max_tokens: int = DEFAULT_WRITE_MAX_TOKENS, + channel: str = "post", + use_adapter: bool = False, generate_fn: GenerateFn | None = None, prompt_sink: PromptSink | None = None, ) -> dict[str, Any]: - """Retrieve exemplars, generate a draft, regenerate once on guard failure.""" + """Retrieve exemplars, generate a draft, regenerate once on guard failure. + + ``channel='article'`` delegates to the article outline→section→stitch path. + ``use_adapter`` loads a local writer LoRA when ``adapters.safetensors`` exists. + """ + resolved = (channel or "post").strip().lower() + if resolved == "article": + from personality_protect.write_article import run_write_article + + return run_write_article( + topic, + points, + paths, + k=k, + max_tokens=max_tokens, + generate_fn=generate_fn, + prompt_sink=prompt_sink, + ) + if resolved != "post": + raise ValueError("channel must be one of: post, article") + topic = topic.strip() points = points.strip() if not topic: @@ -193,6 +228,13 @@ def run_write( style_directives=directives, ) + adapter_path = resolve_writer_adapter(paths) if use_adapter else None + if use_adapter and adapter_path is None: + raise FileNotFoundError( + f"No writer adapter at {paths.adapters_dir}/adapters.safetensors. " + "Train one with: personality-protect train --writer" + ) + generator = generate_fn or mlx_generate_no_adapter model_id = config.base_model or DEFAULT_MLX_MODEL @@ -205,6 +247,7 @@ def run_write( messages, base_model=model_id, max_tokens=max_tokens, + adapter_path=adapter_path, prompt_sink=prompt_sink, ) ).strip() @@ -217,9 +260,10 @@ def run_write( return { "text": draft, + "channel": "post", "voice_mode": config.voice_mode, - "adapter": "none", - "write_adapter": None, + "adapter": "writer" if adapter_path else "none", + "write_adapter": adapter_path, "model": model_id, "k": len(matches), "exemplar_ids": [str(match["id"]) for match in matches], diff --git a/src/personality_protect/write_article.py b/src/personality_protect/write_article.py new file mode 100644 index 0000000..dbfa3bc --- /dev/null +++ b/src/personality_protect/write_article.py @@ -0,0 +1,232 @@ +"""Article channel: outline → section drafts → stitch. + +Posts stay on :mod:`personality_protect.write`. Longform needs section budgets +and article-only retrieval — raising ``max_tokens`` on the post path is not +enough and produces one long stub instead of a structured piece. +""" + +from __future__ import annotations + +import re +from collections.abc import Sequence +from typing import Any + +from personality_protect.chat_prompt import flatten_chat_messages +from personality_protect.config import DEFAULT_MLX_MODEL, ProfilePaths, load_config +from personality_protect.draft_trim import trim_draft, word_count +from personality_protect.models import load_index +from personality_protect.prompt_write import build_write_messages +from personality_protect.style_profile import ( + draft_word_target, + load_style_profile, + style_directives, +) +from personality_protect.voice_index import retrieve +from personality_protect.write import ( + DEFAULT_WRITE_K, + GenerateFn, + PromptSink, + build_brief, + clip_exemplar, + mlx_generate_no_adapter, + normalize_sentence_case, +) +from personality_protect.writer_guards import ( + check_invention, + mask_exemplar_entities, + parrot_reject, +) + +ARTICLE_SOURCES: tuple[str, ...] = ("linkedin_article",) +# Below this the article channel has too little rhythm signal to claim voice. +MIN_ARTICLE_CORPUS = 5 +DEFAULT_ARTICLE_SECTION_MAX_TOKENS = 768 +DEFAULT_ARTICLE_SECTION_WORDS = 280 +MAX_ARTICLE_SECTIONS = 8 +MIN_ARTICLE_SECTIONS = 2 + +_BULLET_RE = re.compile(r"^\s*[-*•]\s+") + + +def count_article_pieces(paths: ProfilePaths) -> int: + """How many article-source pieces are in the local corpus index.""" + return sum(1 for piece in load_index(paths.index_path) if piece.source in ARTICLE_SOURCES) + + +def assert_article_corpus(paths: ProfilePaths, *, minimum: int = MIN_ARTICLE_CORPUS) -> int: + """Raise when the article channel has too little source material.""" + n = count_article_pieces(paths) + if n < minimum: + raise FileNotFoundError( + f"Article channel needs at least {minimum} linkedin_article pieces " + f"in the corpus (found {n}). Ingest more articles, then rebuild " + "index-voice." + ) + return n + + +def outline_from_brief(topic: str, points: str) -> list[str]: + """Deterministic section titles from the brief (no model call). + + Each bullet becomes a section. Thin briefs get the topic as a lead section + plus the bullets so the stitch still has structure. + """ + topic = topic.strip() + lines = [line.strip() for line in (points or "").splitlines() if line.strip()] + bullets: list[str] = [] + for line in lines: + cleaned = _BULLET_RE.sub("", line).strip() + if cleaned: + bullets.append(cleaned) + if not bullets and points.strip(): + bullets = [points.strip()] + + sections: list[str] = [] + if topic and (not bullets or len(bullets) < MIN_ARTICLE_SECTIONS): + sections.append(topic) + for bullet in bullets: + if bullet not in sections: + sections.append(bullet) + if len(sections) >= MAX_ARTICLE_SECTIONS: + break + if len(sections) < MIN_ARTICLE_SECTIONS: + raise ValueError( + "article brief needs at least two section points " + "(topic + bullets, or two bullets)" + ) + return sections + + +def _section_brief(topic: str, section: str, points: str) -> tuple[str, str]: + """Topic/points pair for one section generation call.""" + return ( + f"{topic} — {section}" if topic and topic != section else section, + f"- {section}\n- Stay on this section only.\n- Brief points:\n{points}", + ) + + +def _guard_flags(brief: str, draft: str, exemplars: Sequence[str]) -> dict[str, Any]: + invention = check_invention(brief, normalize_sentence_case(draft)) + return { + "parrot_reject": parrot_reject(draft, list(exemplars)), + "invent_reject": not invention.passed, + "invented_entities": sorted(invention.invented_entities), + "invented_numbers": sorted(invention.invented_numbers), + } + + +def run_write_article( + topic: str, + points: str, + paths: ProfilePaths, + *, + k: int = DEFAULT_WRITE_K, + max_tokens: int = DEFAULT_ARTICLE_SECTION_MAX_TOKENS, + generate_fn: GenerateFn | None = None, + prompt_sink: PromptSink | None = None, + min_articles: int = MIN_ARTICLE_CORPUS, +) -> dict[str, Any]: + """Outline → per-section RAG draft → stitch into one article.""" + topic = topic.strip() + points = points.strip() + if not topic: + raise ValueError("topic must not be empty") + if not points: + raise ValueError("points must not be empty") + + article_count = assert_article_corpus(paths, minimum=min_articles) + sections = outline_from_brief(topic, points) + config = load_config(paths) + style = load_style_profile(paths) + directives = style_directives(style) + # Articles are longer than posts; do not clamp sections to the post ceiling. + section_words = max( + DEFAULT_ARTICLE_SECTION_WORDS, + int(draft_word_target(style)), + ) + generator = generate_fn or mlx_generate_no_adapter + model_id = config.base_model or DEFAULT_MLX_MODEL + + full_brief = build_brief(topic, points) + matches = retrieve( + full_brief, + k=k, + profile=paths.name, + home=paths.home, + sources=ARTICLE_SOURCES, + ) + if k and not matches: + raise FileNotFoundError( + f"No article exemplars indexed for profile {paths.name}. " + "Ingest linkedin_article pieces and run: personality-protect index-voice" + ) + exemplars = [str(match["text"]) for match in matches] + masked = [ + mask_exemplar_entities(clip_exemplar(exemplar), full_brief) for exemplar in exemplars + ] + + section_drafts: list[str] = [] + all_messages: list[list[dict[str, str]]] = [] + attempts_total = 0 + last_guards: dict[str, Any] = { + "parrot_reject": False, + "invent_reject": False, + "invented_entities": [], + "invented_numbers": [], + } + + for section in sections: + section_topic, section_points = _section_brief(topic, section, points) + section_brief = build_brief(section_topic, section_points) + messages = build_write_messages( + topic=section_topic, + points=section_points, + examples=masked, + style_directives=[ + *directives, + f"Write only the section about: {section}", + f"Aim for about {section_words} words in this section.", + ], + ) + all_messages.append(messages) + draft = "" + for attempt in range(1, 3): + attempts_total += 1 + raw = str( + generator( + messages, + base_model=model_id, + max_tokens=max_tokens, + prompt_sink=prompt_sink, + ) + ).strip() + draft = trim_draft(raw, max_words=section_words * 2) + last_guards = _guard_flags(section_brief, draft, exemplars) + if not last_guards["parrot_reject"] and not last_guards["invent_reject"]: + break + section_drafts.append(draft) + + text = "\n\n".join(part for part in section_drafts if part.strip()).strip() + # Final invent check against the full brief the author supplied. + final_guards = _guard_flags(full_brief, text, exemplars) + + return { + "text": text, + "channel": "article", + "voice_mode": config.voice_mode, + "adapter": "none", + "write_adapter": None, + "model": model_id, + "k": len(matches), + "exemplar_ids": [str(match["id"]) for match in matches], + "attempts": attempts_total, + "word_target": section_words * len(sections), + "article_count": article_count, + "sections": sections, + "section_count": len(sections), + "draft_words": word_count(text), + **final_guards, + "exemplar_texts": exemplars, + "messages": all_messages[0] if all_messages else [], + "prompt": flatten_chat_messages(all_messages[0]) if all_messages else "", + } diff --git a/src/personality_protect/writer_sft.py b/src/personality_protect/writer_sft.py new file mode 100644 index 0000000..40d5493 --- /dev/null +++ b/src/personality_protect/writer_sft.py @@ -0,0 +1,139 @@ +"""Brief→post SFT rows for the writer LoRA (not the translator path).""" + +from __future__ import annotations + +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Iterable + +from personality_protect.config import ProfilePaths +from personality_protect.eval_write_holdout import mine_brief_from_holdout +from personality_protect.models import Piece, load_index +from personality_protect.prompt_write import WRITE_SYSTEM_PROMPT, build_write_user_content +from personality_protect.style_profile import load_style_profile, style_directives + +WRITER_SFT_FILENAME = "writer_train.jsonl" +_POST_SOURCES = frozenset({"linkedin_post"}) +_MIN_TARGET_WORDS = 50 + + +def writer_sft_path(paths: ProfilePaths) -> Path: + return paths.sft_dir / WRITER_SFT_FILENAME + + +def load_holdout_id_set(paths: ProfilePaths) -> set[str]: + """Ids listed in the local dogfood holdout file (never indexed for eval).""" + path = paths.root / "dogfood_holdout_ids.json" + if not path.is_file(): + return set() + data = json.loads(path.read_text(encoding="utf-8")) + if isinstance(data, dict): + ids = data.get("holdout_ids") or data.get("ids") or [] + else: + ids = data + return {str(piece_id) for piece_id in ids} + + +def piece_to_writer_example( + piece: Piece, + *, + style_directives_list: list[str] | None = None, +) -> dict[str, Any] | None: + """One chat example: lossy brief → author's post as assistant target.""" + body = (piece.text or "").strip() + if len(body.split()) < _MIN_TARGET_WORDS: + return None + if piece.source not in _POST_SOURCES: + return None + try: + brief = mine_brief_from_holdout(body, holdout_id=piece.id) + except ValueError: + return None + user = build_write_user_content( + topic=brief["topic"], + points=brief["points"], + examples=(), + style_directives=style_directives_list or (), + ) + return { + "messages": [ + {"role": "system", "content": WRITE_SYSTEM_PROMPT}, + {"role": "user", "content": user}, + {"role": "assistant", "content": body}, + ], + "meta": { + "piece_id": piece.id, + "source": piece.source, + "year": piece.year, + "word_count": len(body.split()), + "pair_kind": "writer", + }, + } + + +def build_writer_sft( + pieces: Iterable[Piece], + out_path: Path, + *, + holdout_ids: Iterable[str] = (), + style_directives_list: list[str] | None = None, +) -> dict[str, Any]: + """Write writer SFT JSONL; skip holdouts and unbriefable posts.""" + excluded = {str(piece_id) for piece_id in holdout_ids} + rows: list[dict[str, Any]] = [] + skipped = 0 + for piece in pieces: + if piece.id in excluded: + skipped += 1 + continue + example = piece_to_writer_example( + piece, style_directives_list=style_directives_list + ) + if example is None: + skipped += 1 + continue + rows.append(example) + + out_path.parent.mkdir(parents=True, exist_ok=True) + with out_path.open("w", encoding="utf-8") as handle: + for row in rows: + handle.write(json.dumps(row, ensure_ascii=False) + "\n") + + return { + "path": str(out_path), + "examples": len(rows), + "skipped": skipped, + "holdouts_excluded": sorted(excluded), + "built_at": datetime.now(timezone.utc).isoformat(), + } + + +def run_build_writer_sft(paths: ProfilePaths) -> dict[str, Any]: + """Build writer SFT from all linkedin_post pieces in the corpus index. + + Uses the full index (not the year-gated selection) so recent posts past + ``through_year`` still train the writer. Holdout ids are excluded. + """ + paths.ensure() + holdouts = load_holdout_id_set(paths) + try: + directives = style_directives(load_style_profile(paths)) + except FileNotFoundError: + directives = [] + pieces = [ + piece + for piece in load_index(paths.index_path) + if piece.source in _POST_SOURCES + ] + if not pieces: + raise FileNotFoundError( + f"No linkedin_post pieces available for writer SFT under {paths.root}" + ) + receipt = build_writer_sft( + pieces, + writer_sft_path(paths), + holdout_ids=holdouts, + style_directives_list=directives, + ) + return receipt diff --git a/tests/test_style_profile.py b/tests/test_style_profile.py index d217bf6..111b4c6 100644 --- a/tests/test_style_profile.py +++ b/tests/test_style_profile.py @@ -13,8 +13,10 @@ from personality_protect.select import Selection from personality_protect.style_profile import ( BANNED_AI_FILLER, + LINKEDIN_POST_WORD_CEILING, build_style_profile, corpus_style_stats, + draft_word_target, load_style_profile, run_build_style_profile, style_directives, @@ -93,6 +95,35 @@ def test_build_style_profile_records_median_post_words(): ] stats = build_style_profile(pieces)["stats"] assert stats["median_post_words"] > 0 + assert stats["post_words_p75"] > 0 + assert stats["post_words_p90"] > 0 + + +def test_post_length_ignores_short_comments_for_targets(): + """Length targets come from post-shaped pieces, not comment stubs.""" + short = "Ok." * 5 + long_post = ( + "Contoso Ledger. " * 40 + + "You ship the reconciliation or you own the outage. " + * 20 + ) + pieces = [ + Piece(id="comment", source="linkedin_comment", text=short, year=2024), + Piece(id="post", source="linkedin_post", text=long_post, year=2024), + ] + stats = build_style_profile(pieces)["stats"] + assert stats["post_words_p90"] >= 80 + ceiling = draft_word_target(build_style_profile(pieces)) + assert ceiling >= 300 + assert ceiling <= LINKEDIN_POST_WORD_CEILING + + +def test_draft_word_target_clamps_to_linkedin_ceiling(): + huge = "word " * 2000 + profile = build_style_profile( + [Piece(id="p", source="linkedin_post", text=huge, year=2024)] + ) + assert draft_word_target(profile) == LINKEDIN_POST_WORD_CEILING def test_style_directives_carry_cadence_without_copyable_prose(): diff --git a/tests/test_write.py b/tests/test_write.py index f6e8018..f6d628a 100644 --- a/tests/test_write.py +++ b/tests/test_write.py @@ -293,7 +293,7 @@ def fake_generate(messages, **_kwargs: object) -> str: assert result["invented_numbers"] == ["18%"] -def test_cli_write_rejects_adapter_flag(tmp_path: Path): +def test_cli_write_adapter_requires_weights(tmp_path: Path): _seed_contoso_index(tmp_path) result = runner.invoke( @@ -314,8 +314,8 @@ def test_cli_write_rejects_adapter_flag(tmp_path: Path): ], ) - assert result.exit_code == 2 - assert "adapter=none" in result.output + assert result.exit_code == 1 + assert "No writer adapter" in result.output def test_cli_write_exits_nonzero_when_guards_still_fail(tmp_path: Path): diff --git a/tests/test_write_article.py b/tests/test_write_article.py new file mode 100644 index 0000000..a8a8dea --- /dev/null +++ b/tests/test_write_article.py @@ -0,0 +1,115 @@ +"""Contoso-safe article channel: outline → sections → stitch.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from personality_protect.config import init_profile +from personality_protect.models import Piece, save_index +from personality_protect.style_profile import build_style_profile, save_style_profile +from personality_protect.voice_index import build_voice_index +from personality_protect.write import run_write +from personality_protect.write_article import ( + MIN_ARTICLE_CORPUS, + assert_article_corpus, + outline_from_brief, + run_write_article, +) + + +def _article(n: int, words: int = 200) -> Piece: + body = ("Contoso Ledger section. You own the outage. " * (words // 8)).strip() + return Piece( + id=f"contoso-article-{n}", + source="linkedin_article", + text=body, + year=2024, + ) + + +def _seed_articles(tmp_path: Path, n: int = MIN_ARTICLE_CORPUS) -> Path: + paths, _, _ = init_profile("contoso", home=tmp_path) + pieces = [_article(i) for i in range(n)] + pieces.append( + Piece( + id="contoso-post", + source="linkedin_post", + text="Contoso keeps the queue boring. You name one owner.", + year=2024, + ) + ) + save_index(paths.index_path, pieces) + build_voice_index(paths) + save_style_profile(paths, build_style_profile(pieces)) + return tmp_path + + +def test_outline_from_brief_uses_bullets(): + sections = outline_from_brief( + "Contoso pricing", + "- Name one owner\n- Cut exceptions\n- Keep Ledger boring", + ) + assert sections == ["Name one owner", "Cut exceptions", "Keep Ledger boring"] + + +def test_outline_from_brief_requires_two_sections(): + with pytest.raises(ValueError, match="two section"): + outline_from_brief("", "- Only one") + + +def test_assert_article_corpus_floor(tmp_path: Path): + _seed_articles(tmp_path, n=2) + paths, _, _ = init_profile("contoso", home=tmp_path) + with pytest.raises(FileNotFoundError, match="at least"): + assert_article_corpus(paths, minimum=MIN_ARTICLE_CORPUS) + + +def test_run_write_article_stitches_sections(tmp_path: Path): + _seed_articles(tmp_path, n=MIN_ARTICLE_CORPUS) + paths, _, _ = init_profile("contoso", home=tmp_path) + calls: list = [] + + def fake_generate(messages, **_kwargs: object) -> str: + calls.append(messages) + user = messages[1]["content"] + # Each section prompt names its focus. + assert "Write only the section about:" in user + return ( + "Contoso Ledger holds the line.\n\n" + "You name one owner before the packaging change." + ) + + result = run_write_article( + "Contoso packaging", + "- Name one owner\n- Cut exceptions\n- Keep Ledger boring", + paths, + k=2, + generate_fn=fake_generate, + ) + assert result["channel"] == "article" + assert result["section_count"] == 3 + assert result["adapter"] == "none" + assert len(calls) == 3 + assert result["text"].count("Contoso Ledger holds the line.") == 3 + assert result["article_count"] >= MIN_ARTICLE_CORPUS + + +def test_run_write_channel_article_delegates(tmp_path: Path): + _seed_articles(tmp_path, n=MIN_ARTICLE_CORPUS) + paths, _, _ = init_profile("contoso", home=tmp_path) + + def fake_generate(messages, **_kwargs: object) -> str: + return "Contoso section body with enough words to survive trim." + + result = run_write( + "Contoso packaging", + "- Name one owner\n- Cut exceptions", + paths, + channel="article", + k=1, + generate_fn=fake_generate, + ) + assert result["channel"] == "article" + assert result["section_count"] == 2 diff --git a/tests/test_writer_sft.py b/tests/test_writer_sft.py new file mode 100644 index 0000000..42604f4 --- /dev/null +++ b/tests/test_writer_sft.py @@ -0,0 +1,84 @@ +"""Contoso-safe writer SFT: brief→post rows, holdouts excluded.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from personality_protect.config import init_profile +from personality_protect.models import Piece, save_index +from personality_protect.select import Selection +from personality_protect.style_profile import build_style_profile, save_style_profile +from personality_protect.writer_sft import ( + build_writer_sft, + piece_to_writer_example, + run_build_writer_sft, + writer_sft_path, +) + +CONTOSO_LONG = ( + "Contoso Ledger keeps the queue boring on purpose.\n\n" + "You ship the reconciliation or you own the outage.\n\n" + "You name one owner before the packaging change starts.\n\n" + "You cut exceptions or you explain them in writing.\n\n" + "Partners already know which one you picked this quarter.\n\n" + "Stop pretending the roadmap is the work.\n\n" + "You own the queue you refuse to look at.\n\n" + "Boring beats clever every single time Contoso ships Ledger.\n\n" + "You keep Contoso boring and the partners stay calm." +) + + +def test_piece_to_writer_example_builds_chat_row(): + piece = Piece(id="c1", source="linkedin_post", text=CONTOSO_LONG, year=2024) + row = piece_to_writer_example(piece) + assert row is not None + assert row["meta"]["pair_kind"] == "writer" + assert row["messages"][-1]["role"] == "assistant" + assert "Contoso Ledger" in row["messages"][-1]["content"] + assert "BRIEF:" in row["messages"][1]["content"] + + +def test_build_writer_sft_excludes_holdouts(tmp_path: Path): + pieces = [ + Piece(id="keep", source="linkedin_post", text=CONTOSO_LONG, year=2024), + Piece(id="hold", source="linkedin_post", text=CONTOSO_LONG + " Extra.", year=2024), + ] + out = tmp_path / "writer.jsonl" + receipt = build_writer_sft(pieces, out, holdout_ids={"hold"}) + assert receipt["examples"] == 1 + lines = out.read_text(encoding="utf-8").strip().splitlines() + assert len(lines) == 1 + assert json.loads(lines[0])["meta"]["piece_id"] == "keep" + + +def test_run_build_writer_sft_writes_profile_file(tmp_path: Path): + paths, _, _ = init_profile("contoso", home=tmp_path) + pieces = [ + Piece(id="c1", source="linkedin_post", text=CONTOSO_LONG, year=2024), + Piece(id="c2", source="linkedin_post", text=CONTOSO_LONG + " Again.", year=2024), + ] + save_index(paths.index_path, pieces) + save_style_profile(paths, build_style_profile(pieces)) + paths.selection_path.write_text( + json.dumps( + Selection( + piece_ids=["c1", "c2"], + min_words=10, + through_year=2024, + include_undated=True, + summary={"pieces": 2}, + ).to_dict(), + indent=2, + ) + + "\n", + encoding="utf-8", + ) + paths.root.joinpath("dogfood_holdout_ids.json").write_text( + json.dumps({"holdout_ids": ["c2"]}) + "\n", + encoding="utf-8", + ) + + receipt = run_build_writer_sft(paths) + assert receipt["examples"] == 1 + assert writer_sft_path(paths).is_file() From a6772e873be25f07d34db79b8614539115c4f123 Mon Sep 17 00:00:00 2001 From: Dusan Milicevic Date: Thu, 30 Jul 2026 10:55:08 -0500 Subject: [PATCH 2/3] Resolve writer LoRA from adapters/latest MLX train persists weights under adapters/latest; write --adapter must load from there. Co-authored-by: Cursor --- src/personality_protect/write.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/personality_protect/write.py b/src/personality_protect/write.py index 73e1c38..7fc7edf 100644 --- a/src/personality_protect/write.py +++ b/src/personality_protect/write.py @@ -50,11 +50,14 @@ def resolve_writer_adapter(paths: ProfilePaths) -> str | None: - """Return an adapter directory when a writer LoRA is present on disk.""" - adapters = paths.adapters_dir - weights = adapters / "adapters.safetensors" - if weights.is_file(): - return str(adapters) + """Return an adapter directory when a writer LoRA is present on disk. + + MLX train writes under ``adapters/latest/``; prefer that, then the profile + adapters root. + """ + for directory in (paths.adapters_dir / "latest", paths.adapters_dir): + if (directory / "adapters.safetensors").is_file(): + return str(directory) return None @@ -231,7 +234,7 @@ def run_write( adapter_path = resolve_writer_adapter(paths) if use_adapter else None if use_adapter and adapter_path is None: raise FileNotFoundError( - f"No writer adapter at {paths.adapters_dir}/adapters.safetensors. " + f"No writer adapter at {paths.adapters_dir}/latest/adapters.safetensors. " "Train one with: personality-protect train --writer" ) From 526d0966c0f210bd84afb261ac91a3fd1a18d2f2 Mon Sep 17 00:00:00 2001 From: Dusan Milicevic Date: Thu, 30 Jul 2026 12:37:33 -0500 Subject: [PATCH 3/3] Make README hero the write path, not train/filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quickstart, workflow, and hardware notes now describe ingest → index-voice → build-style-profile → write, which needs no training run: write loads base MLX weights with adapter=none. Regenerate docs/images/cli-shipped.png, which still showed `train --proof` + `filter` as the shipped path, as a Contoso-safe `write` shot (post + --channel article, adapter=none). Move build-writer-sft/train, --adapter, filter/compare/select, and scripts/beast_demo.sh under Advanced (optional), where the adapter is only kept if eval-write-holdout beats the default on holdouts. Co-authored-by: Cursor --- README.md | 88 ++++++++++++++++++------------------ docs/LAUNCH.md | 29 ++++++++---- docs/images/cli-shipped.png | Bin 30475 -> 46010 bytes 3 files changed, 64 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index e06db79..7f3fdf6 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,18 @@ **Stop sounding like everyone else's AI.** -Draft LinkedIn posts and articles in *your* voice on your machine. Index your writing, measure cadence, retrieve short rhythm references, and generate with a local quantized model (~5–7 GB). Optional writer LoRA deepens voice after it beats RAG-alone on holdouts. Your corpus never leaves this Mac. +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 this Mac. Built by [Telivity](https://telivity.com). Apache-2.0. ```text -your writing ──► ingest ──► index-voice ──► build-style-profile ──► write - └── optional: train --writer ──► write --adapter +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`). +

- personality-protect write path + personality-protect write drafting a Contoso post locally with adapter=none

--- @@ -32,19 +33,20 @@ PersonalityProtect keeps the corpus on disk, measures your cadence, retrieves sh 1. **Ingest** your LinkedIn export and/or local notes (stays on disk). 2. **`index-voice`** builds a local retrieval index. 3. **`build-style-profile`** measures cadence (sentence length, short lines, post length band, banned filler). -4. **`write --topic --points`** drafts from the brief only; retrieved posts are rhythm reference. +4. **`write --topic --points`** drafts from the brief only; retrieved pieces are rhythm reference. + +Two channels come out of step 4: -Optional: +- **`--channel post`** (default) targets your long-post band, up to the LinkedIn ~3000-character limit (~550 words). +- **`--channel article`** runs outline → sections → stitch, and needs at least five `linkedin_article` pieces in the corpus. -- **`train --writer`** trains a brief→post LoRA from your posts (holdouts excluded). -- **`write --adapter`** loads that LoRA when `adapters.safetensors` is present. -- **`write --channel article`** runs outline → sections → stitch (needs enough `linkedin_article` pieces). +Local LoRA training stays in the CLI as an experiment, not as the path to a first draft — see [Advanced](#advanced-optional). --- ## Quick start -Requires Python 3.10+ and an Apple Silicon Mac for the happy path. +Requires Python 3.10+ and an Apple Silicon Mac: `write` runs on MLX and needs Metal. ```bash git clone https://github.com/TelivityAI/personality-protect.git @@ -76,13 +78,7 @@ personality-protect write \ personality-protect status ``` -Optional writer LoRA: - -```bash -personality-protect build-writer-sft -personality-protect train --writer --backend mlx -personality-protect write --adapter --topic "…" --points "…" -``` +That is the whole path to a draft — no training step. Optional extras: @@ -98,9 +94,9 @@ pip install -e ".[cuda]" # NVIDIA path (optional) Public docs use **synthetic Contoso / synergy-slop text only**. No personal corpus. -| Write path | Status | Mark | +| `write` (post + article) | `status` | Mark | | --- | --- | --- | -| write path | status | Telivity CLI logo | +| write drafting a Contoso post with adapter=none | status output for the synthetic demo profile | Telivity CLI logo | ```bash personality-protect index-voice @@ -138,14 +134,14 @@ This README uses synthetic examples only (e.g. Contoso, “leverage synergies” ## Hardware -**Happy path: Apple Silicon Mac** (MLX). +**`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 (default write) | ~6 GB download | -| GGUF Q4_K_M (optional) | ~5.6 GB download | -| Writer LoRA | Small (MBs) under the profile | -| Peak RAM while writing / training | Memory-capped; 16 GB+ recommended | +| 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. @@ -178,20 +174,10 @@ Post length targets come from `linkedin_post` pieces (p75/p90), clamped to the L ```bash personality-protect write --topic "…" --points "…" personality-protect write --channel article --topic "…" --points "…" -personality-protect write --adapter --topic "…" --points "…" personality-protect write --topic "…" --points "…" --json ``` -`--topic` and `--points` are the only content the draft may use. Article channel requires at least five `linkedin_article` pieces in the corpus. - -### Writer LoRA - -```bash -personality-protect build-writer-sft -personality-protect train --writer --backend mlx -``` - -Keep the adapter only if it improves holdout drafts vs RAG-alone (`eval-write-holdout`). Delete it if it does not. +`--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`). Article channel requires at least five `linkedin_article` pieces in the corpus. ### Status / API @@ -213,14 +199,13 @@ Global flags (most commands): `--profile`, `--home`, `--json`, plus branding `-- | `ingest` | Index LinkedIn export and/or local paths | | `index-voice` | Build local voice retrieval index | | `build-style-profile` | Build cadence / length / banned-filler style card | -| `build-writer-sft` | Build brief→post SFT for writer LoRA | -| `write` | Draft a post or article (`--channel`, optional `--adapter`) | -| `train` | Local LoRA train (`--writer` for writer LoRA) | +| `write` | Draft a post or article (`--channel post\|article`) | | `eval-write-holdout` | Score write quality on held-out pieces (local receipt) | | `status` | Show profile state | | `demo` | Optional synthetic smoke tour (no download) | | `api` | Loopback HTTP stub | | `logo` | Print Telivity CLI mark | +| `build-writer-sft`, `train` | Optional LoRA experiments — see [Advanced](#advanced-optional) | ### `write` flags @@ -230,7 +215,7 @@ Global flags (most commands): `--profile`, `--home`, `--json`, plus branding `-- | `--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` | Load writer LoRA when present (default off) | +| `--adapter` / `--no-adapter` | Default `--no-adapter` (base weights); `--adapter` needs a trained LoRA | | `--json` | Machine-readable receipt | ### `eval-write-holdout` flags @@ -245,13 +230,28 @@ Global flags (most commands): `--profile`, `--home`, `--json`, plus branding `-- ## Advanced (optional) -Other experiment commands (`select`, `filter`, `compare`, translator pairs) remain in the CLI. The shipped path above does not require them. +Nothing here is needed for a draft. These commands stay in the CLI for local experiments and receipts. ---- +### Writer LoRA (experimental plumbing) -## Launch script +`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: -Operator checklist: [docs/LAUNCH.md](docs/LAUNCH.md). +```bash +personality-protect build-writer-sft +personality-protect train --writer --backend mlx +personality-protect eval-write-holdout --out receipt.json +personality-protect write --adapter --topic "…" --points "…" +``` + +Keep an adapter only if `eval-write-holdout` shows it beating RAG-alone on held-out pieces. Otherwise delete it and stay on the default. Training is not a prerequisite for `write`, and an untested adapter is not an upgrade. + +### Other experiment commands + +`select`, `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. + +### Operator script + +`scripts/beast_demo.sh` drives the older `select` → `train` → `compare` → `eval` sequence, not `write`. Use it for train/compare runs only: ```bash chmod +x scripts/beast_demo.sh @@ -259,6 +259,8 @@ chmod +x scripts/beast_demo.sh ./scripts/beast_demo.sh --skip-download # synthetic smoke ``` +Operator checklist: [docs/LAUNCH.md](docs/LAUNCH.md). + --- ## Develop diff --git a/docs/LAUNCH.md b/docs/LAUNCH.md index c0e7eca..f7a4b2d 100644 --- a/docs/LAUNCH.md +++ b/docs/LAUNCH.md @@ -6,9 +6,10 @@ Operator guide for a local PersonalityProtect run. Corpus, SFT JSONL, adapters, | Path | Machine | Disk download | | --- | --- | --- | -| MLX train + filter | Apple Silicon | MLX 4-bit **~6 GB** | -| GGUF filter | Any (llama.cpp) | Q4_K_M **~5.6 GB** | -| CUDA QLoRA train | NVIDIA 24GB+ VRAM | Prefer GGUF for day-to-day filter | +| `write` (post + article) | Apple Silicon | MLX 4-bit **~6 GB** | +| GGUF `filter` (optional) | Any (llama.cpp) | Q4_K_M **~5.6 GB** | +| MLX LoRA train (optional) | Apple Silicon | Reuses the MLX 4-bit base | +| CUDA QLoRA train (optional) | NVIDIA 24GB+ VRAM | Prefer GGUF for day-to-day `filter` | | Mock / smoke | CI or pipeline check | None | Quantized defaults stay in the **~5–7 GB** range. Full BF16 is not the happy path. @@ -22,15 +23,23 @@ Quantized defaults stay in the **~5–7 GB** range. Full BF16 is not the happy p ## Operator steps -1. Install: `pip install -e ".[dev]"` plus extras (`mlx`, `gguf`, `cuda`, `models`) as needed. +1. Install: `pip install -e ".[dev,mlx]"` plus extras (`gguf`, `cuda`, `models`) as needed. 2. Init: `personality-protect init` -3. Download one quantized artifact: `personality-protect download` (GGUF) and/or `--format mlx` on Apple Silicon. +3. Download the MLX base: `personality-protect download --format mlx` (add `--format gguf` only if you want `filter`). 4. Ingest local writing: `personality-protect ingest --linkedin --path ` -5. Select: `personality-protect select` (warns below 50 pieces; blocks below 20 unless `--force`) -6. Full train: `personality-protect train` (auto steps from SFT count). Useful flags: `--proof`, `--resume`, `--chunk-steps`, `--memory-gb`. CI uses `--smoke` / `--backend mock`. -7. Filter: `personality-protect filter --text "…"` -8. Compare: `personality-protect compare --synthetic slop_branding` -9. Eval: `personality-protect eval --synthetic slop_branding` +5. Index: `personality-protect index-voice` +6. Style card: `personality-protect build-style-profile` +7. Draft: `personality-protect write --topic "…" --points "…"` (add `--channel article` with 5+ `linkedin_article` pieces) +8. Receipt: `personality-protect eval-write-holdout --out receipt.json` +9. State check: `personality-protect status` + +Steps 1–9 need no training run. `write` uses base weights (`adapter=none`) plus the retrieval index and style card. + +## Optional experiments + +- Select + train a LoRA: `personality-protect select`, then `personality-protect train` (`--writer` for the brief→post writer LoRA). Useful flags: `--proof`, `--resume`, `--chunk-steps`, `--memory-gb`. CI uses `--smoke` / `--backend mock`. +- Load an adapter for a draft: `personality-protect write --adapter …` — only after `eval-write-holdout` shows it beating the default on holdouts. +- Rewrite/score existing text: `personality-protect filter --text "…"`, `personality-protect compare --synthetic slop_branding`, `personality-protect eval --synthetic slop_branding`. MLX train is chunked and checkpointed — a crash does not wipe a full run; use `--resume` (incomplete runs also auto-resume). diff --git a/docs/images/cli-shipped.png b/docs/images/cli-shipped.png index ca5272409c935fa0ba800fddf3f2e6b4fb61599f..35906818fcdc478d1b8075bf2bb4ac49bab0339a 100644 GIT binary patch literal 46010 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