Back to projects
Active Started Jun 2026

Style Calibrator

A self-paced app that turns a voice-and-style calibration pack into a clickable flow, then exports a prompt to generate your personal style profile.

Private Repo
JavaScript Python Swift SwiftUI WKWebView

The Problem

Asking an AI to “write in my voice” rarely works, because the model has no grounded description of what your voice actually is. The fix is a structured style profile — but building one by hand is tedious, and the answers that matter (how you write across contexts, what you sound like under pressure, which markers are essential to you) are easy to skip.

What I Built

A self-paced app that turns a voice-and-style calibration prompt pack into a clickable, save-as-you-go flow. You work through modules — real samples, baseline writing, contextual variation, sentence rewrites, phrase reactions, coding-agent response preferences — and the app exports a single prompt you paste into any AI to generate your personal communication-style profile.

Architecture

Style Calibrator is a collection tool, not a synthesis tool — every module answer, the coverage meter, and the exported prompt are built entirely client-side in one ~5,700-line vanilla-JS file (style-calibrator.html), with zero framework and zero required model call. The only place a model enters the app itself is an optional “adaptive tailoring” step that generates better follow-up comparisons; the profile itself is always produced by pasting the exported prompt into an AI of the user’s choosing, outside the app. Three shells serve the identical HTML: a double-clickable file (browser-only, no server), a zero-dependency Python helper (serve.py), and a native macOS app (SwiftUI + WKWebView over a Swift-native LocalServer) that mirrors the Python API route-for-route so the same frontend runs unchanged in all three.

flowchart LR
  U["User answers modules<br/>(A-K, R) in style-calibrator.html"] --> SV{Which shell?}
  SV -->|file://, no server| LS["localStorage autosave<br/>+ manual Save-backup"]
  SV -->|python3 serve.py| PY["Python ThreadingHTTPServer<br/>POST /api/save"]
  SV -->|StyleCalibrator.app| SW["Swift LocalServer<br/>POST /api/save"]
  PY --> FS[("answers.json + STATUS.md<br/>per-project folder, atomic write")]
  SW --> FS
  U -->|optional| AR["Adaptive tailoring:<br/>GET /api/ai-backends detect<br/>POST /api/ai-refresh"]
  AR --> BE{Backend picked}
  BE -->|claude CLI| CC["claude -p, tools disabled<br/>subprocess, stdin-only prompt"]
  BE -->|codex CLI| CX["codex exec, read-only sandbox<br/>neutral temp cwd"]
  BE -->|Ollama| OL["127.0.0.1:11434<br/>on-device, private"]
  CC --> AS[adaptiveState comparisons]
  CX --> AS
  OL --> AS
  AS --> U
  U --> EX["Export: synthesis prompt or<br/>focused generator<br/>(built from local state, no network)"]
  EX --> EXT["Paste into any AI<br/>(user's choice, outside the app)"]
  EXT --> PROF["Style profile, JSON,<br/>reusable generation prompt"]
  PROF -.optional.-> SKILL["SKILL.md.template →<br/>Claude Code skill"]

Models — which, where, why:

StageModel / tierWhere it runsWhy there
Module collection, coverage meter, prompt exportnoneclient-side JS, all shellsDeterministic UI/state problem — no model needed to ask questions, track coverage, or template a prompt from stored answers
Persistence (save/load)noneserve.py (stdlib http.server) or Swift LocalServerPlain JSON read/write to disk; no inference involved
Adaptive comparison batches (optional)Claude Code CLI, Codex CLI, or an installed Ollama model (user’s pick)/api/ai-refresh, invoked only when adaptive tailoring is turned onGenerates “most/least like me” comparison batches faster than static question lists; Ollama is offered specifically as the private, nothing-leaves-the-machine option, cloud CLIs as the convenience option
Final style-profile synthesisany AI the user pastes the exported prompt intooutside the app entirelyDeliberate boundary: collection is a UI problem (this app), synthesis is a reasoning problem the app does not own or vendor-lock — “bring your own model”

Tools & infra — which, why:

  • Python http.server/ThreadingHTTPServer (serve.py, stdlib only, no pip install) — the default durable-save shell; binds 127.0.0.1 only, atomic writes to a real folder so answers survive a browser wipe.
  • Swift Network.framework LocalServer (LocalServer.swift) — a hand-rolled HTTP/1.1 server in the macOS app, one route table mirroring serve.py’s API exactly (/api/save, /api/load, /api/projects, /api/ai-backends, /api/ai-refresh), so style-calibrator.html runs byte-for-byte unchanged inside WKWebView.
  • SwiftUI + WKWebView (StyleCalibratorApp.swift) — thin native shell that points a webview at the local server; the actual UI is the same HTML/JS used in the browser and Python paths, not a separate native rebuild.
  • XcodeGen (project.yml) + build_app.sh — generates the Xcode project and produces dist/StyleCalibrator.app, an unsigned local-only build (no notarization gate for this pass).
  • File storage only, no database<data-dir>/projects/<slug>/{answers.json, STATUS.md, meta.json}, rewritten atomically per keystroke; multiple named profiles live side by side as folders.
  • Ollama HTTP API (127.0.0.1:11434, /api/tags, /api/generate) — optional on-device backend for adaptive tailoring; live-probed for availability and installed models, never assumed.
  • claude / codex CLIs — optional cloud backends for adaptive tailoring, invoked as subprocess argv with the prompt on stdin (never shell-interpolated), tools disabled / read-only sandbox, single-flight semaphore, 120s timeout.
  • pytest (test_serve.py) and a Node test (test_html_logic.mjs) — cover the Python server’s security gates (slug/path containment, body-size caps, same-origin POST check) and the client-side scoring/coverage logic respectively.

