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.
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:
| Stage | Model / tier | Where it runs | Why there |
|---|---|---|---|
| Module collection, coverage meter, prompt export | none | client-side JS, all shells | Deterministic UI/state problem — no model needed to ask questions, track coverage, or template a prompt from stored answers |
| Persistence (save/load) | none | serve.py (stdlib http.server) or Swift LocalServer | Plain 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 on | Generates “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 synthesis | any AI the user pastes the exported prompt into | outside the app entirely | Deliberate 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; binds127.0.0.1only, atomic writes to a real folder so answers survive a browser wipe. - Swift
Network.frameworkLocalServer(LocalServer.swift) — a hand-rolled HTTP/1.1 server in the macOS app, one route table mirroringserve.py’s API exactly (/api/save,/api/load,/api/projects,/api/ai-backends,/api/ai-refresh), sostyle-calibrator.htmlruns byte-for-byte unchanged insideWKWebView. - 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 producesdist/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/codexCLIs — 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
- The user opens one of three shells: the standalone
style-calibrator.htmlfile (browser storage only),python3 serve.py(folder-backed, any browser including Safari), or the packagedStyleCalibrator.app(SwiftUI/WKWebView wrapping the same HTML over a native Swift server). All three serve identical markup and JS. - 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).
- Every answer change triggers an autosave: in the browser-only shell it’s
localStorageplus an on-demand file backup; in the served shells it’s aPOST /api/savethat bothserve.pyand the SwiftLocalServerwrite atomically to<data-dir>/projects/<slug>/answers.json, withSTATUS.mdupdated as a human-readable coverage snapshot. - Optionally, the user turns on adaptive refinement. The client calls
GET /api/ai-backends, which live-detectsclaude/codexonPATHand probes127.0.0.1:11434/api/tagsfor a reachable Ollama daemon and its installed models — nothing is assumed available. - 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 -pwith--allowedTools ""(tools disabled),codex exec --sandbox read-onlyin a neutral temp directory, or an Ollama/api/generatecall withformat:"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. - 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. - 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.
- 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.
- 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.templateto become a/your-styleClaude 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 SwiftLocalServerand the Pythonserve.pyexpose 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
- 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.
- 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.
- 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.