How it works

  1. The user opens one of three shells: the standalone style-calibrator.html file (browser storage only), python3 serve.py (folder-backed, any browser including Safari), or the packaged StyleCalibrator.app (SwiftUI/WKWebView wrapping the same HTML over a native Swift server). All three serve identical markup and JS.
  2. The user works through modules — R (real samples), A-F (baseline writing, preference, semantic adjacency, sentence rewrites, rhetorical structure, tone/stance), V (spoken voice, transcript-only), P/S/I/X (phrase reactions, sound, mechanics, expression range), and K (coding-agent response preferences, a separate axis from writing voice).
  3. Every answer change triggers an autosave: in the browser-only shell it’s localStorage plus an on-demand file backup; in the served shells it’s a POST /api/save that both serve.py and the Swift LocalServer write atomically to <data-dir>/projects/<slug>/answers.json, with STATUS.md updated as a human-readable coverage snapshot.
  4. Optionally, the user turns on adaptive refinement. The client calls GET /api/ai-backends, which live-detects claude/codex on PATH and probes 127.0.0.1:11434/api/tags for a reachable Ollama daemon and its installed models — nothing is assumed available.
  5. With a backend selected, the client builds a comparison-batch prompt from current answers plus any steering notes (“fewer em-dashes,” “no exclamation points”) and POSTs it to /api/ai-refresh. The server dispatches by backend: claude -p with --allowedTools "" (tools disabled), codex exec --sandbox read-only in a neutral temp directory, or an Ollama /api/generate call with format:"json". All subprocess invocations pass the prompt via stdin only (never argv, never a shell) and are bounded by a 120s timeout and a single-flight semaphore.
  6. The AI’s response is parsed into adaptiveState, and the user picks “most/least like me” on generated comparisons; each pick updates the targeted dimension and correlated traits (e.g., formality and directness move together) so later batches skip contexts already well-covered.
  7. When the user is ready, the client builds an export entirely from local state — the full synthesis prompt (with confidence-discipline instructions so thin evidence doesn’t read as false certainty) or one of the focused generators (email, blog, personal chat, professional chat, coding-agent response). No network call is made to produce this text.
  8. The user copies or downloads the exported prompt and pastes it into any AI they choose — this is outside the app and is the only step that actually produces the style profile.
  9. The returned profile includes an executive style thesis, confidence heatmap, per-register map, signature sentence patterns, do/avoid rules, a reusable generation prompt, a JSON profile, and a calibration backlog; the reusable prompt can be dropped into a system prompt, custom GPT, Claude Project instructions, or filled into SKILL.md.template to become a /your-style Claude Code skill.

Local-First by Design

Two ways to run it, both with no account and no cloud:

  • Quick — double-click the single HTML file; answers autosave to the browser, with file-backup on demand.
  • Durable — a zero-dependency Python helper (serve.py, stdlib only, no internet) writes every keystroke atomically to a real folder, so work survives a browser wipe and reopens straight from disk.
  • Native — the packaged macOS app wraps the same HTML in a WKWebView over a Swift-native local server, for a double-clickable app instead of a terminal command.

By default, answers never leave the machine. Adaptive AI tailoring is the one place data can leave — and only when invoked, and only for the backend the user explicitly picks (Ollama stays fully on-device). The repo ships the tool, never anyone’s data.

Tech stack

Vanilla JavaScript/HTML for the entire UI and export logic (style-calibrator.html, no frontend framework). Python 3 standard library (http.server) for the durable local-save helper (serve.py, ai_backends.py) — zero pip dependencies. Swift + SwiftUI + WKWebView + Network.framework for the native macOS app (StyleCalibratorApp/), built via XcodeGen (project.yml) into an unsigned local .app. No database, no vector store, no cloud service required — JSON files on disk are the only persistence layer. Optional integrations: Ollama’s local HTTP API, and the claude/codex CLIs, both invoked read-only/tool-disabled and only for the adaptive-tailoring step.

Results

  • One frontend (style-calibrator.html) runs unmodified across three shells — browser-only, Python-served, and native macOS — because the Swift LocalServer and the Python serve.py expose the identical /api/* route table.
  • Zero required model calls: collection, coverage tracking, and prompt export are fully deterministic; the app functions completely offline end to end if adaptive tailoring is left off.
  • The security model for the one place a model is invoked is deterministic, not prompt-based: subprocess argv only (never shell), prompt always on stdin, tools disabled (claude) / read-only sandbox in a neutral temp cwd (codex), single-flight guard, hard timeout.

Lessons

  1. Collection and synthesis were deliberately split across a hard boundary — a UI/state problem (this app) versus a reasoning problem (any AI the user already has) — rather than baking a vendor API into the tool, so the app has no API key, no billing surface, and no lock-in to one model provider.
  2. The macOS app was built as a thin WKWebView shell over a Swift port of the exact same server API, not a native rewrite of the UI, so one JS codebase stays the single source of truth for the actual product logic across all three run modes.
  3. Making Ollama a first-class, live-probed backend (not just claude/codex) matters specifically because a style profile is a writing fingerprint — the private, on-device option needed to be as easy to pick as the cloud ones, not a fallback.