adikuma commited on
Commit
fd0b01f
·
verified ·
1 Parent(s): a5a0606

initial upload: cleanup code and 688-pair seed dataset

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.local.example +5 -0
  2. .gitattributes +6 -35
  3. .gitignore +25 -0
  4. .python-version +1 -0
  5. DEVELOPMENT.md +102 -0
  6. Makefile +53 -0
  7. README.md +114 -0
  8. configs/data.yaml +12 -0
  9. configs/inject.yaml +50 -0
  10. configs/train.yaml +55 -0
  11. data/seed/batches/casual_messages.json +83 -0
  12. data/seed/batches/long_form_thoughts.json +1 -0
  13. data/seed/batches/meeting_notes.json +83 -0
  14. data/seed/batches/mixed_content.json +72 -0
  15. data/seed/batches/professional_emails.json +82 -0
  16. data/seed/batches/questions_and_asks.json +72 -0
  17. data/seed/batches/technical_dictation.json +82 -0
  18. data/seed/batches/todo_lists.json +83 -0
  19. data/seed/synthetic_pairs.csv +0 -0
  20. data/seed/synthetic_pairs.jsonl +0 -0
  21. docs/HANDBOOK.md +319 -0
  22. docs/data_images/category_counts.png +0 -0
  23. docs/data_images/faithfulness.png +0 -0
  24. docs/data_images/filler_intensity.png +0 -0
  25. docs/data_images/length_distribution.png +0 -0
  26. docs/data_images/raw_vs_clean_length.png +0 -0
  27. docs/data_images/top_fillers.png +0 -0
  28. docs/data_report.md +136 -0
  29. docs/model_card.md +114 -0
  30. docs/model_report.md +86 -0
  31. pyproject.toml +38 -0
  32. scripts/01_download.py +23 -0
  33. scripts/02_train.py +34 -0
  34. scripts/03_evaluate.py +114 -0
  35. scripts/04_export.py +40 -0
  36. scripts/05_benchmark.py +107 -0
  37. scripts/06_pack_and_ship.py +24 -0
  38. scripts/explore_data.py +279 -0
  39. scripts/push_code_to_hub.py +150 -0
  40. scripts/push_to_hub.py +110 -0
  41. src/cleanup/__init__.py +1 -0
  42. src/cleanup/config.py +101 -0
  43. src/cleanup/data/__init__.py +1 -0
  44. src/cleanup/data/download.py +112 -0
  45. src/cleanup/data/inject.py +135 -0
  46. src/cleanup/data/tokenize.py +39 -0
  47. src/cleanup/eval/__init__.py +1 -0
  48. src/cleanup/eval/latency.py +93 -0
  49. src/cleanup/eval/metrics.py +234 -0
  50. src/cleanup/eval/report.py +124 -0
.env.local.example ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # copy to .env.local and fill in. .env.local is gitignored.
2
+ # HF_TOKEN must be a write token if you intend to run scripts/push_to_hub.py.
3
+ # read tokens are fine if you only need to download from gated hub repos.
4
+
5
+ HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxx
.gitattributes CHANGED
@@ -1,35 +1,6 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.jsonl text
2
+ *.csv text
3
+ *.md text
4
+ *.py text
5
+ *.yaml text
6
+ *.toml text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # env and secrets
2
+ .env.local
3
+ .env
4
+
5
+ # python
6
+ __pycache__/
7
+ *.pyc
8
+ *.pyo
9
+ .pytest_cache/
10
+ .venv/
11
+
12
+ # uv
13
+ .uv/
14
+
15
+ # huggingface cache (kept in workspace via HF_HOME on vast)
16
+ .hf/
17
+
18
+ # training outputs
19
+ runs/
20
+ dist/
21
+ data/
22
+ *.log
23
+
24
+ # notebook checkpoints
25
+ .ipynb_checkpoints/
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
DEVELOPMENT.md ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cleanup
2
+
3
+ > Status: implementation. Run `make smoke` first to validate wiring on CPU. Run `make all RUN_ID=r1` on a Vast.ai GPU for the real training pass. Run `make benchmark` locally on a laptop for the latency truth.
4
+
5
+ Fine-tune `Qwen/Qwen2.5-0.5B-Instruct` with LoRA to clean Mumble dictation transcripts. Remove fillers, repeats, and false starts. Fix punctuation and capitalization. Never paraphrase, summarize, or add content. Export to ONNX (fp32 + int8) for the Mumble Rust app's `ort` runtime.
6
+
7
+ See:
8
+ - [`docs/model_card.md`](docs/model_card.md) — published as the HuggingFace Hub README.
9
+ - [`docs/model_report.md`](docs/model_report.md) — human-readable evaluation report (filled in after a run).
10
+ - [`docs/HANDBOOK.md`](docs/HANDBOOK.md) — deep specs.
11
+ - [`../../handoffs/finetune-explainer.md`](../../handoffs/finetune-explainer.md) — conceptual walkthrough.
12
+ - [`../../handoffs/cleanup-execution-plan.md`](../../handoffs/cleanup-execution-plan.md) — operational plan with Vast.ai commands and budget.
13
+
14
+ ## Pipeline
15
+
16
+ ```
17
+ make sync install deps via uv
18
+ make data 01: split the seed jsonl into train/val/test under data/pairs/
19
+ make train RUN_ID=r1 02: lora sft on qwen2.5-0.5b-instruct
20
+ make evaluate RUN_ID=r1 03: raw vs base vs fine-tune on the held-out test, plus adversarial check
21
+ make export RUN_ID=r1 04: merge lora and export fp32 + int8 onnx
22
+ make benchmark RUN_ID=r1 05: cpu latency, LOCAL only
23
+ make pack RUN_ID=r1 06: tar the run, print sha256 and the vastai copy line
24
+ make push RUN_ID=r1 publish to the hf hub
25
+ make all RUN_ID=r1 pipeline end to end on the gpu (no benchmark, no push)
26
+ make smoke tiny cpu run, no gpu needed
27
+ ```
28
+
29
+ ## Data
30
+
31
+ Source of truth: `data/seed/synthetic_pairs.jsonl` — **688 hand-curated (raw, clean) pairs** generated by a multi-agent workflow across 8 categories of dictation:
32
+
33
+ - `casual_messages` — Slack / texts
34
+ - `professional_emails` — work emails
35
+ - `meeting_notes` — voice memos during/after meetings
36
+ - `technical_dictation` — code comments, API notes, design sketches
37
+ - `todo_lists` — action items, shopping, plans
38
+ - `long_form_thoughts` — paragraph-length thinking out loud
39
+ - `questions_and_asks` — questions, requests, asks
40
+ - `mixed_content` — names, numbers, dates, times, addresses
41
+
42
+ Each pair is `{ raw: <Parakeet-shaped lowercase no-punct disfluent input>, clean: <proper English output> }`. The clean side is faithful by construction: every content word in `clean` exists in `raw` (modulo homophone fixes). The model is mechanically incapable of learning to invent content because the data never rewards it.
43
+
44
+ `scripts/01_download.py` splits the seed stratified-by-category into train/val/test (85/10/5) and writes `data/pairs/{train,val,test}.json` plus `data/pairs/meta.json`.
45
+
46
+ The 9 synthetic disfluency operators in `src/cleanup/data/inject.py` are kept for an optional v1.1 augmentation pass that expands the seed by 3-5x by stochastically corrupting clean sides. Off by default in v1.
47
+
48
+ ## Layout
49
+
50
+ ```
51
+ configs/
52
+ data.yaml seed path + split ratios
53
+ inject.yaml optional operator probabilities (v1.1)
54
+ train.yaml lora + adamw hyperparameters
55
+ src/cleanup/
56
+ config.py yaml -> typed dataclasses
57
+ prompts.py frozen system prompt
58
+ infer.py model -> cleaned string (one-shot helper)
59
+ data/
60
+ download.py load seed jsonl, split stratified, write data/pairs/
61
+ inject.py 9 disfluency operators (v1.1 augmentation, off by default)
62
+ tokenize.py chatml format + the response_template for completion-only loss
63
+ train/
64
+ model.py load qwen + apply lora + autodetect precision
65
+ trainer.py trl sftrainer wiring with completion-only collator
66
+ eval/
67
+ metrics.py disfluency removal, punct f1, faithfulness, length, pass rate
68
+ latency.py cpu p50/p95/p99 sweep + realistic mix
69
+ report.py read jsons -> charts + markdown
70
+ export/
71
+ merge.py peft merge_and_unload
72
+ to_onnx.py optimum onnx export
73
+ quantize.py ort int8 dynamic quantization
74
+ pack/
75
+ ship.py tar + sha256 + vastai copy line
76
+ scripts/
77
+ 01_download.py split seed jsonl into train/val/test
78
+ 02_train.py lora sft on qwen2.5-0.5b-instruct
79
+ 03_evaluate.py raw vs base vs fine-tune + adversarial questions
80
+ 04_export.py merge + onnx + int8
81
+ 05_benchmark.py cpu latency (LOCAL only)
82
+ 06_pack_and_ship.py tar + sha256 + vastai copy line
83
+ push_to_hub.py hf hub publish (private by default)
84
+ docs/
85
+ model_card.md hf hub readme (with frontmatter)
86
+ model_report.md human-readable report (numbers filled after a run)
87
+ HANDBOOK.md deep specs
88
+ data/
89
+ seed/synthetic_pairs.jsonl the 612-pair source of truth
90
+ seed/synthetic_pairs.csv same data, human-inspectable
91
+ seed/batches/ per-category breakdowns
92
+ pairs/{train,val,test}.json created by scripts/01_download.py
93
+ runs/<run-id>/
94
+ model/ peft adapter + tokenizer
95
+ merged/ transformers checkpoint after lora merge
96
+ onnx/model.onnx fp32 merged onnx
97
+ onnx/int8/model.onnx int8 quantized onnx
98
+ eval.json quality metrics (raw, base, fine_tuned, adversarial)
99
+ latency_benchmark.json cpu latency (only after local benchmark)
100
+ training_history.json loss curves
101
+ dist/ packed tarballs ready to scp
102
+ ```
Makefile ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: help sync data train evaluate export benchmark pack push all smoke clean
2
+
3
+ RUN_ID ?= run-$(shell date -u +%Y%m%d-%H%M%S)
4
+ LR ?=
5
+ EPOCHS ?=
6
+
7
+ help:
8
+ @echo "make sync install dependencies via uv"
9
+ @echo "make data 01: download disfluencyspeech, build injection pairs"
10
+ @echo "make train RUN_ID=r1 02: lora sft on qwen2.5-0.5b-instruct"
11
+ @echo "make evaluate RUN_ID=r1 03: raw vs base vs fine-tune on the real held-out test"
12
+ @echo "make export RUN_ID=r1 04: merge lora and export fp32 + int8 onnx"
13
+ @echo "make benchmark RUN_ID=r1 05: cpu latency (run locally for the truth)"
14
+ @echo "make pack RUN_ID=r1 06: tar the run, print sha256 and the scp line"
15
+ @echo "make push RUN_ID=r1 publish to the hf hub (uses HF_TOKEN in .env.local)"
16
+ @echo "make all RUN_ID=r1 the whole pipeline end to end (no push, no benchmark)"
17
+ @echo "make smoke tiny cpu run, no gpu needed, validates wiring"
18
+
19
+ sync:
20
+ uv sync
21
+
22
+ data:
23
+ uv run python scripts/01_download.py
24
+
25
+ train:
26
+ uv run python scripts/02_train.py --run-id $(RUN_ID) $(if $(LR),--lr $(LR),) $(if $(EPOCHS),--epochs $(EPOCHS),)
27
+
28
+ evaluate:
29
+ uv run python scripts/03_evaluate.py --run-id $(RUN_ID)
30
+
31
+ export:
32
+ uv run python scripts/04_export.py --run-id $(RUN_ID)
33
+
34
+ benchmark:
35
+ uv run python scripts/05_benchmark.py --run-id $(RUN_ID)
36
+
37
+ pack:
38
+ uv run python scripts/06_pack_and_ship.py --run-id $(RUN_ID)
39
+
40
+ push:
41
+ uv run python scripts/push_to_hub.py --run-id $(RUN_ID)
42
+
43
+ all: sync data train evaluate export pack
44
+
45
+ smoke:
46
+ uv run python scripts/01_download.py --smoke
47
+ uv run python scripts/02_train.py --run-id smoke --smoke
48
+ uv run python scripts/03_evaluate.py --run-id smoke --smoke
49
+
50
+ clean:
51
+ rm -rf runs/*
52
+ rm -rf data/pairs
53
+ rm -rf dist/*
README.md ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen2.5-0.5B-Instruct
4
+ language:
5
+ - en
6
+ pipeline_tag: text2text-generation
7
+ library_name: transformers
8
+ tags:
9
+ - dictation
10
+ - cleanup
11
+ - transcript
12
+ - lora
13
+ - onnx
14
+ - mumble
15
+ metrics:
16
+ - exact_match
17
+ - f1
18
+ ---
19
+
20
+ # mumble-cleanup
21
+
22
+ A small fine-tuned language model that cleans speech-to-text dictation transcripts. Fine-tuned from `Qwen/Qwen2.5-0.5B-Instruct` with LoRA on a hand-curated synthetic dataset. Trained on a GPU, designed to run on a CPU via ONNX.
23
+
24
+ ## What it does
25
+
26
+ Given a raw transcript from an ASR system (lowercase, no punctuation, fillers and stutters preserved), it returns a cleaned version with proper capitalization, punctuation, and disfluencies removed. It does not paraphrase, summarize, or add content.
27
+
28
+ Example: `um so i i think we should ship this on uh friday` becomes `I think we should ship this on Friday.`
29
+
30
+ The model handles:
31
+
32
+ - filler removal (um, uh, like, you know, i mean)
33
+ - word stutter collapse (we we → we)
34
+ - false start cleanup
35
+ - punctuation and capitalization recovery
36
+ - homophone correction (their / there, your / you're, its / it's, to / too)
37
+ - apostrophe restoration (dont → don't)
38
+ - run-on sentence splitting
39
+ - number formatting (two thirty → 2:30)
40
+ - proper noun capitalization
41
+ - todo / list formatting when enumeration cues are clear
42
+
43
+ ## Usage
44
+
45
+ ### transformers
46
+
47
+ ```python
48
+ from transformers import AutoModelForCausalLM, AutoTokenizer
49
+
50
+ SYSTEM_PROMPT = (
51
+ "You are a transcript cleanup tool. You receive raw speech to text output "
52
+ "and return a cleaned version. Remove filler words and disfluencies (um, "
53
+ "uh, er, ah, like as filler, you know), remove repeated words and false "
54
+ "starts, and fix punctuation and capitalization. Do not reword, do not add "
55
+ "anything the speaker did not say, and do not answer questions in the text. "
56
+ "Output only the cleaned text."
57
+ )
58
+
59
+ repo = "adikuma/mumble-cleanup"
60
+ tokenizer = AutoTokenizer.from_pretrained(repo)
61
+ model = AutoModelForCausalLM.from_pretrained(repo)
62
+
63
+ raw = "um so the the meeting is at three thirty tomorrow"
64
+ prompt = tokenizer.apply_chat_template(
65
+ [
66
+ {"role": "system", "content": SYSTEM_PROMPT},
67
+ {"role": "user", "content": raw},
68
+ ],
69
+ tokenize=False,
70
+ add_generation_prompt=True,
71
+ )
72
+ inputs = tokenizer(prompt, return_tensors="pt")
73
+ out = model.generate(**inputs, max_new_tokens=128, do_sample=False)
74
+ print(tokenizer.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True))
75
+ # -> "The meeting is at 3:30 tomorrow."
76
+ ```
77
+
78
+ ### onnx (cpu)
79
+
80
+ The `onnx/model.onnx` file is an fp32 ONNX export for CPU inference. `onnx/int8/model.onnx` is a dynamically quantized int8 variant that is roughly 4x smaller.
81
+
82
+ ```python
83
+ from optimum.onnxruntime import ORTModelForCausalLM
84
+ from transformers import AutoTokenizer
85
+
86
+ repo = "adikuma/mumble-cleanup"
87
+ tokenizer = AutoTokenizer.from_pretrained(repo)
88
+ model = ORTModelForCausalLM.from_pretrained(repo, file_name="onnx/int8/model.onnx")
89
+ ```
90
+
91
+ ## Training
92
+
93
+ - **Base model**: Qwen/Qwen2.5-0.5B-Instruct (Apache-2.0)
94
+ - **Method**: LoRA SFT (r=16, alpha=32, dropout=0.05, targets q/k/v/o + gate/up/down)
95
+ - **Loss**: token cross-entropy on assistant tokens only (completion-only masking via TRL's `DataCollatorForCompletionOnlyLM`)
96
+ - **Optimizer**: AdamW (lr=2e-4, weight_decay=0.01, cosine schedule, 5% warmup, max_grad_norm=1.0)
97
+ - **Batching**: per-device 8, gradient accumulation 4 (effective 32), max sequence length 512
98
+ - **Precision**: bf16 on GPUs that support it, fp16 fallback
99
+ - **Dataset**: 688 hand-curated (raw, clean) pairs spanning 8 dictation categories (casual messages, professional emails, meeting notes, technical dictation, todo lists, long-form thoughts, questions/asks, mixed content). Stratified 85/10/5 train/val/test split.
100
+
101
+ ## Limitations
102
+
103
+ - English only.
104
+ - Trained on synthetic data; real ASR output may have failure modes the synthetic operators did not model.
105
+ - Designed for short-to-medium dictation (up to ~512 tokens). Longer inputs must be chunked.
106
+ - The model can occasionally over-correct when a user genuinely intends a fragment ("running late.") — fine-tune favors fixed-up sentences.
107
+
108
+ ## License
109
+
110
+ Apache-2.0. See [`LICENSE`](../../LICENSE) at the Mumble repo root.
111
+
112
+ ## Acknowledgements
113
+
114
+ Built on top of [`Qwen/Qwen2.5-0.5B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct) by the Qwen team.
configs/data.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data preparation config. v1 uses the hand-crafted seed pairs at the path
2
+ # below as the source of truth. scripts/01_download.py splits them into
3
+ # train/val/test under data/pairs/.
4
+
5
+ seed_path: data/seed/synthetic_pairs.jsonl
6
+
7
+ splits:
8
+ train: 0.85
9
+ val: 0.10
10
+ test: 0.05
11
+
12
+ random_seed: 1337
configs/inject.yaml ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # synthetic disfluency operator config (OPTIONAL, v1.1).
2
+ #
3
+ # v1 trains on the hand-crafted seed pairs only. these 9 operators are kept
4
+ # for a future augmentation pass that applies them to clean sides to expand
5
+ # the training set. enable with `--augment` on scripts/01_download.py once
6
+ # implemented. probabilities tuned to roughly match real Parakeet output.
7
+
8
+ sampling:
9
+ ops_per_example_min: 1
10
+ ops_per_example_max: 4
11
+
12
+ ops:
13
+ add_filler:
14
+ p: 0.55
15
+ vocab: [um, uh, like, "you know", "i mean", so, well, er, ah]
16
+ max_inserts: 2
17
+ word_stutter:
18
+ p: 0.20
19
+ max_repeats: 1
20
+ false_start:
21
+ p: 0.10
22
+ prefixes:
23
+ - "wait,"
24
+ - "actually no,"
25
+ - "i mean,"
26
+ - "scratch that,"
27
+ strip_punct:
28
+ p: 0.70
29
+ drop_rate: 0.6
30
+ lowercase:
31
+ p: 0.85
32
+ merge_sentences:
33
+ p: 0.25
34
+ dropped_apostrophe:
35
+ p: 0.30
36
+ mishear_homophone:
37
+ p: 0.08
38
+ pairs:
39
+ - [their, there]
40
+ - [there, their]
41
+ - [your, "you're"]
42
+ - ["you're", your]
43
+ - [its, "it's"]
44
+ - ["it's", its]
45
+ - [to, too]
46
+ - [too, to]
47
+ repeated_chunk:
48
+ p: 0.06
49
+ chunk_size_min: 2
50
+ chunk_size_max: 4
configs/train.yaml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cleanup model training configuration. single source of truth, consumed by
2
+ # scripts/02_train.py via cleanup.config.load_train_config. never put
3
+ # hyperparameters in code.
4
+
5
+ base_model: Qwen/Qwen2.5-0.5B-Instruct
6
+ language: en
7
+
8
+ # dataset (produced by scripts/01_download.py)
9
+ data_dir: data/pairs
10
+ max_seq_length: 512
11
+ train_rows: null # null means use all
12
+ val_rows: null
13
+
14
+ # lora adapter
15
+ lora:
16
+ r: 16
17
+ alpha: 32
18
+ dropout: 0.05
19
+ bias: none
20
+ target_modules: [q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj]
21
+
22
+ # adamw
23
+ learning_rate: 2.0e-4
24
+ weight_decay: 0.01
25
+ warmup_ratio: 0.05
26
+ max_grad_norm: 1.0
27
+ adam_beta1: 0.9
28
+ adam_beta2: 0.999
29
+ adam_epsilon: 1.0e-8
30
+
31
+ # schedule
32
+ # 584 train rows / effective batch 16 = ~37 steps/epoch. 4 epochs = ~148 steps.
33
+ # enough for lora on a narrow task without overfitting.
34
+ num_epochs: 4
35
+ lr_scheduler_type: cosine
36
+
37
+ # batching
38
+ train_batch_size: 8
39
+ eval_batch_size: 16
40
+ gradient_accumulation_steps: 2
41
+
42
+ # precision (prefer bf16, fall back to fp16, then cpu fp32)
43
+ bf16: true
44
+ fp16: false
45
+ tf32: true
46
+
47
+ # misc
48
+ seed: 42
49
+ metric_for_best_model: eval_loss
50
+ greater_is_better: false
51
+ save_total_limit: 2
52
+ logging_steps: 10
53
+ eval_steps: 30
54
+ save_steps: 30
55
+ dataloader_num_workers: 2
data/seed/batches/casual_messages.json ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "raw": "hey sarah you free for lunch around noon", "clean": "Hey Sarah, you free for lunch around noon?" },
3
+ { "raw": "on my way ill be there in five", "clean": "On my way. I'll be there in 5." },
4
+ { "raw": "um yeah that works for me", "clean": "Yeah, that works for me." },
5
+ { "raw": "can you send me the figma link when you get a sec", "clean": "Can you send me the Figma link when you get a sec?" },
6
+ { "raw": "running late traffic is awful sorry", "clean": "Running late, traffic is awful. Sorry." },
7
+ { "raw": "just landed at sfo grabbing an uber now", "clean": "Just landed at SFO, grabbing an Uber now." },
8
+ { "raw": "did you see the email from priya about the launch", "clean": "Did you see the email from Priya about the launch?" },
9
+ { "raw": "yeah lol that meeting was a waste", "clean": "Yeah lol, that meeting was a waste." },
10
+ { "raw": "ok cool see you at three", "clean": "Ok cool, see you at 3." },
11
+ { "raw": "uh i think the standup got moved to wednesday", "clean": "I think the standup got moved to Wednesday." },
12
+ { "raw": "hey alex you know if the office is open monday its presidents day", "clean": "Hey Alex, you know if the office is open Monday? It's Presidents Day." },
13
+ { "raw": "im at peets if you wanna swing by", "clean": "I'm at Peet's if you wanna swing by." },
14
+ { "raw": "so um i wont be able to make it tonight my dog is sick", "clean": "I won't be able to make it tonight, my dog is sick." },
15
+ { "raw": "can you grab me a coffee on your way in oat milk latte please", "clean": "Can you grab me a coffee on your way in? Oat milk latte please." },
16
+ { "raw": "diego said the prs are ready for review whenever your free", "clean": "Diego said the PRs are ready for review whenever you're free." },
17
+ { "raw": "i mean i guess we could push the demo to friday if needed", "clean": "I guess we could push the demo to Friday if needed." },
18
+ { "raw": "you know what nevermind ill figure it out", "clean": "You know what, nevermind. I'll figure it out." },
19
+ { "raw": "did chen send the slack invite yet", "clean": "Did Chen send the Slack invite yet?" },
20
+ { "raw": "yo just saw the news about stripe wild", "clean": "Yo, just saw the news about Stripe. Wild." },
21
+ { "raw": "uh hey can you cover the standup tomorrow i have a dentist thing", "clean": "Hey, can you cover the standup tomorrow? I have a dentist thing." },
22
+ { "raw": "maya wants to know if your coming to the offsite in tahoe", "clean": "Maya wants to know if you're coming to the offsite in Tahoe." },
23
+ { "raw": "im like ten minutes out should i pick anything up", "clean": "I'm about 10 minutes out. Should I pick anything up?" },
24
+ { "raw": "ok so um basically the client wants to push everything back two weeks which is fine i guess but it means we have to redo the timeline and i havent told james yet so dont mention it", "clean": "Ok so basically the client wants to push everything back 2 weeks, which is fine I guess, but it means we have to redo the timeline. I haven't told James yet, so don't mention it." },
25
+ { "raw": "thanks so much appreciate it", "clean": "Thanks so much, appreciate it." },
26
+ { "raw": "lol no way", "clean": "Lol no way." },
27
+ { "raw": "hey i i think we need to talk about the q3 numbers when you have a sec", "clean": "Hey, I think we need to talk about the Q3 numbers when you have a sec." },
28
+ { "raw": "did you eat yet im starving", "clean": "Did you eat yet? I'm starving." },
29
+ { "raw": "uh so the wifi is down at the office again", "clean": "So the wifi is down at the office again." },
30
+ { "raw": "can we move our one on one to thursday at two thirty", "clean": "Can we move our 1:1 to Thursday at 2:30?" },
31
+ { "raw": "hey um quick question are we using notion or linear for the roadmap now i keep forgetting", "clean": "Hey, quick question. Are we using Notion or Linear for the roadmap now? I keep forgetting." },
32
+ { "raw": "no worries take your time", "clean": "No worries, take your time." },
33
+ { "raw": "got it ill push the fix tonight", "clean": "Got it, I'll push the fix tonight." },
34
+ { "raw": "you know what i mean its just like the whole thing with the vendor is dragging on and we we still dont have a signed contract", "clean": "It's just like, the whole thing with the vendor is dragging on and we still don't have a signed contract." },
35
+ { "raw": "are you in the office today or wfh", "clean": "Are you in the office today or WFH?" },
36
+ { "raw": "sarah said the deck looks great by the way", "clean": "Sarah said the deck looks great by the way." },
37
+ { "raw": "um so i looked at the metrics and conversion is up like fifteen percent week over week which is huge but i think it might be from the email campaign not the new landing page", "clean": "So I looked at the metrics and conversion is up about 15% week over week, which is huge, but I think it might be from the email campaign, not the new landing page." },
38
+ { "raw": "wait what time was the call again", "clean": "Wait, what time was the call again?" },
39
+ { "raw": "lemme know when your free to chat", "clean": "Let me know when you're free to chat." },
40
+ { "raw": "ill bring donuts tomorrow what kind do you want", "clean": "I'll bring donuts tomorrow. What kind do you want?" },
41
+ { "raw": "ok ok ok give me five minutes", "clean": "Ok ok ok, give me 5 minutes." },
42
+ { "raw": "did you finish the writeup for the all hands", "clean": "Did you finish the writeup for the all hands?" },
43
+ { "raw": "um yeah so i talked to legal and they said we cant ship until the dpa is signed which sucks but i mean its what it is", "clean": "Yeah, so I talked to legal and they said we can't ship until the DPA is signed, which sucks, but it is what it is." },
44
+ { "raw": "youre the best thank you", "clean": "You're the best, thank you." },
45
+ { "raw": "so uh whats the plan for friday happy hour", "clean": "So what's the plan for Friday happy hour?" },
46
+ { "raw": "hey can you check if the staging env is up its giving me a 502", "clean": "Hey, can you check if the staging env is up? It's giving me a 502." },
47
+ { "raw": "ill be there in like twenty save me a seat", "clean": "I'll be there in about 20. Save me a seat." },
48
+ { "raw": "did priya respond to your dm yet", "clean": "Did Priya respond to your DM yet?" },
49
+ { "raw": "ugh forgot my laptop charger at home gonna head back real quick", "clean": "Ugh, forgot my laptop charger at home. Gonna head back real quick." },
50
+ { "raw": "i mean honestly the new logo is fine but its not really my vibe", "clean": "Honestly the new logo is fine but it's not really my vibe." },
51
+ { "raw": "so um chen and i were talking and we think we should um just kill the feature and move on its not worth the eng time", "clean": "So Chen and I were talking and we think we should just kill the feature and move on. It's not worth the eng time." },
52
+ { "raw": "what time does the kbbq place close tonight", "clean": "What time does the KBBQ place close tonight?" },
53
+ { "raw": "alex is out sick today fyi", "clean": "Alex is out sick today, FYI." },
54
+ { "raw": "okay so basically the the issue is that the api is rate limiting us at like 100 requests per minute and we need like 500 so we need to either upgrade the plan or batch the calls", "clean": "Okay so basically the issue is that the API is rate limiting us at about 100 requests per minute and we need around 500, so we need to either upgrade the plan or batch the calls." },
55
+ { "raw": "haha that is so true", "clean": "Haha, that is so true." },
56
+ { "raw": "yo where do you want to grab dinner saturday", "clean": "Yo, where do you want to grab dinner Saturday?" },
57
+ { "raw": "the ac in the conference room is broken again", "clean": "The AC in the conference room is broken again." },
58
+ { "raw": "did you get my venmo for the dinner last week", "clean": "Did you get my Venmo for the dinner last week?" },
59
+ { "raw": "uh hey just a heads up the the client call got pushed to four", "clean": "Hey, just a heads up, the client call got pushed to 4." },
60
+ { "raw": "can you send me your address ill drop off the laptop", "clean": "Can you send me your address? I'll drop off the laptop." },
61
+ { "raw": "so um maya wants to know if you can review her pr before eod its blocking her merge", "clean": "So Maya wants to know if you can review her PR before EOD. It's blocking her merge." },
62
+ { "raw": "lol classic james", "clean": "Lol, classic James." },
63
+ { "raw": "im outside whenever your ready", "clean": "I'm outside whenever you're ready." },
64
+ { "raw": "hey um do you have the slides from the q2 review i cant find them on drive", "clean": "Hey, do you have the slides from the Q2 review? I can't find them on Drive." },
65
+ { "raw": "so the the new espresso machine in the kitchen is broken already someones gotta talk to facilities", "clean": "So the new espresso machine in the kitchen is broken already. Someone's gotta talk to facilities." },
66
+ { "raw": "did sarah mention anything about the bonus pool", "clean": "Did Sarah mention anything about the bonus pool?" },
67
+ { "raw": "im gonna grab lunch at sweetgreen want anything", "clean": "I'm gonna grab lunch at Sweetgreen. Want anything?" },
68
+ { "raw": "uh you know what just send it to me whenever its not urgent", "clean": "You know what, just send it to me whenever. It's not urgent." },
69
+ { "raw": "okay um so the the demo went really well i think they want to move forward but they have like a million questions about security and compliance so we need to loop in legal asap", "clean": "Okay so the demo went really well. I think they want to move forward, but they have like a million questions about security and compliance, so we need to loop in legal ASAP." },
70
+ { "raw": "your gonna love this video chen sent me", "clean": "You're gonna love this video Chen sent me." },
71
+ { "raw": "yup sounds good", "clean": "Yup, sounds good." },
72
+ { "raw": "do you know if diego is back from pto yet", "clean": "Do you know if Diego is back from PTO yet?" },
73
+ { "raw": "running to a meeting will ping you after", "clean": "Running to a meeting, will ping you after." },
74
+ { "raw": "uh quick one whats the wifi password at the new office", "clean": "Quick one, what's the wifi password at the new office?" },
75
+ { "raw": "so um i was thinking maybe we just do a small team dinner instead of the whole offsite thing this quarter budget is tight", "clean": "So I was thinking maybe we just do a small team dinner instead of the whole offsite thing this quarter. Budget is tight." },
76
+ { "raw": "hey hey hey congrats on the promo super well deserved", "clean": "Hey, congrats on the promo. Super well deserved." },
77
+ { "raw": "wait did you say tuesday or thursday", "clean": "Wait, did you say Tuesday or Thursday?" },
78
+ { "raw": "ok cool im heading out now ttyl", "clean": "Ok cool, I'm heading out now. TTYL." },
79
+ { "raw": "the airpods you left at my place are on my desk grab em whenever", "clean": "The AirPods you left at my place are on my desk. Grab them whenever." },
80
+ { "raw": "did anyone else get the email from it about the laptop refresh", "clean": "Did anyone else get the email from IT about the laptop refresh?" },
81
+ { "raw": "uh so basically the the deploy failed because of a missing env var and i fixed it but we should probably add a check to ci so this doesnt happen again", "clean": "So basically the deploy failed because of a missing env var. I fixed it, but we should probably add a check to CI so this doesn't happen again." },
82
+ { "raw": "kk see you in a bit", "clean": "Kk, see you in a bit." }
83
+ ]
data/seed/batches/long_form_thoughts.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [{"raw": "so ive been mulling over the pricing change and honestly i think we we moved too fast on the annual discount like the data showed a bump in conversions but um when you look at retention three months out the cohort actually churns harder than the monthly folks so we might be optimizing for a vanity metric instead of long term value", "clean": "I've been mulling over the pricing change, and honestly I think we moved too fast on the annual discount. The data showed a bump in conversions, but when you look at retention three months out, the cohort actually churns harder than the monthly folks. We might be optimizing for a vanity metric instead of long-term value."}, {"raw": "ok so the more i think about the the migration to postgres the more i think we should just bite the bullet and do it in q3 because every month we delay we add more mysql specific code and the cost of the cutover goes up and uh the team is already stretched thin so waiting till q4 just means doing it during the holiday freeze which is worse", "clean": "The more I think about the migration to Postgres, the more I think we should just bite the bullet and do it in Q3. Every month we delay, we add more MySQL-specific code and the cost of the cutover goes up. The team is already stretched thin, so waiting until Q4 just means doing it during the holiday freeze, which is worse."}, {"raw": "i mean i i keep going back and forth on whether we should kill the desktop app but every time i look at the numbers like seven percent of weekly active users are on desktop only and that segment has the highest ltv by far so even though its a pain to maintain killing it would basically hand our most profitable users to a competitor", "clean": "I keep going back and forth on whether we should kill the desktop app, but every time I look at the numbers, 7% of weekly active users are on desktop only, and that segment has the highest LTV by far. Even though it's a pain to maintain, killing it would basically hand our most profitable users to a competitor."}, {"raw": "thinking about the retro from yesterday the the biggest takeaway for me was that we shipped fast but skipped the design review on three of the four flows and now were paying for it in support tickets so um maybe the lesson isnt slow down its more like build in a thirty minute design check before any flow goes to prod even if its just a slack thread with screenshots", "clean": "Thinking about the retro from yesterday, the biggest takeaway for me was that we shipped fast but skipped the design review on three of the four flows, and now we're paying for it in support tickets. Maybe the lesson isn't slow down. It's more like build in a 30 minute design check before any flow goes to prod, even if it's just a Slack thread with screenshots."}, {"raw": "honestly the more i look at our pipeline the more i think the the issue isnt lead volume its that we have no way to tell sales which leads are warm versus cold so they spend half their time on people who were just kicking the tires and miss the ones who actually wanted to buy this week", "clean": "Honestly, the more I look at our pipeline, the more I think the issue isn't lead volume. It's that we have no way to tell sales which leads are warm versus cold, so they spend half their time on people who were just kicking the tires and miss the ones who actually wanted to buy this week."}, {"raw": "um so the conversation with sarah this morning really shifted my thinking on the api versioning thing because i had been assuming customers wanted stability above all else but she pointed out that for our enterprise contracts they actually want predictable change so semver with a clear deprecation window is way better than freezing v one forever", "clean": "The conversation with Sarah this morning really shifted my thinking on the API versioning thing. I had been assuming customers wanted stability above all else, but she pointed out that for our enterprise contracts they actually want predictable change. SemVer with a clear deprecation window is way better than freezing v1 forever."}, {"raw": "ive been planning out the q3 roadmap and i think the three big bets should be um one rewriting the notification service because its the source of like forty percent of our incidents two finally shipping sso for the team plan because every enterprise call hits that wall and three investing in the docs site because our trial to paid conversion is killing us on the self serve side", "clean": "I've been planning out the Q3 roadmap, and I think the three big bets should be: one, rewriting the notification service because it's the source of like 40% of our incidents; two, finally shipping SSO for the team plan because every enterprise call hits that wall; and three, investing in the docs site because our trial-to-paid conversion is killing us on the self-serve side."}, {"raw": "look the the more i sit with the figma feedback the more i think we tried to do too much in one screen like the dashboard has charts filters export and the new ai summary all fighting for attention so maybe we split it into two views one for the at a glance numbers and one for the deeper analysis and let users pick", "clean": "The more I sit with the Figma feedback, the more I think we tried to do too much in one screen. The dashboard has charts, filters, export, and the new AI summary all fighting for attention. Maybe we split it into two views, one for the at-a-glance numbers and one for the deeper analysis, and let users pick."}, {"raw": "reflecting on the last sprint i think the biggest mistake was scoping the auth refactor as a one week task because um nobody on the team had actually touched the legacy session code in two years so we hit landmines every day and ended up extending it twice", "clean": "Reflecting on the last sprint, I think the biggest mistake was scoping the auth refactor as a one-week task. Nobody on the team had actually touched the legacy session code in two years, so we hit landmines every day and ended up extending it twice."}, {"raw": "so the thing that keeps nagging me about the new pricing page is that um we lead with the enterprise tier first and i get the strategic reason but psychologically i think it makes the starter plan feel like a downgrade instead of a starting point so what if we reversed the order or made it a more horizontal layout", "clean": "The thing that keeps nagging me about the new pricing page is that we lead with the enterprise tier first. I get the strategic reason, but psychologically I think it makes the starter plan feel like a downgrade instead of a starting point. What if we reversed the order or made it a more horizontal layout?"}, {"raw": "ok brainstorming here so the new feature could either be a standalone tool you launch from the sidebar or um an inline panel that opens when you hover over a row and i think the inline version is more discoverable but the standalone gives us room to add more functionality later so theres a real tradeoff between adoption now and ceiling later", "clean": "Brainstorming here. The new feature could either be a standalone tool you launch from the sidebar or an inline panel that opens when you hover over a row. I think the inline version is more discoverable, but the standalone gives us room to add more functionality later. There's a real tradeoff between adoption now and ceiling later."}, {"raw": "i i think the meeting with vendor today went better than expected like they actually agreed to the sla terms we asked for which i did not see coming but um the pricing is still way above budget so we either need to negotiate harder or look at the open source alternative even though that means more ops work for us", "clean": "I think the meeting with the vendor today went better than expected. They actually agreed to the SLA terms we asked for, which I did not see coming. The pricing is still way above budget, so we either need to negotiate harder or look at the open source alternative, even though that means more ops work for us."}, {"raw": "ive been turning the the onboarding redesign over in my head and i keep landing on the same conclusion which is that the activation step needs to happen before the account creation not after because right now we lose like sixty percent of signups between email verification and first action", "clean": "I've been turning the onboarding redesign over in my head, and I keep landing on the same conclusion: the activation step needs to happen before the account creation, not after. Right now we lose like 60% of signups between email verification and first action."}, {"raw": "you know the more i talk to customers about the integration the more i realize they dont actually want a slack bot they want notifications that route to whichever tool theyre already in so building a slack integration first might actually be a mistake versus building a generic webhook layer and then bolting slack on top", "clean": "The more I talk to customers about the integration, the more I realize they don't actually want a Slack bot. They want notifications that route to whichever tool they're already in. Building a Slack integration first might actually be a mistake versus building a generic webhook layer and then bolting Slack on top."}, {"raw": "honestly um the more i think about the conference talk proposal the more i feel like the postgres scaling angle is way more interesting than the kubernetes story because everyone is doing kubernetes talks but very few people have actually walked through a multi tenant postgres setup with row level security at our scale", "clean": "Honestly, the more I think about the conference talk proposal, the more I feel like the Postgres scaling angle is way more interesting than the Kubernetes story. Everyone is doing Kubernetes talks, but very few people have actually walked through a multi-tenant Postgres setup with row-level security at our scale."}, {"raw": "the weird thing about the the launch is that we hit our signup target but our activation rate dropped by like fifteen points which tells me we attracted the wrong audience and the email campaign probably overpromised on what the free tier can actually do so we ended up with curious people instead of motivated buyers", "clean": "The weird thing about the launch is that we hit our signup target, but our activation rate dropped by like 15 points. That tells me we attracted the wrong audience, and the email campaign probably overpromised on what the free tier can actually do. We ended up with curious people instead of motivated buyers."}, {"raw": "i mean if i had to summarize what i learned from the last quarter um its basically that velocity is a lagging indicator of clarity like the weeks where we shipped the most were always the weeks where we had a one page brief and a clear owner and the slow weeks were always the ones where we were still arguing about what we were even building", "clean": "If I had to summarize what I learned from the last quarter, it's basically that velocity is a lagging indicator of clarity. The weeks where we shipped the most were always the weeks where we had a one-page brief and a clear owner. The slow weeks were always the ones where we were still arguing about what we were even building."}, {"raw": "so reviewing the the architecture doc that david sent over and i think the core proposal is solid but um he glossed over the migration path completely and at our scale you cant just cut over to a new event bus on a friday night so we need at least a chapter on dual writes and shadow reads before this is ready", "clean": "Reviewing the architecture doc that David sent over, I think the core proposal is solid. He glossed over the migration path completely, and at our scale you can't just cut over to a new event bus on a Friday night. We need at least a chapter on dual writes and shadow reads before this is ready."}, {"raw": "okay weighing the the buy versus build question for the analytics layer i think we go buy because um the team doesnt have the data engineering depth to build it well and even if we did building it would pull two engineers off the core product for at least a quarter which is way more expensive than just paying for amplitude", "clean": "Weighing the buy versus build question for the analytics layer, I think we go buy. The team doesn't have the data engineering depth to build it well, and even if we did, building it would pull two engineers off the core product for at least a quarter, which is way more expensive than just paying for Amplitude."}, {"raw": "ive been thinking about why the the demo went sideways yesterday and honestly i think the issue was that we were demoing features instead of telling a story like the prospect didnt care about our filtering options they cared about whether their team could find the right campaign on a tuesday morning before standup", "clean": "I've been thinking about why the demo went sideways yesterday, and honestly I think the issue was that we were demoing features instead of telling a story. The prospect didn't care about our filtering options. They cared about whether their team could find the right campaign on a Tuesday morning before standup."}, {"raw": "um so the the planning doc for next quarter is sitting at like fifteen items and theres no way were going to ship all of that so i think we need to pick the three things that move the north star metric and ruthlessly defer everything else even if it means saying no to features that engineering is excited about", "clean": "The planning doc for next quarter is sitting at like 15 items, and there's no way we're going to ship all of that. I think we need to pick the three things that move the north star metric and ruthlessly defer everything else, even if it means saying no to features that engineering is excited about."}, {"raw": "i keep coming back to the thought that we we underinvested in observability last year and now were paying for it because every incident takes twice as long to diagnose as it should and the on call rotation is burning people out so even if it feels boring spending q3 on metrics tracing and runbooks would probably be the highest leverage thing we do", "clean": "I keep coming back to the thought that we underinvested in observability last year, and now we're paying for it. Every incident takes twice as long to diagnose as it should, and the on-call rotation is burning people out. Even if it feels boring, spending Q3 on metrics, tracing, and runbooks would probably be the highest leverage thing we do."}, {"raw": "reflecting on the meeting with jenna i think the the core disagreement isnt really about timeline its about scope she wants to ship a focused mvp in six weeks and i want to ship a more complete v one in ten weeks and um neither is wrong but we need to actually name the tradeoff instead of arguing about dates", "clean": "Reflecting on the meeting with Jenna, I think the core disagreement isn't really about timeline. It's about scope. She wants to ship a focused MVP in six weeks, and I want to ship a more complete v1 in ten weeks. Neither is wrong, but we need to actually name the tradeoff instead of arguing about dates."}, {"raw": "the more i think about it the more im convinced our marketing site is the bottleneck like we keep blaming sales for low conversion but the bounce rate on the pricing page is sixty percent which means most people decide we arent for them before they ever talk to a human so fixing the site is probably higher leverage than any sales training", "clean": "The more I think about it, the more I'm convinced our marketing site is the bottleneck. We keep blaming sales for low conversion, but the bounce rate on the pricing page is 60%, which means most people decide we aren't for them before they ever talk to a human. Fixing the site is probably higher leverage than any sales training."}, {"raw": "so um my honest take on the postmortem is that nobody did anything wrong it was a system failure not a person failure like we had no staging environment that mirrored prod we had no automated rollback and the alerting was tied to a single slack channel that was muted overnight so of course it took three hours to catch", "clean": "My honest take on the postmortem is that nobody did anything wrong. It was a system failure, not a person failure. We had no staging environment that mirrored prod, we had no automated rollback, and the alerting was tied to a single Slack channel that was muted overnight. Of course it took three hours to catch."}, {"raw": "ive been chewing on the the customer interview from monday and the thing that stuck out was when she said she wished she could share a dashboard without sharing her whole account which is a pretty clear signal that we need view only links and we should probably prioritize that over the export feature we had on deck", "clean": "I've been chewing on the customer interview from Monday, and the thing that stuck out was when she said she wished she could share a dashboard without sharing her whole account. That's a pretty clear signal that we need view-only links, and we should probably prioritize that over the export feature we had on deck."}, {"raw": "um okay so retrospective on the hiring process for the senior role i think we we moved too slow on the top two candidates and lost both of them to faster companies so going forward we need to commit to same week decisions for anyone who clears the technical bar even if it means deferring other interviews", "clean": "Retrospective on the hiring process for the senior role. I think we moved too slow on the top two candidates and lost both of them to faster companies. Going forward, we need to commit to same-week decisions for anyone who clears the technical bar, even if it means deferring other interviews."}, {"raw": "the question im sitting with after the board meeting is whether we should raise now at a flat round or wait six months and try to grow into a step up because um the market is choppy and the cost of waiting if growth stalls is way worse than the dilution from doing it now", "clean": "The question I'm sitting with after the board meeting is whether we should raise now at a flat round or wait six months and try to grow into a step-up. The market is choppy, and the cost of waiting if growth stalls is way worse than the dilution from doing it now."}, {"raw": "honestly when i look at the the org chart i think the team structure is part of why were slow because we have a backend pod a frontend pod and a platform pod which means every feature has to coordinate across three teams instead of being owned by one cross functional pod that ships the whole thing", "clean": "Honestly, when I look at the org chart, I think the team structure is part of why we're slow. We have a backend pod, a frontend pod, and a platform pod, which means every feature has to coordinate across three teams instead of being owned by one cross-functional pod that ships the whole thing."}, {"raw": "so brainstorm for the the marketing campaign i was thinking what if we did a series of case studies but instead of the usual logo and quote format we record actual video walkthroughs with the customer showing their workflow because um seeing how a real team uses the product is way more persuasive than a polished testimonial", "clean": "Brainstorm for the marketing campaign. What if we did a series of case studies, but instead of the usual logo-and-quote format we record actual video walkthroughs with the customer showing their workflow? Seeing how a real team uses the product is way more persuasive than a polished testimonial."}, {"raw": "i think the thing we got wrong about the the beta program is that we treated it like a marketing event instead of a research function so we onboarded fifty companies and then never followed up with them in a structured way which means we got the signups but lost the actual insight we were trying to collect", "clean": "I think the thing we got wrong about the beta program is that we treated it like a marketing event instead of a research function. We onboarded 50 companies and then never followed up with them in a structured way, which means we got the signups but lost the actual insight we were trying to collect."}, {"raw": "um okay so honestly looking at the the codebase the biggest risk isnt any individual piece of tech debt its that we have like four different patterns for the same problem so a new engineer has to learn which one to use in which context and there's no documentation for any of it", "clean": "Honestly, looking at the codebase, the biggest risk isn't any individual piece of tech debt. It's that we have like four different patterns for the same problem, so a new engineer has to learn which one to use in which context, and there's no documentation for any of it."}, {"raw": "the the thing i keep telling myself about the the redesign is that we cant ship it incrementally because half new half old looks broken to users so either we commit to a flag day cutover or we keep iterating on the current design until were ready to do it properly", "clean": "The thing I keep telling myself about the redesign is that we can't ship it incrementally because half new, half old looks broken to users. Either we commit to a flag-day cutover, or we keep iterating on the current design until we're ready to do it properly."}, {"raw": "so im looking at the the support ticket volume from last month and like sixty percent of them are about the same three issues which means we have a really clear list of what to fix but um the engineering team keeps prioritizing new features so were stuck in a loop where we ship features that generate more tickets we never fix", "clean": "I'm looking at the support ticket volume from last month, and like 60% of them are about the same three issues. We have a really clear list of what to fix, but the engineering team keeps prioritizing new features. We're stuck in a loop where we ship features that generate more tickets we never fix."}, {"raw": "thinking out loud here but um what if the the next hire isnt another senior engineer what if its a developer relations person because we keep getting feedback that our docs are confusing and our examples are out of date and a devrel hire could fix both of those without pulling engineering off the roadmap", "clean": "Thinking out loud here, but what if the next hire isn't another senior engineer? What if it's a developer relations person? We keep getting feedback that our docs are confusing and our examples are out of date, and a DevRel hire could fix both of those without pulling engineering off the roadmap."}, {"raw": "i mean honestly looking back at the the launch retrospective the the thing that surprised me most was how much of the qualitative feedback was positive even though the metrics were mixed which makes me think we shipped something people actually want we just havent figured out how to communicate the value yet", "clean": "Honestly, looking back at the launch retrospective, the thing that surprised me most was how much of the qualitative feedback was positive even though the metrics were mixed. It makes me think we shipped something people actually want; we just haven't figured out how to communicate the value yet."}, {"raw": "so um my read on the the partnership conversation is that they want exclusivity in their vertical but only for two years which is actually a reasonable ask because by year three our position in the market will be different enough that exclusivity wont matter so i think we should counter with a yes plus a higher rev share", "clean": "My read on the partnership conversation is that they want exclusivity in their vertical, but only for two years. That's actually a reasonable ask because by year three our position in the market will be different enough that exclusivity won't matter. I think we should counter with a yes plus a higher rev share."}, {"raw": "okay so the the question for the offsite is do we double down on smb or push harder into mid market because we cant credibly do both with the team we have and the data says smb is easier to acquire but mid market has way better unit economics so its really a question of what kind of company we want to be in two years", "clean": "The question for the offsite is: do we double down on SMB or push harder into mid-market? We can't credibly do both with the team we have. The data says SMB is easier to acquire, but mid-market has way better unit economics. It's really a question of what kind of company we want to be in two years."}, {"raw": "ive been thinking about the the way we run sprint planning and i think the the format is the problem because we spend forty five minutes estimating tickets in a group setting which is a terrible use of senior engineer time so maybe we move to async estimation and use the meeting for actual discussion of tradeoffs", "clean": "I've been thinking about the way we run sprint planning, and I think the format is the problem. We spend 45 minutes estimating tickets in a group setting, which is a terrible use of senior engineer time. Maybe we move to async estimation and use the meeting for actual discussion of tradeoffs."}, {"raw": "um my take on the the q two performance is that we hit our revenue number but missed on every leading indicator like trial signups churn and nps and that pattern means q three is going to be harder than q two so we cant just keep doing the same thing and expect the same result", "clean": "My take on the Q2 performance is that we hit our revenue number but missed on every leading indicator: trial signups, churn, and NPS. That pattern means Q3 is going to be harder than Q2, so we can't just keep doing the same thing and expect the same result."}, {"raw": "so the the broader strategic question is whether were a horizontal tool or a vertical solution because right now were trying to be both and i think thats why our messaging feels muddy and our sales cycles are longer than they should be we need to pick a lane even if it means saying no to half our pipeline", "clean": "The broader strategic question is whether we're a horizontal tool or a vertical solution. Right now we're trying to be both, and I think that's why our messaging feels muddy and our sales cycles are longer than they should be. We need to pick a lane, even if it means saying no to half our pipeline."}, {"raw": "i was uh thinking about how the the ai feature is performing and honestly the the usage numbers are good but the satisfaction scores are mixed which tells me people are trying it but not getting consistent value so before we promote it more heavily we need to figure out which prompts work reliably and which dont", "clean": "I was thinking about how the AI feature is performing, and honestly the usage numbers are good, but the satisfaction scores are mixed. That tells me people are trying it but not getting consistent value. Before we promote it more heavily, we need to figure out which prompts work reliably and which don't."}, {"raw": "so the the part of the the customer call that stuck with me was when he said your product saves us time but its hard to explain to my boss why we pay for it and that is um basically the entire challenge of selling productivity tools in one sentence we need better artifacts that prove the roi", "clean": "The part of the customer call that stuck with me was when he said, your product saves us time, but it's hard to explain to my boss why we pay for it. That is basically the entire challenge of selling productivity tools in one sentence. We need better artifacts that prove the ROI."}, {"raw": "ok so reflecting on the the engineering all hands i think the the team is actually really aligned on what we should build they just dont feel like they have time to build any of it because they're spending most of their time on incident response and customer escalations so the real problem is operational not strategic", "clean": "Reflecting on the engineering all-hands, I think the team is actually really aligned on what we should build. They just don't feel like they have time to build any of it because they're spending most of their time on incident response and customer escalations. The real problem is operational, not strategic."}, {"raw": "the the more i think about it the more i think we should kill the free tier because um the conversion rate from free to paid has been declining for six months and the support cost per free user is actually higher than the support cost per paid user so were subsidizing the wrong people", "clean": "The more I think about it, the more I think we should kill the free tier. The conversion rate from free to paid has been declining for six months, and the support cost per free user is actually higher than the support cost per paid user. We're subsidizing the wrong people."}, {"raw": "so um brainstorming on the the developer experience i think the the biggest win would be a true local dev environment because right now every new hire spends like three days getting set up and senior engineers still hit weird state issues every other week so investing in dev infra would pay back many times over", "clean": "Brainstorming on the developer experience, I think the biggest win would be a true local dev environment. Right now every new hire spends like three days getting set up, and senior engineers still hit weird state issues every other week. Investing in dev infra would pay back many times over."}, {"raw": "i ive been thinking about the the way we onboard new managers and honestly we dont we just promote people and then expect them to figure it out which is why half of our first time managers either burn out or leave within a year so we need an actual program even if its lightweight", "clean": "I've been thinking about the way we onboard new managers, and honestly, we don't. We just promote people and then expect them to figure it out, which is why half of our first-time managers either burn out or leave within a year. We need an actual program, even if it's lightweight."}, {"raw": "ok so um my prediction for the next twelve months is that the the the companies that win are not the ones with the best model theyre the ones with the best workflow integration because the model gap is closing fast but the workflow gap is where actual productivity gains live and most of our competitors arent there yet", "clean": "My prediction for the next twelve months is that the companies that win are not the ones with the best model. They're the ones with the best workflow integration. The model gap is closing fast, but the workflow gap is where actual productivity gains live, and most of our competitors aren't there yet."}, {"raw": "honestly the the thing i learned from the the failed experiment is that you cant fake urgency like we tried to manufacture a deadline to force the team to ship faster and all we did was burn trust because the team knew it was arbitrary so next time we set deadlines they need to be tied to something real", "clean": "Honestly, the thing I learned from the failed experiment is that you can't fake urgency. We tried to manufacture a deadline to force the team to ship faster, and all we did was burn trust because the team knew it was arbitrary. Next time we set deadlines, they need to be tied to something real."}, {"raw": "so the the more i sit with the question of whether to expand to europe in q four the more i think the the answer is not yet because we havent even nailed product market fit in our home market and adding a second geography means splitting an already stretched team across two time zones with different compliance requirements", "clean": "The more I sit with the question of whether to expand to Europe in Q4, the more I think the answer is not yet. We haven't even nailed product-market fit in our home market, and adding a second geography means splitting an already stretched team across two time zones with different compliance requirements."}, {"raw": "um so reviewing the the design review notes from yesterday the the recurring theme is that we keep introducing new ui patterns instead of reusing existing ones which means our design system is fragmenting and every screen looks slightly different so we need a design system audit and a freeze on new patterns until we reconcile", "clean": "Reviewing the design review notes from yesterday, the recurring theme is that we keep introducing new UI patterns instead of reusing existing ones. Our design system is fragmenting and every screen looks slightly different. We need a design system audit and a freeze on new patterns until we reconcile."}, {"raw": "i think the the core insight from the the user research is that people dont think about our product in terms of features they think about it in terms of moments like the moment before a client meeting or the moment after a campaign ends and our roadmap should reflect those moments not our internal feature taxonomy", "clean": "I think the core insight from the user research is that people don't think about our product in terms of features. They think about it in terms of moments, like the moment before a client meeting or the moment after a campaign ends. Our roadmap should reflect those moments, not our internal feature taxonomy."}, {"raw": "so um if i had to make a call right now id say we ship the v one without the the collaboration features because the the data shows people are using us solo for the first thirty days anyway and adding collab on day one slows the launch by two months for a feature most users wont touch until later", "clean": "If I had to make a call right now, I'd say we ship the v1 without the collaboration features. The data shows people are using us solo for the first 30 days anyway, and adding collab on day one slows the launch by two months for a feature most users won't touch until later."}, {"raw": "ive been mulling on the the question of whether to write the the blog post about our outage and i keep landing on yes because the the alternative is letting customers piece together what happened from twitter and i think the the transparency builds way more trust than the embarrassment costs", "clean": "I've been mulling on the question of whether to write the blog post about our outage, and I keep landing on yes. The alternative is letting customers piece together what happened from Twitter, and I think the transparency builds way more trust than the embarrassment costs."}, {"raw": "the the more i look at the conversion funnel the more i think the issue is the signup flow itself because we ask for company size and use case before the user has any idea what the product does so they bounce and the the only way to fix this is to delay all qualifying questions until after activation", "clean": "The more I look at the conversion funnel, the more I think the issue is the signup flow itself. We ask for company size and use case before the user has any idea what the product does, so they bounce. The only way to fix this is to delay all qualifying questions until after activation."}, {"raw": "honestly um the the takeaway from the the offsite for me was that we have way more alignment on the the long term vision than i thought and way less alignment on the the short term priorities than i realized which means our problem isnt strategy its execution focus", "clean": "Honestly, the takeaway from the offsite for me was that we have way more alignment on the long-term vision than I thought, and way less alignment on the short-term priorities than I realized. Our problem isn't strategy. It's execution focus."}, {"raw": "ok so brainstorm what if we treated our changelog as a marketing channel like instead of bullet points hidden in a footer we made each release a short video walkthrough of what shipped and why and we posted it to twitter and linkedin because every release is a chance to remind people were moving fast", "clean": "Brainstorm: what if we treated our changelog as a marketing channel? Instead of bullet points hidden in a footer, we made each release a short video walkthrough of what shipped and why, and we posted it to Twitter and LinkedIn. Every release is a chance to remind people we're moving fast."}, {"raw": "i mean the the part of the the planning conversation that gave me pause was when ryan said we should defer security work until after the launch because um deferring security is how you end up with a breach and a breach is way more expensive than just doing the work now", "clean": "The part of the planning conversation that gave me pause was when Ryan said we should defer security work until after the launch. Deferring security is how you end up with a breach, and a breach is way more expensive than just doing the work now."}, {"raw": "so reflecting on the the candidate i interviewed yesterday i think he was technically strong but uh his answers about collaboration felt rehearsed in a way that made me think he doesnt actually enjoy working with others which is a real problem for a senior role where mentoring is half the job", "clean": "Reflecting on the candidate I interviewed yesterday, I think he was technically strong, but his answers about collaboration felt rehearsed in a way that made me think he doesn't actually enjoy working with others. That's a real problem for a senior role where mentoring is half the job."}, {"raw": "um the the more i look at the the competitor's pricing page the more i think they figured something out we didnt which is that their lowest tier is priced at impulse buy levels so people sign up without thinking and then upgrade later whereas our cheapest tier is priced like a real decision so people deliberate and never come back", "clean": "The more I look at the competitor's pricing page, the more I think they figured something out we didn't. Their lowest tier is priced at impulse-buy levels, so people sign up without thinking and then upgrade later. Our cheapest tier is priced like a real decision, so people deliberate and never come back."}, {"raw": "so the the question im wrestling with for the roadmap is whether to invest in mobile or invest in api because mobile is what users keep asking for but api is what would unlock our biggest enterprise deals and we cant credibly do both well in the same quarter", "clean": "The question I'm wrestling with for the roadmap is whether to invest in mobile or invest in API. Mobile is what users keep asking for, but API is what would unlock our biggest enterprise deals, and we can't credibly do both well in the same quarter."}, {"raw": "i i keep thinking about what james said about technical debt being a forcing function for refactors and i think hes basically right because we never refactor when we have time we only refactor when the the current code becomes impossible to extend so debt isnt always bad it can actually accelerate the right decisions", "clean": "I keep thinking about what James said about technical debt being a forcing function for refactors, and I think he's basically right. We never refactor when we have time. We only refactor when the current code becomes impossible to extend. Debt isn't always bad. It can actually accelerate the right decisions."}, {"raw": "um so my honest assessment of where we are right now is that the the product is good the marketing is improving and the team is the strongest its ever been but our sales motion is broken and until we fix the sales motion none of the other improvements will compound the way they should", "clean": "My honest assessment of where we are right now is that the product is good, the marketing is improving, and the team is the strongest it's ever been. But our sales motion is broken, and until we fix the sales motion, none of the other improvements will compound the way they should."}, {"raw": "so um after the the demo with the the customer this morning i think the the lesson is that we should always lead with a live data demo even if it means showing some rough edges because the the canned demo feels too polished to be real and prospects can sense that", "clean": "After the demo with the customer this morning, I think the lesson is that we should always lead with a live data demo, even if it means showing some rough edges. The canned demo feels too polished to be real, and prospects can sense that."}, {"raw": "i mean the the more i think about the the trust and safety policy the more i think we we need to publish it publicly because right now its an internal doc and customers keep asking for it so making it public would save us time on every enterprise sales cycle and signal that we take it seriously", "clean": "The more I think about the trust and safety policy, the more I think we need to publish it publicly. Right now it's an internal doc, and customers keep asking for it. Making it public would save us time on every enterprise sales cycle and signal that we take it seriously."}, {"raw": "honestly um reflecting on the the last six months i think the the most important thing we did wasnt any single feature it was instituting weekly customer calls because the the calls changed how the whole team thinks about prioritization in a way no roadmap exercise ever could", "clean": "Honestly, reflecting on the last six months, I think the most important thing we did wasn't any single feature. It was instituting weekly customer calls. The calls changed how the whole team thinks about prioritization in a way no roadmap exercise ever could."}, {"raw": "so brainstorming the the gtm for the new tier um i think we should announce it to existing customers two weeks before we make it public because the the upgrade revenue from the current base will likely exceed new acquisition for the first month and giving them a heads up makes them feel like insiders", "clean": "Brainstorming the GTM for the new tier, I think we should announce it to existing customers two weeks before we make it public. The upgrade revenue from the current base will likely exceed new acquisition for the first month, and giving them a heads up makes them feel like insiders."}, {"raw": "you know the the thing about the the ml feature roadmap is that we keep planning it like a traditional product roadmap but ml features have way more uncertainty so the the timeline estimates are basically fiction and we should be planning in terms of research milestones not feature ships", "clean": "The thing about the ML feature roadmap is that we keep planning it like a traditional product roadmap, but ML features have way more uncertainty. The timeline estimates are basically fiction, and we should be planning in terms of research milestones, not feature ships."}, {"raw": "the the more i look at the the activation funnel the more i think the the issue isnt friction its motivation like users get through the steps fine but they dont have a strong enough reason to come back on day two and thats not a ux problem its a value prop problem we need to solve in the first session", "clean": "The more I look at the activation funnel, the more I think the issue isn't friction. It's motivation. Users get through the steps fine, but they don't have a strong enough reason to come back on day two. That's not a UX problem. It's a value prop problem we need to solve in the first session."}, {"raw": "honestly the the gym thing has been a turning point for me because um i used to treat it like an optional checkbox but once i started scheduling lifts at six am the rest of my day just falls into place and i sleep better and i think clearer at work so it stopped being about fitness and became about basically protecting my decision making capacity", "clean": "Honestly, the gym thing has been a turning point for me. I used to treat it like an optional checkbox, but once I started scheduling lifts at 6am, the rest of my day just falls into place, and I sleep better, and I think clearer at work. It stopped being about fitness and became about basically protecting my decision-making capacity."}, {"raw": "reflecting on the the promotion cycle this half i think we we got the calibration wrong because um three of the people we held back were doing senior work on smaller surface area and we kept rewarding people on big teams which basically tells the org that scope matters more than judgment and thats not the message we want to send", "clean": "Reflecting on the promotion cycle this half, I think we got the calibration wrong. Three of the people we held back were doing senior work on smaller surface area, and we kept rewarding people on big teams, which basically tells the org that scope matters more than judgment. That's not the message we want to send."}, {"raw": "looking back at the the build versus buy decision on the the queue we made last year i think we made the right call buying it but um for the wrong reasons because we said it was about speed and it was really about not wanting to own the operational complexity which is fine but we should be honest with ourselves about why", "clean": "Looking back at the build versus buy decision on the queue we made last year, I think we made the right call buying it, but for the wrong reasons. We said it was about speed, and it was really about not wanting to own the operational complexity, which is fine, but we should be honest with ourselves about why."}, {"raw": "the thing that bugs me about our okr process is that we we set them in january then never look at them again until december so they become this performative document instead of a living tool and um if a goal isnt influencing weekly decisions its not really a goal its just a wish we wrote down", "clean": "The thing that bugs me about our OKR process is that we set them in January then never look at them again until December, so they become this performative document instead of a living tool. If a goal isn't influencing weekly decisions, it's not really a goal. It's just a wish we wrote down."}, {"raw": "im starting to wonder if the the move to micro services was actually a regression for us because uh the team spent two quarters on infra work to enable it and now every feature crosses three repos and a deploy pipeline so we ship slower than we did when it was one ugly monolith", "clean": "I'm starting to wonder if the move to microservices was actually a regression for us. The team spent two quarters on infra work to enable it, and now every feature crosses three repos and a deploy pipeline, so we ship slower than we did when it was one ugly monolith."}, {"raw": "between you and me i dont think the the new vp is going to work out because um in the first thirty days he hasnt asked a single question about the customer he keeps talking about process and frameworks and thats a tell in any operating role that you care more about how things look than how they work", "clean": "Between you and me, I don't think the new VP is going to work out. In the first 30 days, he hasn't asked a single question about the customer. He keeps talking about process and frameworks, and that's a tell in any operating role that you care more about how things look than how they work."}, {"raw": "playing devils advocate for a second on the the kill the free tier idea um the the free tier might be losing money directly but its our largest source of word of mouth so even if the unit economics look bad on a spreadsheet killing it could collapse top of funnel in a way no paid acquisition channel can replace", "clean": "Playing devil's advocate for a second on the kill-the-free-tier idea: the free tier might be losing money directly, but it's our largest source of word of mouth. Even if the unit economics look bad on a spreadsheet, killing it could collapse top of funnel in a way no paid acquisition channel can replace."}, {"raw": "if im being completely honest the the real reason i didnt push back on the the launch date wasnt that i agreed with it it was that i was tired of the argument and um thats a leadership failure on my part because picking peace over being right on something material is exactly the move you regret later", "clean": "If I'm being completely honest, the real reason I didn't push back on the launch date wasn't that I agreed with it. It was that I was tired of the argument, and that's a leadership failure on my part. Picking peace over being right on something material is exactly the move you regret later."}, {"raw": "what nobody seems to be saying is that the the activation drop we keep blaming on the onboarding flow probably started when we changed the the trial length from fourteen to seven days like the the timing lines up exactly and we never ran that as a clean ab test we just rolled it out", "clean": "What nobody seems to be saying is that the activation drop we keep blaming on the onboarding flow probably started when we changed the trial length from 14 to 7 days. The timing lines up exactly, and we never ran that as a clean A/B test. We just rolled it out."}, {"raw": "stepping back from the details for a second i think the the broader pattern this year is that we keep solving second order problems before weve really solved the first order ones like were optimizing dashboard load times when half of new users still cant figure out how to invite a teammate", "clean": "Stepping back from the details for a second, I think the broader pattern this year is that we keep solving second-order problems before we've really solved the first-order ones. We're optimizing dashboard load times when half of new users still can't figure out how to invite a teammate."}, {"raw": "the unpopular take here is that we we dont actually need a head of product right now we need a head of operations because um the bottleneck isnt vision its that we cant reliably execute on the vision we already have and adding more strategy on top of broken execution makes things worse not better", "clean": "The unpopular take here is that we don't actually need a head of product right now. We need a head of operations. The bottleneck isn't vision. It's that we can't reliably execute on the vision we already have, and adding more strategy on top of broken execution makes things worse, not better."}, {"raw": "im not sure if this is right but um i think the the reason our code review culture feels broken isnt the reviewers its the the prs themselves they're way too big like nobody can give a meaningful review on a fifteen hundred line diff so we get rubber stamps and then bugs in prod", "clean": "I'm not sure if this is right, but I think the reason our code review culture feels broken isn't the reviewers. It's the PRs themselves. They're way too big. Nobody can give a meaningful review on a 1500 line diff, so we get rubber stamps and then bugs in prod."}, {"raw": "from a pure metrics standpoint the the q one campaign was a failure but um qualitatively it changed how prospects describe us on calls like they used to call us a tool and now they call us a platform so the the brand effect might be doing more work than the the dashboard suggests", "clean": "From a pure metrics standpoint, the Q1 campaign was a failure, but qualitatively it changed how prospects describe us on calls. They used to call us a tool, and now they call us a platform. The brand effect might be doing more work than the dashboard suggests."}, {"raw": "lets be real about this the the the reason were missing the the q two forecast isnt the the market and isnt the the team its that we built the the forecast off two outlier deals from q four and pretended that was a trend so we baked optimism into the model and now were paying for it", "clean": "Let's be real about this. The reason we're missing the Q2 forecast isn't the market and isn't the team. It's that we built the forecast off two outlier deals from Q4 and pretended that was a trend. We baked optimism into the model, and now we're paying for it."}, {"raw": "the part that frustrates me is that we we keep running planning offsites and producing beautifully structured documents and then um three weeks in everyone is back to whatever they were going to do anyway so the the offsite was theater it didnt actually change behavior", "clean": "The part that frustrates me is that we keep running planning offsites and producing beautifully structured documents, and then three weeks in, everyone is back to whatever they were going to do anyway. The offsite was theater. It didn't actually change behavior."}, {"raw": "after the meeting with finance today im genuinely worried about the the runway math because um the the burn assumptions baked into the plan assume we hit fifty percent of pipeline and historically we hit thirty so the real runway is probably four months shorter than what's on the slide", "clean": "After the meeting with finance today, I'm genuinely worried about the runway math. The burn assumptions baked into the plan assume we hit 50% of pipeline, and historically we hit 30%. The real runway is probably four months shorter than what's on the slide."}, {"raw": "i keep coming back to the the question of whether we should have hired a designer earlier because um the the engineering team has been making design decisions by default for a year and you can see it in the inconsistency of the the ui its not their fault its just not their job", "clean": "I keep coming back to the question of whether we should have hired a designer earlier. The engineering team has been making design decisions by default for a year, and you can see it in the inconsistency of the UI. It's not their fault. It's just not their job."}, {"raw": "one thing i havent figured out is how to give the the new senior engineer the the autonomy he wants without losing visibility into what hes building because um the last two times i pulled back hard on reviews we ended up with surprises in prod that would have been caught in an earlier conversation", "clean": "One thing I haven't figured out is how to give the new senior engineer the autonomy he wants without losing visibility into what he's building. The last two times I pulled back hard on reviews, we ended up with surprises in prod that would have been caught in an earlier conversation."}, {"raw": "the contrarian view is that the the long sales cycles arent a sales problem theyre a product problem because um if the value were obvious people wouldnt need six weeks of demos and security reviews to commit so shortening cycles probably starts with making the the product easier to evaluate not training reps to close harder", "clean": "The contrarian view is that the long sales cycles aren't a sales problem. They're a product problem. If the value were obvious, people wouldn't need six weeks of demos and security reviews to commit. Shortening cycles probably starts with making the product easier to evaluate, not training reps to close harder."}, {"raw": "what i wish we had done differently is taken the the design partner program more seriously like we signed up eight design partners and then um treated them like regular customers with extra meetings instead of building actual feedback loops into the the product process so we got the the optics without the the learning", "clean": "What I wish we had done differently is taken the design partner program more seriously. We signed up eight design partners, and then treated them like regular customers with extra meetings, instead of building actual feedback loops into the product process. We got the optics without the learning."}, {"raw": "thinking out loud here um what if the the reason our nps dropped wasnt anything we did but just the the fact that we crossed some threshold of users where the the early evangelists got diluted by more pragmatic buyers who score lower by default and the the real underlying experience hasnt actually degraded", "clean": "Thinking out loud here, what if the reason our NPS dropped wasn't anything we did, but just the fact that we crossed some threshold of users where the early evangelists got diluted by more pragmatic buyers who score lower by default, and the real underlying experience hasn't actually degraded?"}, {"raw": "if i had to bet on what the the next twelve months looks like id say the the open source competitor takes more share than people expect at the the low end and that pushes us upmarket faster than we planned which is fine but we need to make sure our enterprise muscle is actually ready", "clean": "If I had to bet on what the next 12 months looks like, I'd say the open source competitor takes more share than people expect at the low end, and that pushes us upmarket faster than we planned. That's fine, but we need to make sure our enterprise muscle is actually ready."}, {"raw": "the awkward truth is that the the head of sales role weve been trying to fill for six months hasnt been filled because um we cant articulate what success looks like in the first ninety days so every candidate ends up asking the question and we give a different answer each time which they obviously notice", "clean": "The awkward truth is that the head of sales role we've been trying to fill for six months hasn't been filled because we can't articulate what success looks like in the first 90 days. Every candidate ends up asking the question, and we give a different answer each time, which they obviously notice."}, {"raw": "to be clear about my position um im not against the the rewrite im against scoping the the rewrite without first answering whether the the existing system actually has the issues we keep blaming it for because half the bugs people attribute to legacy code are actually in the new code we wrote on top of it", "clean": "To be clear about my position, I'm not against the rewrite. I'm against scoping the rewrite without first answering whether the existing system actually has the issues we keep blaming it for. Half the bugs people attribute to legacy code are actually in the new code we wrote on top of it."}, {"raw": "every time i look at the the cohort data the the same pattern jumps out which is that the the users who invite a teammate in week one have like four times the the retention of users who dont so um if we believe that pattern is causal then everything in onboarding should bend toward that single action", "clean": "Every time I look at the cohort data, the same pattern jumps out, which is that the users who invite a teammate in week one have like 4 times the retention of users who don't. If we believe that pattern is causal, then everything in onboarding should bend toward that single action."}, {"raw": "the framing that helped me was um treating roadmap planning as a portfolio not a list like you should have a couple of high confidence low ceiling bets a couple of medium confidence medium ceiling bets and one really speculative thing and right now our roadmap is just thirty medium confidence medium ceiling bets which is the worst of all worlds", "clean": "The framing that helped me was treating roadmap planning as a portfolio, not a list. You should have a couple of high confidence, low ceiling bets, a couple of medium confidence, medium ceiling bets, and one really speculative thing, and right now our roadmap is just 30 medium confidence, medium ceiling bets, which is the worst of all worlds."}, {"raw": "what surprises me is how much better the the engineering team has gotten at writing docs since we tied promo packets to documentation contributions like i thought that would feel transactional but um it actually changed the the underlying culture because the the act of writing things down became a normal part of finishing work", "clean": "What surprises me is how much better the engineering team has gotten at writing docs since we tied promo packets to documentation contributions. I thought that would feel transactional, but it actually changed the underlying culture, because the act of writing things down became a normal part of finishing work."}, {"raw": "from where i sit the the biggest risk to the the q three plan isnt any of the the items on the plan its that we have no buffer for the the inevitable customer escalation that pulls two engineers off the roadmap for three weeks and we keep planning as if that wont happen even though it has happened every quarter", "clean": "From where I sit, the biggest risk to the Q3 plan isn't any of the items on the plan. It's that we have no buffer for the inevitable customer escalation that pulls two engineers off the roadmap for three weeks, and we keep planning as if that won't happen, even though it has happened every quarter."}, {"raw": "i think we underestimate how much of the the founders early customer relationships are still carrying the the company because um when i look at our top twenty accounts twelve of them came from a direct conversation with a founder and replacing that channel with a real sales motion is way harder than we acknowledge", "clean": "I think we underestimate how much of the founder's early customer relationships are still carrying the company. When I look at our top 20 accounts, 12 of them came from a direct conversation with a founder, and replacing that channel with a real sales motion is way harder than we acknowledge."}, {"raw": "the strongest argument against this is that um shipping a desktop app in twenty twenty six feels like swimming against the the tide because everyone is going web and mobile but the the counter argument is that thats exactly why theres an opening since nobody is building serious tools for the the desktop user anymore", "clean": "The strongest argument against this is that shipping a desktop app in 2026 feels like swimming against the tide because everyone is going web and mobile. But the counter argument is that that's exactly why there's an opening, since nobody is building serious tools for the desktop user anymore."}, {"raw": "if you squint at it the the churn problem and the the activation problem are actually the the same problem at different time horizons because um the the people churning at month six are the the same kind of people who barely activated in week one we just dont catch them until later", "clean": "If you squint at it, the churn problem and the activation problem are actually the same problem at different time horizons. The people churning at month six are the same kind of people who barely activated in week one. We just don't catch them until later."}, {"raw": "the way i would frame it now is that we we have a really good product for a really specific user and a mediocre product for everyone else and uh the the temptation is to broaden but the the discipline is to go narrower because the the gap between mediocre and good costs you trust and trust compounds", "clean": "The way I would frame it now is that we have a really good product for a really specific user and a mediocre product for everyone else. The temptation is to broaden, but the discipline is to go narrower, because the gap between mediocre and good costs you trust, and trust compounds."}, {"raw": "im going to take the unpopular side here and say the the all hands format is broken because um most of the the company tunes out for everything except the the qa section so we should just cut the the prepared content and run a forty five minute open qa with the leadership team because that is what people show up for anyway", "clean": "I'm going to take the unpopular side here and say the all-hands format is broken. Most of the company tunes out for everything except the Q&A section, so we should just cut the prepared content and run a 45 minute open Q&A with the leadership team. That is what people show up for anyway."}, {"raw": "in retrospect the the smartest thing we did last year wasnt any product decision it was hiring a recruiter in january because um every other team we benchmarked against fell apart on hiring in the second half and we kept shipping because we had a pipeline they didnt have so the the investment paid for itself ten times over", "clean": "In retrospect, the smartest thing we did last year wasn't any product decision. It was hiring a recruiter in January. Every other team we benchmarked against fell apart on hiring in the second half, and we kept shipping because we had a pipeline they didn't have. The investment paid for itself 10 times over."}, {"raw": "the silent killer here is meeting load like nobody complains because everyone has the the same load but um when i look at calendar density for our senior engineers theyre at twenty plus hours of meetings a week which means they have basically zero deep work time and were paying senior engineer salaries for project management work", "clean": "The silent killer here is meeting load. Nobody complains because everyone has the same load, but when I look at calendar density for our senior engineers, they're at 20 plus hours of meetings a week, which means they have basically zero deep work time. We're paying senior engineer salaries for project management work."}, {"raw": "fwiw the the thing that actually moved my read on the the partnership opportunity wasnt any of the the slides it was a five minute conversation with their head of support who described their roadmap in terms of customer outcomes instead of feature ships which is a really good signal about how they think", "clean": "FWIW, the thing that actually moved my read on the partnership opportunity wasn't any of the slides. It was a five minute conversation with their head of support, who described their roadmap in terms of customer outcomes instead of feature ships, which is a really good signal about how they think."}, {"raw": "this might be a hot take but i think the the obsession with developer experience inside the the company has gone too far because um we spent a quarter on internal tooling that probably saved each engineer thirty minutes a week and during that same quarter the the product roadmap slipped by two months so the the math doesnt actually work", "clean": "This might be a hot take, but I think the obsession with developer experience inside the company has gone too far. We spent a quarter on internal tooling that probably saved each engineer 30 minutes a week, and during that same quarter the product roadmap slipped by two months. The math doesn't actually work."}, {"raw": "the real question is whether we we have the the conviction to walk away from the the deal that finance keeps pushing because um the the terms are bad and we'll regret them in a year but everyone is scared of missing the the number this quarter and that fear is going to make us sign something we shouldnt", "clean": "The real question is whether we have the conviction to walk away from the deal that finance keeps pushing. The terms are bad, and we'll regret them in a year, but everyone is scared of missing the number this quarter, and that fear is going to make us sign something we shouldn't."}, {"raw": "what i keep missing is how the the kids schedule has completely reshaped my work week like um i thought i could keep the the same operating model after the the baby but um mornings are gone evenings are gone and the only deep work window is now ten pm to midnight which is not sustainable", "clean": "What I keep missing is how the kids' schedule has completely reshaped my work week. I thought I could keep the same operating model after the baby, but mornings are gone, evenings are gone, and the only deep work window is now 10pm to midnight, which is not sustainable."}, {"raw": "we underestimated how much the the support team was carrying for the the product because um once we shifted them off of triaging bugs and onto pure escalation handling the the bug backlog exploded and engineering finally had to acknowledge that the the support team had been silently fixing things for years", "clean": "We underestimated how much the support team was carrying for the product. Once we shifted them off of triaging bugs and onto pure escalation handling, the bug backlog exploded, and engineering finally had to acknowledge that the support team had been silently fixing things for years."}, {"raw": "honestly the the book i read last weekend completely reframed how i think about distribution because um the author argued that every successful saas company eventually picks one channel and goes deep instead of running ten shallow ones and looking at our channel mix we are absolutely guilty of running ten shallow ones", "clean": "Honestly, the book I read last weekend completely reframed how I think about distribution. The author argued that every successful SaaS company eventually picks one channel and goes deep instead of running 10 shallow ones, and looking at our channel mix, we are absolutely guilty of running 10 shallow ones."}, {"raw": "reflecting on the the side project i started last fall i think the the lesson is that um momentum dies the the moment you have to make a decision about hosting or domains so for the the next one im going to just slap it on a free tier and ship something ugly because the the friction kills more projects than the the quality bar does", "clean": "Reflecting on the side project I started last fall, I think the lesson is that momentum dies the moment you have to make a decision about hosting or domains. For the next one, I'm going to just slap it on a free tier and ship something ugly, because the friction kills more projects than the quality bar does."}, {"raw": "looking back at the the way we ran the the q two roadmap planning the the mistake was opening it up to the whole company for input because um we got two hundred suggestions and zero prioritization signal so we spent three weeks synthesizing input and ended up with a roadmap nobody actually felt ownership over", "clean": "Looking back at the way we ran the Q2 roadmap planning, the mistake was opening it up to the whole company for input. We got 200 suggestions and zero prioritization signal, so we spent three weeks synthesizing input and ended up with a roadmap nobody actually felt ownership over."}, {"raw": "the thing that bugs me about the the way we measure engineering productivity is that we keep using deploy frequency as the the headline metric and um deploy frequency went up but the the average pr size shrank to fix the metric so we get more deploys of smaller changes which isnt actually faster work its just smaller batches", "clean": "The thing that bugs me about the way we measure engineering productivity is that we keep using deploy frequency as the headline metric, and deploy frequency went up, but the average PR size shrank to fix the metric. We get more deploys of smaller changes, which isn't actually faster work. It's just smaller batches."}, {"raw": "im starting to wonder if the the constant slack notifications are actually the the reason i feel cooked at the end of every day because um theres no point during work hours where im not at least partially context switching and the cumulative cost of that is way higher than i used to acknowledge", "clean": "I'm starting to wonder if the constant Slack notifications are actually the reason I feel cooked at the end of every day. There's no point during work hours where I'm not at least partially context switching, and the cumulative cost of that is way higher than I used to acknowledge."}, {"raw": "between you and me the the reason the the launch slipped wasnt anything the team did wrong it was that i kept adding scope every week because i was nervous about how it would land and um that nervousness translated into more requirements not better requirements which is the the worst kind of leader interference", "clean": "Between you and me, the reason the launch slipped wasn't anything the team did wrong. It was that I kept adding scope every week because I was nervous about how it would land, and that nervousness translated into more requirements, not better requirements, which is the worst kind of leader interference."}, {"raw": "playing devils advocate for a second on the the the new york office expansion um most of the people pushing for it live in new york already and um none of the strategic arguments actually hold up under scrutiny like our customer base is distributed and our hiring is remote so the office is mostly a lifestyle decision dressed up as strategy", "clean": "Playing devil's advocate for a second on the New York office expansion, most of the people pushing for it live in New York already, and none of the strategic arguments actually hold up under scrutiny. Our customer base is distributed and our hiring is remote, so the office is mostly a lifestyle decision dressed up as strategy."}, {"raw": "if im being completely honest about the the q one numbers we hit revenue but we hit it by pulling forward deals from q two so the the headline looks good but the the underlying pipeline is weaker than it appears and um next quarter we are going to feel that and probably blame it on the wrong thing", "clean": "If I'm being completely honest about the Q1 numbers, we hit revenue, but we hit it by pulling forward deals from Q2. The headline looks good, but the underlying pipeline is weaker than it appears, and next quarter we are going to feel that and probably blame it on the wrong thing."}, {"raw": "what nobody seems to be saying about the the new ai feature is that um it works great in the demos but in real customer accounts the the data quality is way more variable than the demo accounts so the the perceived gap between demo and production is going to be a trust issue and we need to set expectations before launch", "clean": "What nobody seems to be saying about the new AI feature is that it works great in the demos, but in real customer accounts the data quality is way more variable than the demo accounts. The perceived gap between demo and production is going to be a trust issue, and we need to set expectations before launch."}, {"raw": "stepping back from the details on the the hiring philosophy debate i think the the disagreement is really about risk tolerance like some of us want to hire fast and let attrition correct mistakes and some of us want to hire slow and minimize mistakes upfront and both are valid but we have to actually pick one", "clean": "Stepping back from the details on the hiring philosophy debate, I think the disagreement is really about risk tolerance. Some of us want to hire fast and let attrition correct mistakes, and some of us want to hire slow and minimize mistakes upfront. Both are valid, but we have to actually pick one."}, {"raw": "the unpopular take here is that the the customer advisory board has been a net negative for the the product because um the the customers on it are our most engaged power users and theyre advocating for features that would only make sense for power users which pulls our roadmap away from where the the broader market actually is", "clean": "The unpopular take here is that the customer advisory board has been a net negative for the product. The customers on it are our most engaged power users, and they're advocating for features that would only make sense for power users, which pulls our roadmap away from where the broader market actually is."}, {"raw": "im not sure if this is right but um i think the the way we structure perf reviews actively discourages the the behavior we say we want like we ask people to take risks then we evaluate them on consistency so the the rational move is to take small safe risks and that is exactly what we see in the data", "clean": "I'm not sure if this is right, but I think the way we structure perf reviews actively discourages the behavior we say we want. We ask people to take risks, then we evaluate them on consistency, so the rational move is to take small safe risks, and that is exactly what we see in the data."}, {"raw": "from a pure metrics standpoint the the documentation site is a rounding error in our traffic but uh every enterprise deal we lost in q one cited gaps in the docs as a reason so the the metric we should be measuring isnt traffic its whether docs are removing friction from the the sales cycle", "clean": "From a pure metrics standpoint, the documentation site is a rounding error in our traffic, but every enterprise deal we lost in Q1 cited gaps in the docs as a reason. The metric we should be measuring isn't traffic. It's whether docs are removing friction from the sales cycle."}, {"raw": "lets be real about this the the the reason we keep hiring senior engineers and losing them in year one is that we sell them a vision of green field architecture work and then put them on the the same maintenance treadmill as everyone else so the the gap between recruiting pitch and actual day to day is what is killing us", "clean": "Let's be real about this. The reason we keep hiring senior engineers and losing them in year one is that we sell them a vision of green field architecture work, and then put them on the same maintenance treadmill as everyone else. The gap between recruiting pitch and actual day-to-day is what is killing us."}, {"raw": "the part that frustrates me is that we we have all this great qualitative feedback from churned customers and um it just sits in a doc nobody reads because there is no owner for closing the the loop between research and roadmap so the the same lessons get rediscovered every six months by a new person", "clean": "The part that frustrates me is that we have all this great qualitative feedback from churned customers, and it just sits in a doc nobody reads, because there is no owner for closing the loop between research and roadmap. The same lessons get rediscovered every six months by a new person."}, {"raw": "after the meeting with the the board chair last night im rethinking the the whole annual planning structure because um his point was that the the more turbulent the the market the shorter your planning horizon should be and we are still operating on a twelve month plan in a market that resets every quarter", "clean": "After the meeting with the board chair last night, I'm rethinking the whole annual planning structure. His point was that the more turbulent the market, the shorter your planning horizon should be, and we are still operating on a 12 month plan in a market that resets every quarter."}, {"raw": "i keep coming back to the the idea that um most of our retention problems are actually onboarding problems we just measure them at the wrong time like by the time someone churns in month four the the decision was probably made in week one we just didnt have any signal for it", "clean": "I keep coming back to the idea that most of our retention problems are actually onboarding problems. We just measure them at the wrong time. By the time someone churns in month four, the decision was probably made in week one. We just didn't have any signal for it."}, {"raw": "one thing i havent figured out is how to talk about levels with engineers in a way that doesnt feel like a checklist because um the the rubric helps with consistency but it also makes growth conversations feel transactional and the the people who care most about growth are the ones who hate the transactional framing the most", "clean": "One thing I haven't figured out is how to talk about levels with engineers in a way that doesn't feel like a checklist. The rubric helps with consistency, but it also makes growth conversations feel transactional, and the people who care most about growth are the ones who hate the transactional framing the most."}, {"raw": "the contrarian view on the the meeting culture debate is that um cutting meetings doesnt actually create deep work time it just creates fragmented slack threads that take more total time and resolve worse so the the answer isnt fewer meetings its better meetings with clearer outputs", "clean": "The contrarian view on the meeting culture debate is that cutting meetings doesn't actually create deep work time. It just creates fragmented Slack threads that take more total time and resolve worse. The answer isn't fewer meetings. It's better meetings with clearer outputs."}, {"raw": "what i wish we had done differently with the the european launch is um sent someone to actually live there for two months before we made any decisions because we made every choice from a slack channel in california and you can feel that in how out of touch the the messaging is for that market", "clean": "What I wish we had done differently with the European launch is sent someone to actually live there for two months before we made any decisions. We made every choice from a Slack channel in California, and you can feel that in how out of touch the messaging is for that market."}, {"raw": "thinking out loud here um what if the the right move for the the next hire isnt seniority its someone with a totally different industry background because the the team is starting to think the same way and we keep arriving at the the same kinds of solutions to every problem", "clean": "Thinking out loud here, what if the right move for the next hire isn't seniority? It's someone with a totally different industry background, because the team is starting to think the same way, and we keep arriving at the same kinds of solutions to every problem."}, {"raw": "if i had to bet on which of our competitors becomes the the real threat in eighteen months its not the the well funded one its the the smaller team that just shipped a really opinionated workflow product because um opinionated beats configurable every time when the the buyer is overwhelmed", "clean": "If I had to bet on which of our competitors becomes the real threat in 18 months, it's not the well-funded one. It's the smaller team that just shipped a really opinionated workflow product, because opinionated beats configurable every time when the buyer is overwhelmed."}, {"raw": "the awkward truth is that the the friendships ive let slide this past year because of work arent going to magically recover when things calm down because um things never calm down and the the longer i wait the the harder it is to text someone after six months of silence", "clean": "The awkward truth is that the friendships I've let slide this past year because of work aren't going to magically recover when things calm down. Things never calm down, and the longer I wait, the harder it is to text someone after six months of silence."}, {"raw": "to be clear about my position on the the rebrand im not against changing the the logo im against doing it in q three when we have three large launches because um nothing good comes from changing the the visual identity in the middle of a heavy launch cycle and we are setting ourselves up to do both badly", "clean": "To be clear about my position on the rebrand, I'm not against changing the logo. I'm against doing it in Q3 when we have three large launches. Nothing good comes from changing the visual identity in the middle of a heavy launch cycle, and we are setting ourselves up to do both badly."}, {"raw": "every time i look at the the data on what drives expansion revenue um the the single biggest predictor is whether the customer has more than five active seats by day thirty and we have basically no product mechanisms designed around that milestone we just kind of hope it happens", "clean": "Every time I look at the data on what drives expansion revenue, the single biggest predictor is whether the customer has more than five active seats by day 30, and we have basically no product mechanisms designed around that milestone. We just kind of hope it happens."}, {"raw": "the framing that helped me on the the platform versus application debate was um treating it as a sequencing question instead of a binary like every successful platform was a successful application first and trying to be a platform before you have an application is how you end up with neither", "clean": "The framing that helped me on the platform versus application debate was treating it as a sequencing question instead of a binary. Every successful platform was a successful application first, and trying to be a platform before you have an application is how you end up with neither."}, {"raw": "what surprises me about the the trip planning for august is how much friction there still is in like coordinating four adults across three calendars um you'd think by now there would be a tool that doesnt suck but every time we try one we end up back in a group text", "clean": "What surprises me about the trip planning for August is how much friction there still is in coordinating four adults across three calendars. You'd think by now there would be a tool that doesn't suck, but every time we try one, we end up back in a group text."}, {"raw": "from where i sit on the the onboarding redesign the the central question is whether we optimize for first session success or first week success because um theyre actually different problems and the the design decisions you make for one can hurt the other and we keep conflating the two in roadmap discussions", "clean": "From where I sit on the onboarding redesign, the central question is whether we optimize for first session success or first week success. They're actually different problems, and the design decisions you make for one can hurt the other, and we keep conflating the two in roadmap discussions."}, {"raw": "i think we underestimate how exhausting the the always on culture is for the the team because um nobody complains directly they just slowly disengage like the the slack response times get longer the the questions in standups get shorter and by the time someone resigns weve already lost them six months earlier", "clean": "I think we underestimate how exhausting the always-on culture is for the team. Nobody complains directly. They just slowly disengage. The Slack response times get longer, the questions in standups get shorter, and by the time someone resigns, we've already lost them six months earlier."}, {"raw": "the strongest argument against the the api first strategy is that um most of our customers dont want an api they want a great ui and building api first means every feature ships later and worse than it would have if we just built the ui directly so we are paying a tax for a future that may not arrive", "clean": "The strongest argument against the API-first strategy is that most of our customers don't want an API. They want a great UI, and building API-first means every feature ships later and worse than it would have if we just built the UI directly. We are paying a tax for a future that may not arrive."}, {"raw": "if you squint at it the the qa process and the the code review process are basically duplicating the the same coverage and uh we should probably just collapse them into one stronger gate instead of running both half heartedly because the the failure mode is that each one assumes the the other caught the issue", "clean": "If you squint at it, the QA process and the code review process are basically duplicating the same coverage, and we should probably just collapse them into one stronger gate instead of running both half-heartedly. The failure mode is that each one assumes the other caught the issue."}, {"raw": "the way i would frame it now is that the the company we are trying to be in eighteen months requires a completely different operating cadence than the the one we have today and um pretending we can keep our current rituals while doubling in size is the the highest leverage thing we are getting wrong", "clean": "The way I would frame it now is that the company we are trying to be in 18 months requires a completely different operating cadence than the one we have today, and pretending we can keep our current rituals while doubling in size is the highest leverage thing we are getting wrong."}, {"raw": "im going to take the unpopular side here on the the vendor consolidation push because um in theory it saves money in practice it creates a single point of failure for the the entire stack and the the last time we did this the the vendor used the the leverage to raise prices on renewal", "clean": "I'm going to take the unpopular side here on the vendor consolidation push. In theory it saves money. In practice it creates a single point of failure for the entire stack, and the last time we did this, the vendor used the leverage to raise prices on renewal."}, {"raw": "in retrospect the the call to deprioritize accessibility work two years ago is one of the bigger regrets i have because um now were retrofitting it under enterprise procurement pressure and retrofitting is at least three times more expensive than building it in from the start", "clean": "In retrospect, the call to deprioritize accessibility work two years ago is one of the bigger regrets I have. Now we're retrofitting it under enterprise procurement pressure, and retrofitting is at least 3 times more expensive than building it in from the start."}, {"raw": "fwiw my read on the the conversation with the the analyst yesterday is that um the the category we are positioning ourselves in is actually shrinking and the the category next door is growing fast so we either reframe the the company within the the next two quarters or we get stuck defending share in a declining market", "clean": "FWIW, my read on the conversation with the analyst yesterday is that the category we are positioning ourselves in is actually shrinking, and the category next door is growing fast. We either reframe the company within the next two quarters, or we get stuck defending share in a declining market."}, {"raw": "this might be a hot take but um documentation is a leadership responsibility not an individual contributor task because every time leadership writes down a decision the the the whole org saves hours of clarification questions and every time leadership doesnt the the same conversation happens fifty times in slack", "clean": "This might be a hot take, but documentation is a leadership responsibility, not an individual contributor task. Every time leadership writes down a decision, the whole org saves hours of clarification questions, and every time leadership doesn't, the same conversation happens 50 times in Slack."}]
data/seed/batches/meeting_notes.json ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "raw": "ok so we decided to ship the dark mode rollout next sprint priya is the owner", "clean": "We decided to ship the dark mode rollout next sprint. Priya is the owner." },
3
+ { "raw": "action item follow up with legal about the dpa", "clean": "Action item: follow up with legal about the DPA." },
4
+ { "raw": "uh raj is blocked on the snowflake creds", "clean": "Raj is blocked on the Snowflake creds." },
5
+ { "raw": "kickoff went well", "clean": "Kickoff went well." },
6
+ { "raw": "we agreed to drop the gamification scope", "clean": "We agreed to drop the gamification scope." },
7
+ { "raw": "main risk is um the api migration sliding into q3", "clean": "Main risk is the API migration sliding into Q3." },
8
+ { "raw": "so the the takeaway from standup is that we need a second backend engineer on the billing rewrite or we wont hit the august date", "clean": "The takeaway from standup is that we need a second backend engineer on the billing rewrite, or we won't hit the August date." },
9
+ { "raw": "note for myself ping sasha about the figma handoff for the onboarding screens she said tuesday", "clean": "Note for myself: ping Sasha about the Figma handoff for the onboarding screens. She said Tuesday." },
10
+ { "raw": "ok decision recap we are going with stripe over adyen because of the existing connector and lower lift", "clean": "Decision recap: we are going with Stripe over Adyen because of the existing connector and lower lift." },
11
+ { "raw": "follow up with marcus about the soc2 evidence binder he said he would have it by friday", "clean": "Follow up with Marcus about the SOC2 evidence binder. He said he would have it by Friday." },
12
+ { "raw": "um so jenna walked us through the the new pricing model and i think the main concern is enterprise customers who already signed annual contracts at the old rate were going to have to grandfather them for at least twelve months", "clean": "Jenna walked us through the new pricing model, and I think the main concern is enterprise customers who already signed annual contracts at the old rate. We're going to have to grandfather them for at least 12 months." },
13
+ { "raw": "action for me draft the postmortem doc for the saturday outage by eod tomorrow", "clean": "Action for me: draft the postmortem doc for the Saturday outage by EOD tomorrow." },
14
+ { "raw": "we we agreed the mvp doesnt need sso just basic email auth", "clean": "We agreed the MVP doesn't need SSO, just basic email auth." },
15
+ { "raw": "tom raised a good point about rate limiting on the public api we havent specced it yet", "clean": "Tom raised a good point about rate limiting on the public API. We haven't specced it yet." },
16
+ { "raw": "ok i think the the consensus is we postpone the rebrand til after the series b closes", "clean": "I think the consensus is we postpone the rebrand until after the Series B closes." },
17
+ { "raw": "quick recap from the sync with marketing they want the landing page copy locked by the fifteenth and they need three hero variants for ab testing", "clean": "Quick recap from the sync with marketing: they want the landing page copy locked by the 15th, and they need three hero variants for AB testing." },
18
+ { "raw": "blocker emily cant access the staging env her vpn cert expired", "clean": "Blocker: Emily can't access the staging env. Her VPN cert expired." },
19
+ { "raw": "so basically the the the customer escalation from acme is because we shipped the wrong invoice format and now their ap team is rejecting all our bills since march we need to issue corrected invoices and probably credit a late fee", "clean": "Basically, the customer escalation from Acme is because we shipped the wrong invoice format, and now their AP team is rejecting all our bills since March. We need to issue corrected invoices and probably credit a late fee." },
20
+ { "raw": "decision keep the monorepo dont split", "clean": "Decision: keep the monorepo, don't split." },
21
+ { "raw": "um nikhil owns the data migration script", "clean": "Nikhil owns the data migration script." },
22
+ { "raw": "follow up with the salesforce admin about field permissions for the new lead source", "clean": "Follow up with the Salesforce admin about field permissions for the new lead source." },
23
+ { "raw": "so the meeting with retool went well they can do the the integration in like two weeks if we give them schema access by monday", "clean": "The meeting with Retool went well. They can do the integration in about two weeks if we give them schema access by Monday." },
24
+ { "raw": "action item carlos to finalize the okr draft for q2 by thursday", "clean": "Action item: Carlos to finalize the OKR draft for Q2 by Thursday." },
25
+ { "raw": "i mean the the bigger concern is we still dont have a hire for the staff sre role and oncall is killing the team", "clean": "The bigger concern is we still don't have a hire for the staff SRE role, and oncall is killing the team." },
26
+ { "raw": "decided to use postgres over mongo for the events service", "clean": "Decided to use Postgres over Mongo for the events service." },
27
+ { "raw": "noted concern from sales the new tier pricing might cannibalize the mid market plan", "clean": "Noted concern from sales: the new tier pricing might cannibalize the mid-market plan." },
28
+ { "raw": "ok so um the the design review with rachel was productive we settled on the the side nav pattern and shes going to spec out the empty states by next week", "clean": "The design review with Rachel was productive. We settled on the side nav pattern, and she's going to spec out the empty states by next week." },
29
+ { "raw": "reminder push the kafka upgrade to the april window not march", "clean": "Reminder: push the Kafka upgrade to the April window, not March." },
30
+ { "raw": "ben said hes going to take ownership of the the customer health score project", "clean": "Ben said he's going to take ownership of the customer health score project." },
31
+ { "raw": "main thing from the the board prep is we need cleaner cohort retention charts and a separate slide on gross margin trajectory", "clean": "Main thing from the board prep is we need cleaner cohort retention charts and a separate slide on gross margin trajectory." },
32
+ { "raw": "uh the offsite is confirmed for the week of june ninth in lisbon", "clean": "The offsite is confirmed for the week of June 9th in Lisbon." },
33
+ { "raw": "so we we talked through the eu rollout and the the biggest open question is whether we host in frankfurt or dublin and i think aws frankfurt makes more sense for latency to the dach market", "clean": "We talked through the EU rollout, and the biggest open question is whether we host in Frankfurt or Dublin. I think AWS Frankfurt makes more sense for latency to the DACH market." },
34
+ { "raw": "action item review the the security questionnaire from hubspot before friday", "clean": "Action item: review the security questionnaire from HubSpot before Friday." },
35
+ { "raw": "um julien is going to lead the migration off heroku to fly", "clean": "Julien is going to lead the migration off Heroku to Fly." },
36
+ { "raw": "we agreed not to backport the the patch to the v1 line just call it out in the changelog", "clean": "We agreed not to backport the patch to the v1 line, just call it out in the changelog." },
37
+ { "raw": "ok recap from the the 1 1 with mira shes feeling stretched between the platform team and the the growth pod we need to pick one for q3", "clean": "Recap from the 1:1 with Mira: she's feeling stretched between the platform team and the growth pod. We need to pick one for Q3." },
38
+ { "raw": "decision use clerk for auth not roll our own", "clean": "Decision: use Clerk for auth, not roll our own." },
39
+ { "raw": "owner for the langsmith eval setup is dev", "clean": "Owner for the LangSmith eval setup is Dev." },
40
+ { "raw": "follow up vinay said he can probably get us a discount on the the datadog renewal if we commit to two years", "clean": "Follow up: Vinay said he can probably get us a discount on the Datadog renewal if we commit to two years." },
41
+ { "raw": "the the the customer interview with northwind was a goldmine they basically said our reporting module is so bad they export to excel every week and rebuild dashboards manually we need to prioritize the reporting v2 work", "clean": "The customer interview with Northwind was a goldmine. They basically said our reporting module is so bad they export to Excel every week and rebuild dashboards manually. We need to prioritize the reporting v2 work." },
42
+ { "raw": "uh action item add a slack channel for the the procurement project", "clean": "Action item: add a Slack channel for the procurement project." },
43
+ { "raw": "you know the the engineering planning session ran long but we got alignment on the the q2 themes which are reliability platform leverage and shipping the agent feature", "clean": "The engineering planning session ran long, but we got alignment on the Q2 themes, which are reliability, platform leverage, and shipping the agent feature." },
44
+ { "raw": "owners reliability is hannah platform is kai agent feature is me", "clean": "Owners: reliability is Hannah, platform is Kai, agent feature is me." },
45
+ { "raw": "concern from finance the the burn rate is creeping up because of the new gpu spend", "clean": "Concern from finance: the burn rate is creeping up because of the new GPU spend." },
46
+ { "raw": "we decided to delay the the public beta by three weeks", "clean": "We decided to delay the public beta by three weeks." },
47
+ { "raw": "um ok so the the the retro takeaways were one we shipped too late in the the sprint two qa was a bottleneck and three we should split the the team into a build and a polish track for the next cycle", "clean": "The retro takeaways were: one, we shipped too late in the sprint; two, QA was a bottleneck; and three, we should split the team into a build and a polish track for the next cycle." },
48
+ { "raw": "follow up with hr on the the contractor conversion for amelia", "clean": "Follow up with HR on the contractor conversion for Amelia." },
49
+ { "raw": "agreed to spike out the the temporal proof of concept before committing", "clean": "Agreed to spike out the Temporal proof of concept before committing." },
50
+ { "raw": "uh ok decision recap pricing page experiment ships next monday daniel owns copy max owns the the dev work", "clean": "Decision recap: pricing page experiment ships next Monday. Daniel owns copy, Max owns the dev work." },
51
+ { "raw": "i i think the the meeting with twilio went sideways their their enterprise rep was pushing us to commit to a hundred thousand annual but our usage data only supports like forty so we we walked", "clean": "I think the meeting with Twilio went sideways. Their enterprise rep was pushing us to commit to $100k annual, but our usage data only supports about $40k, so we walked." },
52
+ { "raw": "action item draft a one pager for the the board on the agent strategy", "clean": "Action item: draft a one-pager for the board on the agent strategy." },
53
+ { "raw": "noted the the data team needs a dedicated analyst by end of q2", "clean": "Noted: the data team needs a dedicated analyst by end of Q2." },
54
+ { "raw": "um quick note from the customer call with patagonia they they want sso via okta not azure ad", "clean": "Quick note from the customer call with Patagonia: they want SSO via Okta, not Azure AD." },
55
+ { "raw": "ok so we agreed on the the the architecture for the search rewrite were going with opensearch managed by aws and a thin elasticsearch compat layer so we dont break existing clients", "clean": "We agreed on the architecture for the search rewrite. We're going with OpenSearch managed by AWS and a thin Elasticsearch compat layer so we don't break existing clients." },
56
+ { "raw": "blocker the the figma file from the agency is still missing the the mobile breakpoints", "clean": "Blocker: the Figma file from the agency is still missing the mobile breakpoints." },
57
+ { "raw": "decision drop intercom move to plain", "clean": "Decision: drop Intercom, move to Plain." },
58
+ { "raw": "follow up email sara about the the partnership terms with anthropic", "clean": "Follow up: email Sara about the partnership terms with Anthropic." },
59
+ { "raw": "um the the main thing from the marketing sync is we need three customer logos on the homepage by the june launch and right now we only have signed permission from two", "clean": "The main thing from the marketing sync is we need three customer logos on the homepage by the June launch, and right now we only have signed permission from two." },
60
+ { "raw": "action item write up the the rfc for the event bus changes", "clean": "Action item: write up the RFC for the event bus changes." },
61
+ { "raw": "i mean the the recurring theme from support is that the the error messages are unhelpful and customers cant self serve we should prioritize the error copy audit", "clean": "The recurring theme from support is that the error messages are unhelpful and customers can't self-serve. We should prioritize the error copy audit." },
62
+ { "raw": "we we agreed to freeze hiring outside of the the two open eng roles", "clean": "We agreed to freeze hiring outside of the two open eng roles." },
63
+ { "raw": "owner for the the q2 board deck is the cfo office", "clean": "Owner for the Q2 board deck is the CFO office." },
64
+ { "raw": "uh follow up the the legal review on the master services agreement template is still pending need to ping zoe", "clean": "Follow up: the legal review on the master services agreement template is still pending. Need to ping Zoe." },
65
+ { "raw": "ok so um the the the conclusion from the architecture review is that the the current monolith is fine for another twelve to eighteen months and we should focus on extracting the the billing and search services first because those are the highest contention points", "clean": "The conclusion from the architecture review is that the current monolith is fine for another 12 to 18 months, and we should focus on extracting the billing and search services first because those are the highest contention points." },
66
+ { "raw": "decision skip the the conference sponsorship this year", "clean": "Decision: skip the conference sponsorship this year." },
67
+ { "raw": "noted aaron will run point on the the gartner submission", "clean": "Noted: Aaron will run point on the Gartner submission." },
68
+ { "raw": "um quick recap from the sync with the the data team they need read replicas for analytics queries the current setup is hammering prod", "clean": "Quick recap from the sync with the data team: they need read replicas for analytics queries. The current setup is hammering prod." },
69
+ { "raw": "follow up with mike about the the on call rotation hes pulling too many shifts this month", "clean": "Follow up with Mike about the oncall rotation. He's pulling too many shifts this month." },
70
+ { "raw": "action item update the the runbook for the redis failover", "clean": "Action item: update the runbook for the Redis failover." },
71
+ { "raw": "we we decided the the q1 north star is activation not retention", "clean": "We decided the Q1 north star is activation, not retention." },
72
+ { "raw": "so the the the all hands feedback was largely positive but a recurring theme was that people want more transparency on roadmap priorities and the the exec team should own a public quarterly update", "clean": "The all-hands feedback was largely positive, but a recurring theme was that people want more transparency on roadmap priorities, and the exec team should own a public quarterly update." },
73
+ { "raw": "uh decision move the daily standup to async via slack starting next week", "clean": "Decision: move the daily standup to async via Slack starting next week." },
74
+ { "raw": "owner for the slack workflow is rosa", "clean": "Owner for the Slack workflow is Rosa." },
75
+ { "raw": "i mean the the the conversation with the the new design lead candidate went really well i think we should move to references shes got strong opinions on systems work and a clean portfolio shipped at linear and ramp", "clean": "The conversation with the new design lead candidate went really well. I think we should move to references. She's got strong opinions on systems work and a clean portfolio shipped at Linear and Ramp." },
76
+ { "raw": "ok the the open question is do we charge for the the api access on the the starter tier i lean no but pricing wants yes", "clean": "The open question is: do we charge for the API access on the starter tier? I lean no, but pricing wants yes." },
77
+ { "raw": "action item book a deep dive with the the security team about the supply chain audit findings", "clean": "Action item: book a deep dive with the security team about the supply chain audit findings." },
78
+ { "raw": "um followup the the customer success team wants a dashboard for renewal risk we promised a v1 by end of month", "clean": "Follow up: the customer success team wants a dashboard for renewal risk. We promised a v1 by end of month." },
79
+ { "raw": "decision dont rebuild the the admin panel just iterate on the the current one", "clean": "Decision: don't rebuild the admin panel, just iterate on the current one." },
80
+ { "raw": "you know the the meeting with the the cfo about headcount went better than expected he approved two more eng hires for q3 and a pm rec for the the platform team", "clean": "The meeting with the CFO about headcount went better than expected. He approved two more eng hires for Q3 and a PM rec for the platform team." },
81
+ { "raw": "uh note from the the design crit the the new dashboard has too much cognitive load on first load we need to collapse the the secondary metrics by default", "clean": "Note from the design crit: the new dashboard has too much cognitive load on first load. We need to collapse the secondary metrics by default." },
82
+ { "raw": "ok so um the the the wrap up from todays planning is feature freeze on the the may release is may twelfth qa window is may thirteenth through nineteenth and we ship the morning of the twentieth assuming no p0s", "clean": "The wrap-up from today's planning is: feature freeze on the May release is May 12th, QA window is May 13th through 19th, and we ship the morning of the 20th, assuming no P0s." }
83
+ ]
data/seed/batches/mixed_content.json ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "raw": "meeting with sarah at two fifteen pm tomorrow", "clean": "Meeting with Sarah at 2:15 PM tomorrow." },
3
+ { "raw": "my number is six five oh five five five eight nine four two", "clean": "My number is 650-555-8942." },
4
+ { "raw": "the address is fourteen twenty three pine avenue apartment seven b", "clean": "The address is 1423 Pine Avenue, Apartment 7B." },
5
+ { "raw": "send the package to nine eighty seven oak drive in austin texas", "clean": "Send the package to 987 Oak Drive in Austin, Texas." },
6
+ { "raw": "um book the flight for march twelfth departing at seven forty am", "clean": "Book the flight for March 12th departing at 7:40 AM." },
7
+ { "raw": "her email is jessica dot chen at gmail dot com", "clean": "Her email is jessica.chen@gmail.com." },
8
+ { "raw": "we we need to be at the airport by four thirty for united three oh seven to denver", "clean": "We need to be at the airport by 4:30 for United 307 to Denver." },
9
+ { "raw": "the invoice total comes to twelve thousand four hundred and eighty seven dollars and sixty cents", "clean": "The invoice total comes to $12,487.60." },
10
+ { "raw": "dr patel is available on tuesday june fourth at ten fifteen", "clean": "Dr. Patel is available on Tuesday, June 4th at 10:15." },
11
+ { "raw": "order number is x y z four four nine zero eight ship to forty two waverly place new york new york one zero zero one one", "clean": "Order number is XYZ44908. Ship to 42 Waverly Place, New York, NY 10011." },
12
+ { "raw": "uh the quarterly review is scheduled for september twenty third at nine am sharp", "clean": "The quarterly review is scheduled for September 23rd at 9 AM sharp." },
13
+ { "raw": "call me back at three one two five five five oh two one four when you get a chance", "clean": "Call me back at 312-555-0214 when you get a chance." },
14
+ { "raw": "the conference is at the marriott on lexington between forty seventh and forty eighth", "clean": "The conference is at the Marriott on Lexington between 47th and 48th." },
15
+ { "raw": "transfer two thousand five hundred from chase checking to my fidelity account", "clean": "Transfer $2,500 from Chase checking to my Fidelity account." },
16
+ { "raw": "lunch with marcus from stripe on thursday at twelve thirty at tartine", "clean": "Lunch with Marcus from Stripe on Thursday at 12:30 at Tartine." },
17
+ { "raw": "the meeting room is two oh four on the second floor", "clean": "The meeting room is 204 on the second floor." },
18
+ { "raw": "my flight is jetblue eleven seventy two boarding at gate c twenty two", "clean": "My flight is JetBlue 1172 boarding at gate C22." },
19
+ { "raw": "dentist appointment monday august eleventh at eight forty five", "clean": "Dentist appointment Monday, August 11th at 8:45." },
20
+ { "raw": "the rental is one thousand eight hundred fifty per month plus utilities", "clean": "The rental is $1,850 per month plus utilities." },
21
+ { "raw": "email priya at p dot raman at acme corp dot io about the contract", "clean": "Email Priya at p.raman@acmecorp.io about the contract." },
22
+ { "raw": "i think the package was delivered to seven sixteen birch lane not seven sixty", "clean": "I think the package was delivered to 716 Birch Lane, not 760." },
23
+ { "raw": "the wedding is on saturday october eighteenth at five pm at the ritz carlton", "clean": "The wedding is on Saturday, October 18th at 5 PM at the Ritz-Carlton." },
24
+ { "raw": "uh tracking number is one z nine nine nine four four four six eight one two three four five", "clean": "Tracking number is 1Z9994446812345." },
25
+ { "raw": "rebecca lives at twenty two ten valencia street san francisco california nine four one one zero", "clean": "Rebecca lives at 2210 Valencia Street, San Francisco, California 94110." },
26
+ { "raw": "you you need to pick up the kids at three fifteen from lincoln elementary", "clean": "You need to pick up the kids at 3:15 from Lincoln Elementary." },
27
+ { "raw": "the new tesla model y is forty seven thousand nine hundred", "clean": "The new Tesla Model Y is $47,900." },
28
+ { "raw": "her phone is plus four four seven seven oh one two three four five six seven", "clean": "Her phone is +44 7701 234 567." },
29
+ { "raw": "ill be at the office around nine forty five after the standup", "clean": "I'll be at the office around 9:45 after the standup." },
30
+ { "raw": "the room dimensions are twelve feet by fourteen feet six inches", "clean": "The room dimensions are 12 feet by 14 feet 6 inches." },
31
+ { "raw": "send the invoice to accounts payable at northstar industries dot com", "clean": "Send the invoice to accountspayable@northstarindustries.com." },
32
+ { "raw": "so um the project deadline is friday november seventh end of business", "clean": "The project deadline is Friday, November 7th, end of business." },
33
+ { "raw": "dial nine for an outside line then one eight hundred five five five four three two one", "clean": "Dial 9 for an outside line, then 1-800-555-4321." },
34
+ { "raw": "my zoom id is seven eight four five nine two one three six passcode delta", "clean": "My Zoom ID is 7845921 36, passcode Delta." },
35
+ { "raw": "we need three dozen bagels for the meeting on wednesday morning", "clean": "We need 3 dozen bagels for the meeting on Wednesday morning." },
36
+ { "raw": "the prescription is for ten milligrams twice a day for fourteen days", "clean": "The prescription is for 10 milligrams twice a day for 14 days." },
37
+ { "raw": "uber driver arriving in four minutes silver toyota camry license seven a b c three four five", "clean": "Uber driver arriving in 4 minutes. Silver Toyota Camry, license 7ABC345." },
38
+ { "raw": "i i need to reschedule from tuesday at two to wednesday at three thirty", "clean": "I need to reschedule from Tuesday at 2 to Wednesday at 3:30." },
39
+ { "raw": "the airbnb is at six twelve mission street unit four c check in is after three pm", "clean": "The Airbnb is at 612 Mission Street, Unit 4C. Check-in is after 3 PM." },
40
+ { "raw": "amazon order is one one one dash five two three four five six seven dash eight nine zero one two three four", "clean": "Amazon order is 111-5234567-8901234." },
41
+ { "raw": "the cost breakdown is two fifty for design four hundred for development and one fifty for testing", "clean": "The cost breakdown is $250 for design, $400 for development, and $150 for testing." },
42
+ { "raw": "meet at peet's coffee on castro at eleven fifteen this morning", "clean": "Meet at Peet's Coffee on Castro at 11:15 this morning." },
43
+ { "raw": "um the kickoff call is monday at nine am pacific noon eastern", "clean": "The kickoff call is Monday at 9 AM Pacific, noon Eastern." },
44
+ { "raw": "the package weighs three point four kilos and ships from frankfurt", "clean": "The package weighs 3.4 kilos and ships from Frankfurt." },
45
+ { "raw": "well her birthday is on the twenty fifth of december same as christmas", "clean": "Her birthday is on the 25th of December, same as Christmas." },
46
+ { "raw": "amex ending in four three two one was charged seventy nine ninety nine", "clean": "Amex ending in 4321 was charged $79.99." },
47
+ { "raw": "you know the new office is at three twenty market street suite eleven hundred", "clean": "The new office is at 320 Market Street, Suite 1100." },
48
+ { "raw": "drive to portland is six hours forty minutes via i five", "clean": "Drive to Portland is 6 hours 40 minutes via I-5." },
49
+ { "raw": "the venmo handle is at alex hyphen rodriguez ninety two", "clean": "The Venmo handle is @alex-rodriguez92." },
50
+ { "raw": "i mean the lease starts on june first and runs for twelve months", "clean": "The lease starts on June 1st and runs for 12 months." },
51
+ { "raw": "her flight lands at sfo at eight twenty pm on terminal two", "clean": "Her flight lands at SFO at 8:20 PM on Terminal 2." },
52
+ { "raw": "send the contract to legal at twin oaks ventures dot co", "clean": "Send the contract to legal@twinoaksventures.co." },
53
+ { "raw": "the rent is split three ways at eight fifty each per month", "clean": "The rent is split 3 ways at $850 each per month." },
54
+ { "raw": "uh starbucks on union square opens at five thirty am", "clean": "Starbucks on Union Square opens at 5:30 AM." },
55
+ { "raw": "the model number is g e dash four two zero seven s and the serial is r x nine eight seven six five", "clean": "The model number is GE-4207S and the serial is RX98765." },
56
+ { "raw": "her instagram is at maya underscore writes plural", "clean": "Her Instagram is @maya_writes." },
57
+ { "raw": "the apartment is one bed one bath six fifty square feet asking thirty two hundred", "clean": "The apartment is 1 bed, 1 bath, 650 square feet, asking $3,200." },
58
+ { "raw": "call kaiser at one eight zero zero four six four four zero zero zero to schedule", "clean": "Call Kaiser at 1-800-464-4000 to schedule." },
59
+ { "raw": "the team retreat is june fifteenth through eighteenth in lake tahoe", "clean": "The team retreat is June 15th through 18th in Lake Tahoe." },
60
+ { "raw": "so the iphone fifteen pro max in titanium is twelve hundred bucks", "clean": "The iPhone 15 Pro Max in titanium is $1,200." },
61
+ { "raw": "address is two oh oh north michigan avenue chicago illinois six oh six oh one", "clean": "Address is 200 North Michigan Avenue, Chicago, Illinois 60601." },
62
+ { "raw": "uh she she works at google in mountain view as a staff engineer making three twenty base", "clean": "She works at Google in Mountain View as a staff engineer making $320K base." },
63
+ { "raw": "my license plate is eight charlie tango whiskey four six two", "clean": "My license plate is 8CTW462." },
64
+ { "raw": "checkout is by eleven am with a fifty dollar late fee after", "clean": "Checkout is by 11 AM with a $50 late fee after." },
65
+ { "raw": "the qr code links to bit dot ly slash three k j l two p", "clean": "The QR code links to bit.ly/3kJL2p." },
66
+ { "raw": "i think the meeting is in conference room aspen on the fourth floor at two pm", "clean": "I think the meeting is in conference room Aspen on the fourth floor at 2 PM." },
67
+ { "raw": "the doctor said sixty over ninety blood pressure is fine but watch the sodium", "clean": "The doctor said 60/90 blood pressure is fine, but watch the sodium." },
68
+ { "raw": "well um book the suite at the four seasons for two nights checking in friday the twenty second", "clean": "Book the suite at the Four Seasons for 2 nights, checking in Friday the 22nd." },
69
+ { "raw": "the gym opens at five am closes at midnight weekdays and ten pm on weekends", "clean": "The gym opens at 5 AM, closes at midnight weekdays and 10 PM on weekends." },
70
+ { "raw": "her social is one two three dash four five dash six seven eight nine for the i nine form", "clean": "Her social is 123-45-6789 for the I-9 form." },
71
+ { "raw": "ship to ted lasso care of richmond fc one hundred kingsmeadow road kingston upon thames", "clean": "Ship to Ted Lasso, care of Richmond FC, 100 Kingsmeadow Road, Kingston upon Thames." }
72
+ ]
data/seed/batches/professional_emails.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "raw": "hi marcus quick follow up on the contract did legal sign off yet", "clean": "Hi Marcus, quick follow up on the contract. Did legal sign off yet?" },
3
+ { "raw": "thanks for sending the deck ill review tonight", "clean": "Thanks for sending the deck. I'll review tonight." },
4
+ { "raw": "um can we push the standup to ten thirty", "clean": "Can we push the standup to 10:30?" },
5
+ { "raw": "approved go ahead with the vendor", "clean": "Approved. Go ahead with the vendor." },
6
+ { "raw": "hey priya wanted to check in on the northwind integration are we still on track for the june fifteenth launch", "clean": "Hey Priya, wanted to check in on the Northwind integration. Are we still on track for the June 15th launch?" },
7
+ { "raw": "hi team just a heads up that ill be out of office thursday and friday next week so please loop in daniel for any urgent acme requests", "clean": "Hi team, just a heads up that I'll be out of office Thursday and Friday next week, so please loop in Daniel for any urgent Acme requests." },
8
+ { "raw": "so um following up on yesterdays call i think we agreed that we we need to push the migration to q four because the the staging environment isnt ready yet let me know if that matches your notes", "clean": "Following up on yesterday's call, I think we agreed that we need to push the migration to Q4 because the staging environment isn't ready yet. Let me know if that matches your notes." },
9
+ { "raw": "hi rachel could you share the latest forecast model when you get a chance id like to review before the board meeting", "clean": "Hi Rachel, could you share the latest forecast model when you get a chance? I'd like to review before the board meeting." },
10
+ { "raw": "confirming the call at three pm pacific tomorrow", "clean": "Confirming the call at 3 PM Pacific tomorrow." },
11
+ { "raw": "looping in jamie from procurement", "clean": "Looping in Jamie from procurement." },
12
+ { "raw": "hi alex um quick question about the salesforce integration do we have api credentials yet or are we still waiting on it", "clean": "Hi Alex, quick question about the Salesforce integration. Do we have API credentials yet, or are we still waiting on IT?" },
13
+ { "raw": "thanks for the intro nice to e meet you sarah ill send over a calendar invite for next week so we can chat about the partnership", "clean": "Thanks for the intro. Nice to e-meet you, Sarah. I'll send over a calendar invite for next week so we can chat about the partnership." },
14
+ { "raw": "wanted to flag a blocker on the data pipeline work we we cant proceed until the schema review is signed off by the architecture team can we get that on the agenda for fridays sync", "clean": "Wanted to flag a blocker on the data pipeline work. We can't proceed until the schema review is signed off by the architecture team. Can we get that on the agenda for Friday's sync?" },
15
+ { "raw": "hi tom please find attached the q two performance summary key highlights are revenue up twelve percent and churn down to three point two percent happy to walk through on a call", "clean": "Hi Tom, please find attached the Q2 performance summary. Key highlights are revenue up 12% and churn down to 3.2%. Happy to walk through on a call." },
16
+ { "raw": "good morning everyone weekly update for the platform team we shipped the new auth flow on monday started work on the billing redesign tuesday and we we resolved the redis outage by wednesday afternoon blockers remain on the analytics dashboard pending design review", "clean": "Good morning everyone, weekly update for the platform team. We shipped the new auth flow on Monday, started work on the billing redesign Tuesday, and we resolved the Redis outage by Wednesday afternoon. Blockers remain on the analytics dashboard pending design review." },
17
+ { "raw": "appreciate the quick turnaround", "clean": "Appreciate the quick turnaround." },
18
+ { "raw": "ill circle back after the standup", "clean": "I'll circle back after the standup." },
19
+ { "raw": "noted thanks", "clean": "Noted, thanks." },
20
+ { "raw": "hey ben do you have bandwidth this sprint to take the stripe webhook work", "clean": "Hey Ben, do you have bandwidth this sprint to take the Stripe webhook work?" },
21
+ { "raw": "hi emma unfortunately i wont be able to make the two pm tomorrow could we move it to wednesday morning instead", "clean": "Hi Emma, unfortunately I won't be able to make the 2 PM tomorrow. Could we move it to Wednesday morning instead?" },
22
+ { "raw": "um following up on the proposal we sent last tuesday wanted to check if you had any questions or feedback before we move forward with scoping", "clean": "Following up on the proposal we sent last Tuesday. Wanted to check if you had any questions or feedback before we move forward with scoping." },
23
+ { "raw": "so i i think we should reconsider the timeline because the design dependencies are still in flux can we sync tomorrow to align", "clean": "I think we should reconsider the timeline because the design dependencies are still in flux. Can we sync tomorrow to align?" },
24
+ { "raw": "hi all sharing the meeting notes from this mornings roadmap review key decisions one we will deprioritize the mobile rewrite two q three focus is enterprise sso three we need to hire two more backend engineers please reply with corrections by end of day", "clean": "Hi all, sharing the meeting notes from this morning's roadmap review. Key decisions: 1) we will deprioritize the mobile rewrite, 2) Q3 focus is enterprise SSO, 3) we need to hire two more backend engineers. Please reply with corrections by end of day." },
25
+ { "raw": "hi anna could you double check the invoice from contoso the amount looks higher than we agreed", "clean": "Hi Anna, could you double check the invoice from Contoso? The amount looks higher than we agreed." },
26
+ { "raw": "sounds good lets lock it in", "clean": "Sounds good, let's lock it in." },
27
+ { "raw": "ill be five minutes late to the standup", "clean": "I'll be five minutes late to the standup." },
28
+ { "raw": "hi david wanted to share a quick update on the kubernetes migration we completed the staging cutover yesterday and production is scheduled for next thursday let me know if you have concerns", "clean": "Hi David, wanted to share a quick update on the Kubernetes migration. We completed the staging cutover yesterday and production is scheduled for next Thursday. Let me know if you have concerns." },
29
+ { "raw": "thanks for flagging that ill take a look this afternoon and circle back", "clean": "Thanks for flagging that. I'll take a look this afternoon and circle back." },
30
+ { "raw": "hi linda apologies for the delayed response um i was out sick yesterday to answer your question yes we can support the additional skus for the holiday campaign", "clean": "Hi Linda, apologies for the delayed response, I was out sick yesterday. To answer your question, yes, we can support the additional SKUs for the holiday campaign." },
31
+ { "raw": "can you forward me the latest deck", "clean": "Can you forward me the latest deck?" },
32
+ { "raw": "hi karthik just confirming receipt of the contract ill review with legal and get back to you by end of week", "clean": "Hi Karthik, just confirming receipt of the contract. I'll review with legal and get back to you by end of week." },
33
+ { "raw": "um so quick clarification on the spec when you say real time do you mean sub second or under five seconds because that materially affects the architecture", "clean": "Quick clarification on the spec. When you say real time, do you mean sub-second or under five seconds? Because that materially affects the architecture." },
34
+ { "raw": "hi jordan ill have to decline the fridays product sync due to a conflict with the customer advisory board could someone share notes afterward thanks", "clean": "Hi Jordan, I'll have to decline the Friday's product sync due to a conflict with the Customer Advisory Board. Could someone share notes afterward? Thanks." },
35
+ { "raw": "so um as discussed in our one on one ill be taking over the onboarding workstream from monday id like to start by reviewing the existing documentation and shadowing a few sessions before making any changes", "clean": "As discussed in our one-on-one, I'll be taking over the onboarding workstream from Monday. I'd like to start by reviewing the existing documentation and shadowing a few sessions before making any changes." },
36
+ { "raw": "great work on the launch yesterday team really impressive execution", "clean": "Great work on the launch yesterday team. Really impressive execution." },
37
+ { "raw": "hi ravi the figma file you shared looks great just one note on the checkout flow the cta button should use our primary brand color not the secondary teal", "clean": "Hi Ravi, the Figma file you shared looks great. Just one note on the checkout flow: the CTA button should use our primary brand color, not the secondary teal." },
38
+ { "raw": "um can you remind me whats the deadline for the okrs draft", "clean": "Can you remind me, what's the deadline for the OKRs draft?" },
39
+ { "raw": "hi maria following up on our conversation at the conference last week id love to set up thirty minutes to explore how acme and northwind might collaborate on the data residency offering are you free tuesday or wednesday next week", "clean": "Hi Maria, following up on our conversation at the conference last week. I'd love to set up 30 minutes to explore how Acme and Northwind might collaborate on the data residency offering. Are you free Tuesday or Wednesday next week?" },
40
+ { "raw": "ill take the action item to draft the rfc", "clean": "I'll take the action item to draft the RFC." },
41
+ { "raw": "hi steve um you know i wanted to flag that the the deliverables for sprint twenty three are at risk we lost two engineers to the incident response this week so were probably going to slip the search refactor by about three days", "clean": "Hi Steve, I wanted to flag that the deliverables for sprint 23 are at risk. We lost two engineers to the incident response this week, so we're probably going to slip the search refactor by about three days." },
42
+ { "raw": "thanks ill review and get back to you by tomorrow noon", "clean": "Thanks, I'll review and get back to you by tomorrow noon." },
43
+ { "raw": "hi nina could you share the latest customer health scores for the enterprise segment id like to bring some data points to the qbr on monday", "clean": "Hi Nina, could you share the latest customer health scores for the Enterprise segment? I'd like to bring some data points to the QBR on Monday." },
44
+ { "raw": "um so the the legal team came back with comments on the msa most are minor but they want to renegotiate the liability cap which could push our close date by two weeks lets discuss on tomorrows call", "clean": "The legal team came back with comments on the MSA. Most are minor, but they want to renegotiate the liability cap, which could push our close date by two weeks. Let's discuss on tomorrow's call." },
45
+ { "raw": "hi laura ill be working from the london office next week so my hours will shift accordingly please send any urgent items via slack", "clean": "Hi Laura, I'll be working from the London office next week, so my hours will shift accordingly. Please send any urgent items via Slack." },
46
+ { "raw": "ack will do", "clean": "Ack, will do." },
47
+ { "raw": "no problem ill resend the calendar invite with the correct dial in", "clean": "No problem, I'll resend the calendar invite with the correct dial-in." },
48
+ { "raw": "hi felipe wanted to share that weve decided to go with the dataworks proposal over snowmark thanks for considering us on the alternative path well revisit in q one", "clean": "Hi Felipe, wanted to share that we've decided to go with the DataWorks proposal over SnowMark. Thanks for considering us on the alternative path. We'll revisit in Q1." },
49
+ { "raw": "hi everyone um just a reminder that the the all hands is moving from thursday at three to wednesday at four this week due to the leadership offsite calendar invites have been updated accordingly", "clean": "Hi everyone, just a reminder that the all-hands is moving from Thursday at 3 to Wednesday at 4 this week due to the leadership offsite. Calendar invites have been updated accordingly." },
50
+ { "raw": "hi diego please find the latest sow attached weve incorporated your feedback on milestones two and three and adjusted the payment schedule to net thirty as requested looking forward to your countersignature", "clean": "Hi Diego, please find the latest SOW attached. We've incorporated your feedback on milestones two and three and adjusted the payment schedule to net 30 as requested. Looking forward to your countersignature." },
51
+ { "raw": "wanted to give you a heads up", "clean": "Wanted to give you a heads up." },
52
+ { "raw": "hi joel can you take a quick look at the pull request when you have a minute its blocking the release", "clean": "Hi Joel, can you take a quick look at the pull request when you have a minute? It's blocking the release." },
53
+ { "raw": "thanks for the thorough review i agree with most of the comments but i pushed back on point three because i think the existing pattern is more consistent happy to discuss live if helpful", "clean": "Thanks for the thorough review. I agree with most of the comments, but I pushed back on point three because I think the existing pattern is more consistent. Happy to discuss live if helpful." },
54
+ { "raw": "hi tara um wanted to clarify the scope for the audit project does it include the legacy crm or are we only looking at the new platform this changes the level of effort significantly", "clean": "Hi Tara, wanted to clarify the scope for the audit project. Does it include the legacy CRM, or are we only looking at the new platform? This changes the level of effort significantly." },
55
+ { "raw": "ok confirmed for monday at nine", "clean": "OK, confirmed for Monday at 9." },
56
+ { "raw": "hi greg sharing the postmortem doc from yesterdays incident root cause was a misconfigured iam policy on the staging account remediation steps are listed at the bottom please review and add comments by friday", "clean": "Hi Greg, sharing the postmortem doc from yesterday's incident. Root cause was a misconfigured IAM policy on the staging account. Remediation steps are listed at the bottom. Please review and add comments by Friday." },
57
+ { "raw": "hi olivia i i want to flag that we we may need to delay the launch by a week because the security review hasnt been completed and we cant ship without sign off from infosec ill push for a faster turnaround but wanted to set expectations early", "clean": "Hi Olivia, I want to flag that we may need to delay the launch by a week because the security review hasn't been completed and we can't ship without sign off from InfoSec. I'll push for a faster turnaround, but wanted to set expectations early." },
58
+ { "raw": "yes that works for me", "clean": "Yes, that works for me." },
59
+ { "raw": "hi raj congratulations on closing the takashi deal that was huge", "clean": "Hi Raj, congratulations on closing the Takashi deal. That was huge." },
60
+ { "raw": "hi sam um a quick question on expensing the offsite is dinner on day two covered by the company card or should i put it on my personal card and submit", "clean": "Hi Sam, a quick question on expensing the offsite. Is dinner on day two covered by the company card, or should I put it on my personal card and submit?" },
61
+ { "raw": "thanks for organizing the workshop yesterday the breakout on customer segmentation was particularly valuable ill share my notes with the broader team this afternoon", "clean": "Thanks for organizing the workshop yesterday. The breakout on customer segmentation was particularly valuable. I'll share my notes with the broader team this afternoon." },
62
+ { "raw": "hi natalie introducing you to peter our new head of solutions engineering peter natalie is the cto at globex and an excellent thought partner on platform architecture ill let you two take it from here", "clean": "Hi Natalie, introducing you to Peter, our new head of solutions engineering. Peter, Natalie is the CTO at Globex and an excellent thought partner on platform architecture. I'll let you two take it from here." },
63
+ { "raw": "um so we we need to align on the messaging for the press release before friday because comms wants to lock the final draft by end of day i can send over the latest version this afternoon for your eyes", "clean": "We need to align on the messaging for the press release before Friday because comms wants to lock the final draft by end of day. I can send over the latest version this afternoon for your eyes." },
64
+ { "raw": "hi mark a quick reminder that perf reviews are due by next wednesday please complete your self assessment in workday before then", "clean": "Hi Mark, a quick reminder that perf reviews are due by next Wednesday. Please complete your self assessment in Workday before then." },
65
+ { "raw": "lets table that for the offsite", "clean": "Let's table that for the offsite." },
66
+ { "raw": "hi audrey unfortunately i need to push our coffee chat to next week i have a customer escalation that needs my attention today and tomorrow apologies for the late notice", "clean": "Hi Audrey, unfortunately I need to push our coffee chat to next week. I have a customer escalation that needs my attention today and tomorrow. Apologies for the late notice." },
67
+ { "raw": "hi finance team can you confirm whether the pmo budget code is still active for q three im trying to submit a vendor invoice and the system is rejecting it", "clean": "Hi finance team, can you confirm whether the PMO budget code is still active for Q3? I'm trying to submit a vendor invoice and the system is rejecting it." },
68
+ { "raw": "um so quick update on the hiring pipeline weve got three strong candidates in final rounds for the staff engineer role expecting to extend offers by next friday let me know if you want to be a part of the debriefs", "clean": "Quick update on the hiring pipeline. We've got three strong candidates in final rounds for the staff engineer role, expecting to extend offers by next Friday. Let me know if you want to be a part of the debriefs." },
69
+ { "raw": "hi katie thanks for jumping on the call earlier as a recap your team will own the data ingestion piece and ours will own the dashboard layer ill draft the rasic and share by tomorrow", "clean": "Hi Katie, thanks for jumping on the call earlier. As a recap, your team will own the data ingestion piece and ours will own the dashboard layer. I'll draft the RASIC and share by tomorrow." },
70
+ { "raw": "hi henry youre right that the the dashboard query is slow ive opened a ticket with the data platform team and they should have a fix in by end of next week ill keep you posted on progress", "clean": "Hi Henry, you're right that the dashboard query is slow. I've opened a ticket with the data platform team and they should have a fix in by end of next week. I'll keep you posted on progress." },
71
+ { "raw": "happy friday everyone heres your weekly roundup deployments stable nps trending up two points new customer wins include vortex and lumina shoutout to the support team for closing out the backlog this week", "clean": "Happy Friday everyone, here's your weekly roundup. Deployments stable, NPS trending up two points, new customer wins include Vortex and Lumina. Shoutout to the support team for closing out the backlog this week." },
72
+ { "raw": "hi paul can we move the budget review from twelve to one im double booked", "clean": "Hi Paul, can we move the budget review from 12 to 1? I'm double booked." },
73
+ { "raw": "hi sandra um sharing the draft press release for tomorrows announcement please review and send any comments by six pm tonight im looping in legal and comms for final sign off", "clean": "Hi Sandra, sharing the draft press release for tomorrow's announcement. Please review and send any comments by 6 PM tonight. I'm looping in legal and comms for final sign off." },
74
+ { "raw": "got it ill update the ticket", "clean": "Got it, I'll update the ticket." },
75
+ { "raw": "hi eric im handing off the integration work to nadia effective monday shes already up to speed on the the technical details and ive scheduled a kickoff with her and your team for next tuesday at ten am please reach out to her for any new requests going forward", "clean": "Hi Eric, I'm handing off the integration work to Nadia effective Monday. She's already up to speed on the technical details, and I've scheduled a kickoff with her and your team for next Tuesday at 10 AM. Please reach out to her for any new requests going forward." },
76
+ { "raw": "thanks again for your patience while we sort this out", "clean": "Thanks again for your patience while we sort this out." },
77
+ { "raw": "hi simone i i think your analysis missed the seasonality adjustment if you reapply the the q four multiplier i believe the variance closes can you rerun and send me the updated numbers before the steerco", "clean": "Hi Simone, I think your analysis missed the seasonality adjustment. If you reapply the Q4 multiplier, I believe the variance closes. Can you rerun and send me the updated numbers before the SteerCo?" },
78
+ { "raw": "hi ian short note to confirm that we approved your travel request to the prague office for the june workshop please book through the corporate portal", "clean": "Hi Ian, short note to confirm that we approved your travel request to the Prague office for the June workshop. Please book through the corporate portal." },
79
+ { "raw": "um following up please let me know your availability next week", "clean": "Following up, please let me know your availability next week." },
80
+ { "raw": "hi everyone reminder that the q two okr grading session is on thursday at two pm please come prepared with your final scores and supporting evidence", "clean": "Hi everyone, reminder that the Q2 OKR grading session is on Thursday at 2 PM. Please come prepared with your final scores and supporting evidence." },
81
+ { "raw": "hi adrian appreciate the proposal but were going to pass for now budget constraints this quarter mean we cant take on additional vendors until january please keep us on your radar for next year", "clean": "Hi Adrian, appreciate the proposal but we're going to pass for now. Budget constraints this quarter mean we can't take on additional vendors until January. Please keep us on your radar for next year." }
82
+ ]
data/seed/batches/questions_and_asks.json ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "raw": "can you send me the deck", "clean": "Can you send me the deck?" },
3
+ { "raw": "whats the eta on the prod fix", "clean": "What's the ETA on the prod fix?" },
4
+ { "raw": "did the build pass", "clean": "Did the build pass?" },
5
+ { "raw": "any update from sarah", "clean": "Any update from Sarah?" },
6
+ { "raw": "wheres the figma file", "clean": "Where's the Figma file?" },
7
+ { "raw": "um quick question who owns the billing service these days", "clean": "Quick question: who owns the billing service these days?" },
8
+ { "raw": "hey can you uh share the postgres connection string with me i cant find it in vault", "clean": "Hey, can you share the Postgres connection string with me? I can't find it in Vault." },
9
+ { "raw": "so like whats the plan for the friday demo are we doing it live or recorded", "clean": "What's the plan for the Friday demo? Are we doing it live or recorded?" },
10
+ { "raw": "do you know if marcus is back from pto i needed his eyes on the auth flow", "clean": "Do you know if Marcus is back from PTO? I needed his eyes on the auth flow." },
11
+ { "raw": "could you um pull the latest analytics dump for q two and uh drop it in the shared drive", "clean": "Could you pull the latest analytics dump for Q2 and drop it in the shared drive?" },
12
+ { "raw": "whats the status on the stripe webhook fix and are we still seeing failed retries", "clean": "What's the status on the Stripe webhook fix? Are we still seeing failed retries?" },
13
+ { "raw": "hey priya can you walk me through how the rate limiter works i want to understand the token bucket part", "clean": "Hey Priya, can you walk me through how the rate limiter works? I want to understand the token bucket part." },
14
+ { "raw": "do you think we can ship by friday or should we push it to next sprint", "clean": "Do you think we can ship by Friday, or should we push it to next sprint?" },
15
+ { "raw": "uh quick favor can you review my pr its the one for the dashboard refactor", "clean": "Quick favor: can you review my PR? It's the one for the dashboard refactor." },
16
+ { "raw": "you know whats weird the api is returning four oh four for some users can you check the logs", "clean": "You know what's weird? The API is returning 404 for some users. Can you check the logs?" },
17
+ { "raw": "do we have a runbook for the kafka outage scenario or do i need to write one", "clean": "Do we have a runbook for the Kafka outage scenario, or do I need to write one?" },
18
+ { "raw": "hey um so i was wondering uh could you introduce me to the person who runs platform i think their name is dan", "clean": "Hey, I was wondering, could you introduce me to the person who runs platform? I think their name is Dan." },
19
+ { "raw": "is the staging environment up i keep getting connection refused", "clean": "Is the staging environment up? I keep getting connection refused." },
20
+ { "raw": "what time does the standup start tomorrow", "clean": "What time does the standup start tomorrow?" },
21
+ { "raw": "where do i find the onboarding docs for new hires", "clean": "Where do I find the onboarding docs for new hires?" },
22
+ { "raw": "can you give me a rough estimate on how long the postgres upgrade will take like ballpark", "clean": "Can you give me a rough estimate on how long the Postgres upgrade will take? Like a ballpark?" },
23
+ { "raw": "so like who do i talk to about getting access to the snowflake warehouse", "clean": "Who do I talk to about getting access to the Snowflake warehouse?" },
24
+ { "raw": "uh did anyone uh look at the alert that fired at three am last night", "clean": "Did anyone look at the alert that fired at 3 AM last night?" },
25
+ { "raw": "hey can you forward me the calendar invite for the q three planning meeting", "clean": "Hey, can you forward me the calendar invite for the Q3 planning meeting?" },
26
+ { "raw": "do you have like a few minutes today to pair on the redis caching issue im stuck on", "clean": "Do you have a few minutes today to pair on the Redis caching issue? I'm stuck on." },
27
+ { "raw": "i mean is the api gateway behind cloudflare or is it direct to ec two right now", "clean": "Is the API gateway behind Cloudflare, or is it direct to EC2 right now?" },
28
+ { "raw": "could you uh share your thoughts on the architecture doc i sent yesterday i want to lock it down by eod", "clean": "Could you share your thoughts on the architecture doc I sent yesterday? I want to lock it down by EOD." },
29
+ { "raw": "whens the next release window i need to slot in the schema migration", "clean": "When's the next release window? I need to slot in the schema migration." },
30
+ { "raw": "hey jordan can you confirm the meeting at two thirty is still happening", "clean": "Hey Jordan, can you confirm the meeting at 2:30 is still happening?" },
31
+ { "raw": "do you know where the terraform state is stored for the dev cluster", "clean": "Do you know where the Terraform state is stored for the dev cluster?" },
32
+ { "raw": "uh whats the difference between the v two and v three endpoints im trying to figure out which one to call", "clean": "What's the difference between the v2 and v3 endpoints? I'm trying to figure out which one to call." },
33
+ { "raw": "so um can you check if the elastic cluster is healthy i think we lost a node", "clean": "Can you check if the Elastic cluster is healthy? I think we lost a node." },
34
+ { "raw": "any chance you could review my design doc before the meeting at four", "clean": "Any chance you could review my design doc before the meeting at 4?" },
35
+ { "raw": "wait do we have monitoring set up for the new lambda or is it flying blind right now", "clean": "Wait, do we have monitoring set up for the new Lambda, or is it flying blind right now?" },
36
+ { "raw": "could someone please uh take a look at ticket eight nine four two its been open for like two weeks", "clean": "Could someone please take a look at ticket 8942? It's been open for two weeks." },
37
+ { "raw": "is there a reason why we picked dynamodb over postgres for this i want to understand the tradeoff", "clean": "Is there a reason why we picked DynamoDB over Postgres for this? I want to understand the tradeoff." },
38
+ { "raw": "hey can you um drop a link to the slack channel for the incident response team", "clean": "Hey, can you drop a link to the Slack channel for the incident response team?" },
39
+ { "raw": "whens your earliest availability this week for a thirty minute sync on the launch", "clean": "When's your earliest availability this week for a 30-minute sync on the launch?" },
40
+ { "raw": "are we still on for the one on one tomorrow or did you want to reschedule", "clean": "Are we still on for the 1:1 tomorrow, or did you want to reschedule?" },
41
+ { "raw": "uh do you have the the latest numbers for daily active users", "clean": "Do you have the latest numbers for daily active users?" },
42
+ { "raw": "hey could you send over the contract draft i need to forward it to legal by tomorrow morning", "clean": "Hey, could you send over the contract draft? I need to forward it to legal by tomorrow morning." },
43
+ { "raw": "whats the the recommended way to handle retries when calling the payments api", "clean": "What's the recommended way to handle retries when calling the payments API?" },
44
+ { "raw": "so um like how does the the new feature flag system work i havent used it before", "clean": "How does the new feature flag system work? I haven't used it before." },
45
+ { "raw": "can you ping me the access credentials for the grafana dashboard", "clean": "Can you ping me the access credentials for the Grafana dashboard?" },
46
+ { "raw": "uh is anyone working on the the search performance issue or should i pick it up", "clean": "Is anyone working on the search performance issue, or should I pick it up?" },
47
+ { "raw": "hey rohan when youre free could you take like ten minutes to explain the okta sso integration to me", "clean": "Hey Rohan, when you're free, could you take 10 minutes to explain the Okta SSO integration to me?" },
48
+ { "raw": "do you know if were sending the the verification emails through sendgrid or ses these days", "clean": "Do you know if we're sending the verification emails through SendGrid or SES these days?" },
49
+ { "raw": "uh question how do we handle uh database backups for the multi region setup", "clean": "Question: how do we handle database backups for the multi-region setup?" },
50
+ { "raw": "could you take a look at this stack trace when you get a chance i cant tell if its a null pointer or a race condition", "clean": "Could you take a look at this stack trace when you get a chance? I can't tell if it's a null pointer or a race condition." },
51
+ { "raw": "hey can you share your screen for a sec i want to see how you set up the the docker compose file", "clean": "Hey, can you share your screen for a sec? I want to see how you set up the Docker Compose file." },
52
+ { "raw": "wheres the link for the the all hands today", "clean": "Where's the link for the all hands today?" },
53
+ { "raw": "do you have feedback on the prd i shared last night", "clean": "Do you have feedback on the PRD I shared last night?" },
54
+ { "raw": "um could you introduce me to alex from the data team i want to ask about the etl pipeline", "clean": "Could you introduce me to Alex from the data team? I want to ask about the ETL pipeline." },
55
+ { "raw": "so so like whats the the policy on using ai tools for code review do we have a stance yet", "clean": "What's the policy on using AI tools for code review? Do we have a stance yet?" },
56
+ { "raw": "hey uh quick one are you the right person to ask about the the vpn config or should i ping someone else", "clean": "Hey, quick one: are you the right person to ask about the VPN config, or should I ping someone else?" },
57
+ { "raw": "can you check if the prod deploy actually went through im not seeing the new endpoint", "clean": "Can you check if the prod deploy actually went through? I'm not seeing the new endpoint." },
58
+ { "raw": "whats the the timeline looking like for the ios release are we still targeting june fifteenth", "clean": "What's the timeline looking like for the iOS release? Are we still targeting June 15?" },
59
+ { "raw": "i mean do we know why the the build is taking like forty minutes now it used to be ten", "clean": "Do we know why the build is taking 40 minutes now? It used to be 10." },
60
+ { "raw": "hey could you uh send me the link to the the customer interview notes from last week", "clean": "Hey, could you send me the link to the customer interview notes from last week?" },
61
+ { "raw": "um is the the staging db a copy of prod or is it seeded with fake data", "clean": "Is the staging DB a copy of prod, or is it seeded with fake data?" },
62
+ { "raw": "do you think we should use react query or swr for the new dashboard whats your take", "clean": "Do you think we should use React Query or SWR for the new dashboard? What's your take?" },
63
+ { "raw": "can you remind me whens the cutoff for performance reviews this cycle", "clean": "Can you remind me when's the cutoff for performance reviews this cycle?" },
64
+ { "raw": "hey uh could you walk me through the the oauth flow for the partner integration im implementing it tomorrow", "clean": "Hey, could you walk me through the OAuth flow for the partner integration? I'm implementing it tomorrow." },
65
+ { "raw": "is there a a reason were still on node sixteen i thought we were upgrading to twenty last quarter", "clean": "Is there a reason we're still on Node 16? I thought we were upgrading to 20 last quarter." },
66
+ { "raw": "so um whens your team going to be done with the the migration off mongodb i need to plan around it", "clean": "When's your team going to be done with the migration off MongoDB? I need to plan around it." },
67
+ { "raw": "could you spare like fifteen minutes this afternoon to debug this typescript error with me its driving me crazy", "clean": "Could you spare 15 minutes this afternoon to debug this TypeScript error with me? It's driving me crazy." },
68
+ { "raw": "hey uh whos the the right point of contact for the the security review process im starting a new project", "clean": "Hey, who's the right point of contact for the security review process? I'm starting a new project." },
69
+ { "raw": "do you know if we have a a slack bot for paging on call or do i need to use pagerduty directly", "clean": "Do you know if we have a Slack bot for paging on-call, or do I need to use PagerDuty directly?" },
70
+ { "raw": "can you double check that the the s three bucket is set to private im paranoid we leaked something", "clean": "Can you double-check that the S3 bucket is set to private? I'm paranoid we leaked something." },
71
+ { "raw": "wheres your team standing on the the q two roadmap are we tracking green or are there blockers i should know about", "clean": "Where's your team standing on the Q2 roadmap? Are we tracking green, or are there blockers I should know about?" }
72
+ ]
data/seed/batches/technical_dictation.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "raw": "the mutex is held across the await point which deadlocks tokio", "clean": "The mutex is held across the await point, which deadlocks Tokio." },
3
+ { "raw": "um getUserById returns null when the id is negative we need to handle that", "clean": "getUserById returns null when the id is negative. We need to handle that." },
4
+ { "raw": "wrap the call in a try catch", "clean": "Wrap the call in a try-catch." },
5
+ { "raw": "so the the postgres migration adds a unique index on email but theres already duplicate rows so it fails", "clean": "The Postgres migration adds a unique index on email, but there are already duplicate rows, so it fails." },
6
+ { "raw": "the websocket reconnect logic uses exponential backoff capped at thirty seconds", "clean": "The websocket reconnect logic uses exponential backoff capped at 30 seconds." },
7
+ { "raw": "i think the the race condition is in the cache invalidation when two writers hit at the same time you know one overwrites the other and we lose the second update", "clean": "The race condition is in the cache invalidation. When two writers hit at the same time, one overwrites the other and we lose the second update." },
8
+ { "raw": "use sha256 not md5", "clean": "Use SHA-256, not MD5." },
9
+ { "raw": "the jwt expiry is set to fifteen minutes but the refresh token lasts seven days", "clean": "The JWT expiry is set to 15 minutes, but the refresh token lasts 7 days." },
10
+ { "raw": "uh the useeffect runs twice in strict mode thats expected in react eighteen and nineteen", "clean": "The useEffect runs twice in strict mode. That's expected in React 18 and 19." },
11
+ { "raw": "we we should switch from axios to fetch since were already polyfilling it", "clean": "We should switch from axios to fetch since we're already polyfilling it." },
12
+ { "raw": "the the onnx runtime session is leaking memory because we never call release on it", "clean": "The ONNX Runtime session is leaking memory because we never call release on it." },
13
+ { "raw": "um so the the design tradeoff here is we either we either denormalize for read speed or we keep the join and pay the latency cost every request i mean for our access pattern denormalization wins", "clean": "The design tradeoff here is we either denormalize for read speed or we keep the join and pay the latency cost every request. For our access pattern, denormalization wins." },
14
+ { "raw": "add a debounce of three hundred milliseconds on the search input", "clean": "Add a debounce of 300 milliseconds on the search input." },
15
+ { "raw": "the parakeet model expects sixteen kilohertz mono pcm not stereo", "clean": "The Parakeet model expects 16kHz mono PCM, not stereo." },
16
+ { "raw": "well the the the deserializer panics on empty arrays because we typed it as non empty vec instead of vec", "clean": "The deserializer panics on empty arrays because we typed it as NonEmptyVec instead of Vec." },
17
+ { "raw": "rename the function to parseHotkeyConfig its more descriptive", "clean": "Rename the function to parseHotkeyConfig. It's more descriptive." },
18
+ { "raw": "the api returns four oh three when the bearer token is malformed not four oh one which is wrong per rfc seventy two thirty five", "clean": "The API returns 403 when the bearer token is malformed, not 401, which is wrong per RFC 7235." },
19
+ { "raw": "uh so we should add idempotency keys on the post endpoint otherwise retries create duplicate orders", "clean": "We should add idempotency keys on the POST endpoint, otherwise retries create duplicate orders." },
20
+ { "raw": "the fastapi dependency injection resolves dependencies per request so caching the db session there means we lose connection pooling benefits", "clean": "The FastAPI dependency injection resolves dependencies per request, so caching the DB session there means we lose connection pooling benefits." },
21
+ { "raw": "switch to pnpm workspaces", "clean": "Switch to pnpm workspaces." },
22
+ { "raw": "the the tauri command is async but the frontend invoke isnt awaiting the promise", "clean": "The Tauri command is async, but the frontend invoke isn't awaiting the promise." },
23
+ { "raw": "i mean the the the schema migration is non backward compatible we drop the legacy column in step three but step two reads it so we need to deploy them in separate releases", "clean": "The schema migration is non-backward-compatible. We drop the legacy column in step three, but step two reads it, so we need to deploy them in separate releases." },
24
+ { "raw": "log the request id on every span", "clean": "Log the request ID on every span." },
25
+ { "raw": "the lambda cold start is around eight hundred milliseconds because of the bundle size", "clean": "The Lambda cold start is around 800 milliseconds because of the bundle size." },
26
+ { "raw": "um the rust borrow checker is complaining because were holding a mutable reference to self while calling a method that takes self mutably", "clean": "The Rust borrow checker is complaining because we're holding a mutable reference to self while calling a method that takes self mutably." },
27
+ { "raw": "use serdejson not simdjson the perf difference doesnt matter here", "clean": "Use serde_json, not simd-json. The perf difference doesn't matter here." },
28
+ { "raw": "the the ci pipeline times out at twenty minutes mostly because the integration tests run sequentially we should parallelize with jest workers", "clean": "The CI pipeline times out at 20 minutes, mostly because the integration tests run sequentially. We should parallelize with Jest workers." },
29
+ { "raw": "drop the index on createdAt its never used in our query plans", "clean": "Drop the index on createdAt. It's never used in our query plans." },
30
+ { "raw": "the the the oauth callback url has to match exactly including the trailing slash otherwise google rejects with redirect uri mismatch", "clean": "The OAuth callback URL has to match exactly, including the trailing slash, otherwise Google rejects with redirect_uri_mismatch." },
31
+ { "raw": "we need to add mTLS between the api gateway and the upstream service", "clean": "We need to add mTLS between the API gateway and the upstream service." },
32
+ { "raw": "the the websocket frame is getting truncated at sixty four kilobytes because the upstream proxy buffer is too small", "clean": "The websocket frame is getting truncated at 64 kilobytes because the upstream proxy buffer is too small." },
33
+ { "raw": "uh extract the audio resampler into its own crate", "clean": "Extract the audio resampler into its own crate." },
34
+ { "raw": "the django orm is generating n plus one queries on the related manager we need to add a select related or a prefetch related", "clean": "The Django ORM is generating N+1 queries on the related manager. We need to add a select_related or a prefetch_related." },
35
+ { "raw": "um well the the the issue is the websocket upgrade fails behind cloudflare unless we disable rocket loader for that route and also bump the keepalive timeout on the origin", "clean": "The issue is the websocket upgrade fails behind Cloudflare unless we disable Rocket Loader for that route and also bump the keepalive timeout on the origin." },
36
+ { "raw": "add a retry with jitter", "clean": "Add a retry with jitter." },
37
+ { "raw": "the convex mutation runs in a transaction so we cant call external apis from it", "clean": "The Convex mutation runs in a transaction, so we can't call external APIs from it." },
38
+ { "raw": "so the the s3 multipart upload needs parts to be at least five megabytes except for the last one", "clean": "The S3 multipart upload needs parts to be at least 5 megabytes, except for the last one." },
39
+ { "raw": "rewrite this in zod its safer than manual type guards", "clean": "Rewrite this in Zod. It's safer than manual type guards." },
40
+ { "raw": "uh the the redis lock is being released by a different process than the one that acquired it because were not checking the token before del so use the lua script for atomic compare and delete", "clean": "The Redis lock is being released by a different process than the one that acquired it because we're not checking the token before DEL. Use the Lua script for atomic compare-and-delete." },
41
+ { "raw": "the test fails because the mock returns undefined not null", "clean": "The test fails because the mock returns undefined, not null." },
42
+ { "raw": "well i mean the the perf regression came from the new react server component because its now making a fresh database connection per render instead of reusing the pool", "clean": "The perf regression came from the new React server component because it's now making a fresh database connection per render instead of reusing the pool." },
43
+ { "raw": "use struct of arrays for the audio buffer not array of structs", "clean": "Use struct-of-arrays for the audio buffer, not array-of-structs." },
44
+ { "raw": "the the the kafka consumer lag is climbing because the downstream sink is doing synchronous writes to elasticsearch we should batch them every two seconds or every five hundred records whichever comes first", "clean": "The Kafka consumer lag is climbing because the downstream sink is doing synchronous writes to Elasticsearch. We should batch them every 2 seconds or every 500 records, whichever comes first." },
45
+ { "raw": "the the type inference fails on the generic so we need an explicit annotation", "clean": "The type inference fails on the generic, so we need an explicit annotation." },
46
+ { "raw": "use pinia not vuex", "clean": "Use Pinia, not Vuex." },
47
+ { "raw": "uh the prisma client doesnt support raw json operators on sqlite so we need to fall back to a raw query for that filter", "clean": "The Prisma client doesn't support raw JSON operators on SQLite, so we need to fall back to a raw query for that filter." },
48
+ { "raw": "set max old space size to four gigabytes for the build", "clean": "Set max-old-space-size to 4 gigabytes for the build." },
49
+ { "raw": "so the the the deployment failed because the helm chart references an image tag that doesnt exist in ecr the ci pushed to the wrong region", "clean": "The deployment failed because the Helm chart references an image tag that doesn't exist in ECR. The CI pushed to the wrong region." },
50
+ { "raw": "the the pytorch dataloader is bottlenecked on cpu because num workers is set to zero bump it to eight", "clean": "The PyTorch DataLoader is bottlenecked on CPU because num_workers is set to 0. Bump it to 8." },
51
+ { "raw": "validate the request body with pydantic", "clean": "Validate the request body with Pydantic." },
52
+ { "raw": "um the the global state in zustand is fine for this its overkill to bring in redux", "clean": "The global state in Zustand is fine for this. It's overkill to bring in Redux." },
53
+ { "raw": "we we need to add a circuit breaker around the stripe api calls because their occasional five hundred is taking down our checkout flow", "clean": "We need to add a circuit breaker around the Stripe API calls because their occasional 500 is taking down our checkout flow." },
54
+ { "raw": "the the the typescript compiler is slow because we have too many barrel files split them up", "clean": "The TypeScript compiler is slow because we have too many barrel files. Split them up." },
55
+ { "raw": "use httpClient.get with a timeout of five seconds", "clean": "Use HTTPClient.get with a timeout of 5 seconds." },
56
+ { "raw": "uh well the the cors preflight is failing because we dont return access control allow credentials on the options response", "clean": "The CORS preflight is failing because we don't return Access-Control-Allow-Credentials on the OPTIONS response." },
57
+ { "raw": "the docker image is one point two gigabytes use a distroless base", "clean": "The Docker image is 1.2 gigabytes. Use a distroless base." },
58
+ { "raw": "so the the indexeddb transaction auto commits when control returns to the event loop so dont await anything other than indexeddb operations inside it", "clean": "The IndexedDB transaction auto-commits when control returns to the event loop, so don't await anything other than IndexedDB operations inside it." },
59
+ { "raw": "the the unwrap panics when the env var is missing use expect with a message or better yet ok or", "clean": "The unwrap panics when the env var is missing. Use expect with a message, or better yet, ok_or." },
60
+ { "raw": "add a finally block to close the cursor", "clean": "Add a finally block to close the cursor." },
61
+ { "raw": "um the the supabase rls policy doesnt apply to service role keys so the test that uses the admin client passes but the real client gets denied", "clean": "The Supabase RLS policy doesn't apply to service role keys, so the test that uses the admin client passes, but the real client gets denied." },
62
+ { "raw": "the worker thread doesnt have access to the dom so we cant use document there", "clean": "The worker thread doesn't have access to the DOM, so we can't use document there." },
63
+ { "raw": "i mean the the gpu memory fragments after about two hours of inference because we keep allocating different sized batches we should pad to a fixed shape", "clean": "The GPU memory fragments after about 2 hours of inference because we keep allocating different-sized batches. We should pad to a fixed shape." },
64
+ { "raw": "use bytes not string for binary payloads in protobuf", "clean": "Use bytes, not string, for binary payloads in protobuf." },
65
+ { "raw": "the the the websocket heartbeat needs to be smaller than the load balancer idle timeout otherwise the connection gets killed mid session aws nlb defaults to three hundred fifty seconds", "clean": "The websocket heartbeat needs to be smaller than the load balancer idle timeout, otherwise the connection gets killed mid-session. AWS NLB defaults to 350 seconds." },
66
+ { "raw": "extract the formatter into a hook called useFormattedDate", "clean": "Extract the formatter into a hook called useFormattedDate." },
67
+ { "raw": "uh the the rust async fn in trait works on stable now but you need return position impl trait in trait for some patterns", "clean": "The Rust async fn in trait works on stable now, but you need return-position impl Trait in Trait for some patterns." },
68
+ { "raw": "log to stderr not stdout for cli tools", "clean": "Log to stderr, not stdout, for CLI tools." },
69
+ { "raw": "so the the the api spec says four twenty nine on rate limit but our middleware returns five oh three fix the status code", "clean": "The API spec says 429 on rate limit, but our middleware returns 503. Fix the status code." },
70
+ { "raw": "the the cron job runs every two thirty am but the timezone is utc not local so it actually fires at seven thirty am india time", "clean": "The cron job runs every 2:30 AM, but the timezone is UTC, not local, so it actually fires at 7:30 AM India time." },
71
+ { "raw": "memoize the selector with reselect", "clean": "Memoize the selector with reselect." },
72
+ { "raw": "um the the the gosh the panic message says index out of bounds because the slice is empty when the input file has no header row we should return an error instead of indexing zero", "clean": "The panic message says index out of bounds because the slice is empty when the input file has no header row. We should return an error instead of indexing zero." },
73
+ { "raw": "the postgres advisory lock is per session not per transaction", "clean": "The Postgres advisory lock is per session, not per transaction." },
74
+ { "raw": "uh use bun for the dev server it starts in like ninety milliseconds vs three seconds with node", "clean": "Use Bun for the dev server. It starts in about 90 milliseconds vs 3 seconds with Node." },
75
+ { "raw": "the the the python gil prevents true parallelism in cpu bound code so we should use multiprocessing or move the hot loop to a c extension or rust via pyo3", "clean": "The Python GIL prevents true parallelism in CPU-bound code, so we should use multiprocessing, or move the hot loop to a C extension, or Rust via PyO3." },
76
+ { "raw": "rename main.rs to lib.rs and add a bin folder", "clean": "Rename main.rs to lib.rs and add a bin folder." },
77
+ { "raw": "well the the swr cache key needs to include the user id otherwise different users see each others data after a route transition", "clean": "The SWR cache key needs to include the user ID, otherwise different users see each other's data after a route transition." },
78
+ { "raw": "the the sqlite database is locked because two writers are competing enable wal mode", "clean": "The SQLite database is locked because two writers are competing. Enable WAL mode." },
79
+ { "raw": "use struct unpacking for clarity", "clean": "Use struct unpacking for clarity." },
80
+ { "raw": "uh um so the the the proper fix is to move the side effect out of render into a useEffect with the right dependency array currently its running on every render which is why we see the api hammered", "clean": "The proper fix is to move the side effect out of render into a useEffect with the right dependency array. Currently it's running on every render, which is why we see the API hammered." },
81
+ { "raw": "the the rate limiter is token bucket not leaky bucket", "clean": "The rate limiter is token bucket, not leaky bucket." }
82
+ ]
data/seed/batches/todo_lists.json ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "raw": "pick up bread milk and eggs", "clean": "Pick up bread, milk, and eggs." },
3
+ { "raw": "first call alex then send the report to finance and finally book the flight to denver", "clean": "1. Call Alex\n2. Send the report to finance\n3. Book the flight to Denver" },
4
+ { "raw": "um remind me to grab coffee filters paper towels and dish soap from target", "clean": "Remind me to grab coffee filters, paper towels, and dish soap from Target." },
5
+ { "raw": "the three deliverables for q3 are the auth rewrite the new onboarding flow and the billing dashboard", "clean": "The three deliverables for Q3:\n- The auth rewrite\n- The new onboarding flow\n- The billing dashboard" },
6
+ { "raw": "tomorrow do the standup at nine then review prs then sync with maya at two then write the postmortem", "clean": "Tomorrow:\n1. Standup at 9\n2. Review PRs\n3. Sync with Maya at 2\n4. Write the postmortem" },
7
+ { "raw": "i need to pack socks underwear a charger and my passport", "clean": "I need to pack socks, underwear, a charger, and my passport." },
8
+ { "raw": "so the agenda for monday is uh first we do intros then we go through the roadmap then we open it up for questions", "clean": "The agenda for Monday:\n1. Intros\n2. Go through the roadmap\n3. Open it up for questions" },
9
+ { "raw": "buy aspirin tylenol and band aids from cvs", "clean": "Buy aspirin, Tylenol, and Band-Aids from CVS." },
10
+ { "raw": "uh the four candidates we shortlisted are priya marcus chen and aisha", "clean": "The four candidates we shortlisted:\n- Priya\n- Marcus\n- Chen\n- Aisha" },
11
+ { "raw": "i mean for the trip pack the tent the sleeping bag the camp stove and a water filter", "clean": "For the trip, pack the tent, the sleeping bag, the camp stove, and a water filter." },
12
+ { "raw": "first thing tomorrow ship the migration then write the launch post then ping the support team", "clean": "First thing tomorrow:\n1. Ship the migration\n2. Write the launch post\n3. Ping the support team" },
13
+ { "raw": "grab oat milk a bag of coffee and some bananas", "clean": "Grab oat milk, a bag of coffee, and some bananas." },
14
+ { "raw": "the the rooms we still need to paint are the guest bedroom the hallway and the laundry room", "clean": "The rooms we still need to paint:\n- The guest bedroom\n- The hallway\n- The laundry room" },
15
+ { "raw": "well first you boil the pasta then you brown the garlic then you add the cream and finally toss it all together", "clean": "1. Boil the pasta\n2. Brown the garlic\n3. Add the cream\n4. Toss it all together" },
16
+ { "raw": "todo items for friday review the figma file approve the contract and call back jordan", "clean": "Todo items for Friday:\n- Review the Figma file\n- Approve the contract\n- Call back Jordan" },
17
+ { "raw": "remember dentist at three pharmacy at four pickup kids at five thirty", "clean": "Remember:\n1. Dentist at 3\n2. Pharmacy at 4\n3. Pick up kids at 5:30" },
18
+ { "raw": "the the the things i love about the new macbook are the screen the battery and the keyboard", "clean": "The things I love about the new MacBook:\n- The screen\n- The battery\n- The keyboard" },
19
+ { "raw": "uh add tomatoes basil mozzarella and olive oil to the cart", "clean": "Add tomatoes, basil, mozzarella, and olive oil to the cart." },
20
+ { "raw": "for the demo we need to first introduce the problem then walk through the architecture then show the live demo and finally take questions", "clean": "For the demo:\n1. Introduce the problem\n2. Walk through the architecture\n3. Show the live demo\n4. Take questions" },
21
+ { "raw": "books i want to finish this month are dune neuromancer and the three body problem", "clean": "Books I want to finish this month:\n- Dune\n- Neuromancer\n- The Three-Body Problem" },
22
+ { "raw": "uh you know first wake up at six then run five miles then shower then make breakfast", "clean": "1. Wake up at 6\n2. Run five miles\n3. Shower\n4. Make breakfast" },
23
+ { "raw": "make sure to bring sunscreen a hat sunglasses and water", "clean": "Make sure to bring sunscreen, a hat, sunglasses, and water." },
24
+ { "raw": "the bugs to fix before friday are the login redirect the timezone issue in the calendar the slow query on the dashboard and the broken avatar upload", "clean": "Bugs to fix before Friday:\n- The login redirect\n- The timezone issue in the calendar\n- The slow query on the dashboard\n- The broken avatar upload" },
25
+ { "raw": "i i need to send the invoice update the contract and respond to sarahs email", "clean": "I need to send the invoice, update the contract, and respond to Sarah's email." },
26
+ { "raw": "so the deploy process is uh first run the migrations then push the docker image then update the kubernetes config then verify in staging then promote to prod", "clean": "The deploy process:\n1. Run the migrations\n2. Push the Docker image\n3. Update the Kubernetes config\n4. Verify in staging\n5. Promote to prod" },
27
+ { "raw": "groceries we need paper towels trash bags laundry pods and dryer sheets", "clean": "Groceries we need: paper towels, trash bags, laundry pods, and dryer sheets." },
28
+ { "raw": "the top three priorities are shipping the api documenting the sdk and onboarding the new engineer", "clean": "The top three priorities:\n- Shipping the API\n- Documenting the SDK\n- Onboarding the new engineer" },
29
+ { "raw": "uh first marinate the chicken for an hour then grill it for twelve minutes then let it rest for five minutes", "clean": "1. Marinate the chicken for an hour\n2. Grill it for 12 minutes\n3. Let it rest for 5 minutes" },
30
+ { "raw": "the the team leads on this project are ravi diana and tom", "clean": "The team leads on this project are Ravi, Diana, and Tom." },
31
+ { "raw": "errands today drop off the dry cleaning hit the post office and pick up coffee beans", "clean": "Errands today:\n- Drop off the dry cleaning\n- Hit the post office\n- Pick up coffee beans" },
32
+ { "raw": "um okay so first we need to review the contract then negotiate the terms then sign it and then wire the deposit", "clean": "1. Review the contract\n2. Negotiate the terms\n3. Sign it\n4. Wire the deposit" },
33
+ { "raw": "for the camping trip we need a tent sleeping pads a lantern and matches", "clean": "For the camping trip we need a tent, sleeping pads, a lantern, and matches." },
34
+ { "raw": "the the four metrics we track are dau retention churn and arpu", "clean": "The four metrics we track:\n- DAU\n- Retention\n- Churn\n- ARPU" },
35
+ { "raw": "tonight uh wash the dishes take out the trash and water the plants", "clean": "Tonight:\n- Wash the dishes\n- Take out the trash\n- Water the plants" },
36
+ { "raw": "well first wake the kids up then make their lunches then drop them at school then head to the office", "clean": "1. Wake the kids up\n2. Make their lunches\n3. Drop them at school\n4. Head to the office" },
37
+ { "raw": "buy a printer some ink cartridges and printer paper from staples", "clean": "Buy a printer, some ink cartridges, and printer paper from Staples." },
38
+ { "raw": "the the questions to ask in the interview are tell me about a hard project how do you handle conflict and where do you want to be in five years", "clean": "Questions to ask in the interview:\n- Tell me about a hard project\n- How do you handle conflict\n- Where do you want to be in five years" },
39
+ { "raw": "for the road trip download podcasts charge the kindle pack snacks and fill the tank", "clean": "For the road trip:\n- Download podcasts\n- Charge the Kindle\n- Pack snacks\n- Fill the tank" },
40
+ { "raw": "uh send the slides confirm the room and email the attendees", "clean": "Send the slides, confirm the room, and email the attendees." },
41
+ { "raw": "step one open terminal step two run npm install step three start the dev server", "clean": "1. Open terminal\n2. Run npm install\n3. Start the dev server" },
42
+ { "raw": "favorite restaurants in seattle are canlis altura and the walrus and the carpenter", "clean": "Favorite restaurants in Seattle:\n- Canlis\n- Altura\n- The Walrus and the Carpenter" },
43
+ { "raw": "i need to to to renew my license update my insurance and book the eye exam", "clean": "I need to renew my license, update my insurance, and book the eye exam." },
44
+ { "raw": "um so the agenda for the offsite is uh monday team building tuesday strategy session wednesday product planning and thursday wrap up and dinner", "clean": "Agenda for the offsite:\n- Monday: team building\n- Tuesday: strategy session\n- Wednesday: product planning\n- Thursday: wrap up and dinner" },
45
+ { "raw": "grab some lemons limes mint and club soda", "clean": "Grab some lemons, limes, mint, and club soda." },
46
+ { "raw": "the cities we are launching in next quarter are austin nashville and raleigh", "clean": "The cities we're launching in next quarter:\n- Austin\n- Nashville\n- Raleigh" },
47
+ { "raw": "first uh boot the laptop then plug in the dongle then start the meeting at exactly ten", "clean": "1. Boot the laptop\n2. Plug in the dongle\n3. Start the meeting at exactly 10" },
48
+ { "raw": "things i forgot to pack toothbrush deodorant and my phone charger", "clean": "Things I forgot to pack: toothbrush, deodorant, and my phone charger." },
49
+ { "raw": "the the open prs i need to review are the redis upgrade the otel instrumentation and the new feature flag service", "clean": "Open PRs I need to review:\n- The Redis upgrade\n- The OTel instrumentation\n- The new feature flag service" },
50
+ { "raw": "uh you know like first stretch for ten minutes then do the run then cool down for five", "clean": "1. Stretch for 10 minutes\n2. Do the run\n3. Cool down for 5" },
51
+ { "raw": "pick up rosemary thyme and garlic for the roast", "clean": "Pick up rosemary, thyme, and garlic for the roast." },
52
+ { "raw": "the steps to reproduce the bug are go to settings click on billing select a plan and watch it crash", "clean": "Steps to reproduce the bug:\n1. Go to settings\n2. Click on billing\n3. Select a plan\n4. Watch it crash" },
53
+ { "raw": "we we we need to update the readme bump the version tag the release and publish to npm", "clean": "We need to:\n1. Update the README\n2. Bump the version\n3. Tag the release\n4. Publish to npm" },
54
+ { "raw": "send chocolates flowers and a card to mom for her birthday", "clean": "Send chocolates, flowers, and a card to Mom for her birthday." },
55
+ { "raw": "um the apps i use every day are slack notion linear and figma", "clean": "Apps I use every day:\n- Slack\n- Notion\n- Linear\n- Figma" },
56
+ { "raw": "tomorrow morning first check email then triage support tickets then start on the redesign", "clean": "Tomorrow morning:\n1. Check email\n2. Triage support tickets\n3. Start on the redesign" },
57
+ { "raw": "the the courses i want to take this year are systems design distributed systems and machine learning", "clean": "Courses I want to take this year:\n- Systems Design\n- Distributed Systems\n- Machine Learning" },
58
+ { "raw": "remember to feed the cat lock the door and set the alarm before leaving", "clean": "Remember to feed the cat, lock the door, and set the alarm before leaving." },
59
+ { "raw": "uh so for the launch checklist we need to first qa the build then prepare the release notes then notify customer success then post the announcement and finally monitor errors for the first hour", "clean": "Launch checklist:\n1. QA the build\n2. Prepare the release notes\n3. Notify customer success\n4. Post the announcement\n5. Monitor errors for the first hour" },
60
+ { "raw": "things to buy at home depot screws a level a tape measure and wood glue", "clean": "Things to buy at Home Depot:\n- Screws\n- A level\n- A tape measure\n- Wood glue" },
61
+ { "raw": "i need to call the plumber book the carpet cleaner and order new blinds", "clean": "I need to call the plumber, book the carpet cleaner, and order new blinds." },
62
+ { "raw": "well first you commit your changes then push to your branch then open a pr then request review", "clean": "1. Commit your changes\n2. Push to your branch\n3. Open a PR\n4. Request review" },
63
+ { "raw": "uh pack the laptop the iPad the kindle and noise canceling headphones", "clean": "Pack the laptop, the iPad, the Kindle, and noise-canceling headphones." },
64
+ { "raw": "the the three features we are cutting from v1 are dark mode the chrome extension and team workspaces", "clean": "The three features we're cutting from v1:\n- Dark mode\n- The Chrome extension\n- Team workspaces" },
65
+ { "raw": "you know first kick off the meeting then go over the metrics then talk about the roadmap then leave time for q and a", "clean": "1. Kick off the meeting\n2. Go over the metrics\n3. Talk about the roadmap\n4. Leave time for Q&A" },
66
+ { "raw": "groceries milk eggs butter and a loaf of sourdough", "clean": "Groceries: milk, eggs, butter, and a loaf of sourdough." },
67
+ { "raw": "the people to invite to the dinner are james emily robert and lily", "clean": "People to invite to the dinner:\n- James\n- Emily\n- Robert\n- Lily" },
68
+ { "raw": "first thing monday morning check slack respond to urgent threads then start coding", "clean": "First thing Monday morning:\n1. Check Slack\n2. Respond to urgent threads\n3. Start coding" },
69
+ { "raw": "um pick up some paper towels a sponge and dish soap on the way home", "clean": "Pick up some paper towels, a sponge, and dish soap on the way home." },
70
+ { "raw": "the the the steps for the recipe are chop the onions saute them add the broth simmer for twenty minutes and blend until smooth", "clean": "Steps for the recipe:\n1. Chop the onions\n2. Saute them\n3. Add the broth\n4. Simmer for 20 minutes\n5. Blend until smooth" },
71
+ { "raw": "remind me to call mom send the birthday card and book the flight home for thanksgiving", "clean": "Remind me to call Mom, send the birthday card, and book the flight home for Thanksgiving." },
72
+ { "raw": "the the cloud providers we evaluated are aws gcp and azure", "clean": "Cloud providers we evaluated:\n- AWS\n- GCP\n- Azure" },
73
+ { "raw": "uh you know first set up the camera then check the lighting then start recording then save the file", "clean": "1. Set up the camera\n2. Check the lighting\n3. Start recording\n4. Save the file" },
74
+ { "raw": "things i love about portland the food the coffee and the bookstores", "clean": "Things I love about Portland: the food, the coffee, and the bookstores." },
75
+ { "raw": "the the the items on the packing list are passport boarding pass cash and a power adapter", "clean": "Items on the packing list:\n- Passport\n- Boarding pass\n- Cash\n- A power adapter" },
76
+ { "raw": "today i need to finish the spec review the design and start on the prototype", "clean": "Today I need to finish the spec, review the design, and start on the prototype." },
77
+ { "raw": "um first run the linter then run the tests then run the build and finally open a pr", "clean": "1. Run the linter\n2. Run the tests\n3. Run the build\n4. Open a PR" },
78
+ { "raw": "the the speakers at the conference are linda chen jamal ortiz and yuki tanaka", "clean": "Speakers at the conference:\n- Linda Chen\n- Jamal Ortiz\n- Yuki Tanaka" },
79
+ { "raw": "for breakfast i want eggs bacon toast and orange juice", "clean": "For breakfast I want eggs, bacon, toast, and orange juice." },
80
+ { "raw": "so the the the order of operations is uh first authenticate the user then validate the input then write to the database then send the confirmation email", "clean": "Order of operations:\n1. Authenticate the user\n2. Validate the input\n3. Write to the database\n4. Send the confirmation email" },
81
+ { "raw": "remember to bring the charger the dongle and the presenter clicker", "clean": "Remember to bring the charger, the dongle, and the presenter clicker." },
82
+ { "raw": "the channels we should monitor are general engineering incidents and the on call channel", "clean": "Channels we should monitor:\n- general\n- engineering\n- incidents\n- the on-call channel" }
83
+ ]
data/seed/synthetic_pairs.csv ADDED
The diff for this file is too large to render. See raw diff
 
data/seed/synthetic_pairs.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
docs/HANDBOOK.md ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mumble cleanup model: handbook
2
+
3
+ The complete picture for the optional transcript cleanup model: the research
4
+ behind it, the fine-tuning mechanism in detail, the metrics, and what every file
5
+ should do. This is the reference to read before writing the code.
6
+
7
+ Nothing in this project runs automatically. Training happens on a rented GPU
8
+ only when you decide to launch it.
9
+
10
+ ---
11
+
12
+ ## 1. Where this fits
13
+
14
+ Mumble's dictation pipeline today:
15
+
16
+ push-to-talk -> capture (cpal, 16kHz mono, gain normalized, pre-roll)
17
+ -> Parakeet-TDT-0.6B-v3 (int8 ONNX via sherpa-onnx)
18
+ -> custom dictionary substitution
19
+ -> paste at cursor
20
+
21
+ The ASR model transcribes verbatim, including disfluencies ("um", "uh"),
22
+ repeated words, and false starts, and it has no idea about list or paragraph
23
+ structure. The cleanup model is an OPTIONAL pass inserted between transcription
24
+ and paste:
25
+
26
+ ... -> Parakeet text -> [cleanup model, if enabled] -> paste
27
+
28
+ It removes fillers and disfluencies, fixes punctuation and capitalization, and
29
+ does light formatting, WITHOUT rewording, adding facts, or answering questions
30
+ in the text. This mirrors what Wispr Flow does.
31
+
32
+ ---
33
+
34
+ ## 2. Research recap
35
+
36
+ ### 2.1 The ASR model and int8 vs fp32
37
+
38
+ Mumble ships **Parakeet-TDT-0.6B-v3** quantized to **int8** (the
39
+ `encoder.int8.onnx` etc. assets), run through sherpa-onnx on ONNX Runtime. We
40
+ benchmarked int8 against the fp32 build on 150 utterances of LibriSpeech
41
+ test-clean (see `bench/BENCHMARK.md`):
42
+
43
+ | Variant | WER % | CER % | RTF | Model RAM | Peak RAM |
44
+ |---|---|---|---|---|---|
45
+ | int8 (shipped) | 1.69 | 0.64 | 0.036 (27x realtime) | 723 MB | 2.17 GB |
46
+ | fp32 | 1.44 | 0.47 | 0.08 (12.5x realtime) | 2.29 GB | 3.88 GB |
47
+
48
+ Published NVIDIA reference is 1.93% on full test-clean, so our setup is correct.
49
+ Decision: **int8 stays the default** (2.2x faster, a third of the RAM, only 0.25
50
+ points of WER cost); fp32 becomes an optional download for users who want max
51
+ quality. Both numbers go in the Settings model-quality selector.
52
+
53
+ ### 2.2 Why a cleanup model, and why fine-tune
54
+
55
+ A zero-shot prototype (Qwen2.5-0.5B-Instruct, plain prompt) cleaned real Mumble
56
+ transcripts well: it removed "and and" -> "and", "Claude Claude" -> "Claude",
57
+ fillers, and fixed punctuation, at about 1.7s per transcript on CPU. But on
58
+ short or ambiguous inputs it slipped into assistant mode ("Yes, ...", "Sure, I
59
+ can ...") and lightly reworded. A source-overlap guard caught those cases.
60
+
61
+ Fine-tuning fixes this at the root: instead of hoping a prompt constrains the
62
+ model, we teach the behavior from data. This is also why a 0.5B model is enough.
63
+ Google's whole pitch for tiny models is task-specific fine-tuning.
64
+
65
+ ### 2.3 Base model choice
66
+
67
+ | | Gemma 3 270M | Qwen2.5-0.5B (chosen) | Qwen3-0.6B |
68
+ |---|---|---|---|
69
+ | License | Gemma ToU, gated, passes down to shipped weights | Apache-2.0, ungated | Apache-2.0, ungated |
70
+ | Notes | best tiny FT base on merits, but license friction for a paid app | already prototyped, zero migration | a bit more headroom, must disable thinking |
71
+
72
+ Gemma 4 is Apache-2.0 and ungated but its smallest size is 2.3B, far over our
73
+ latency budget. **Decision: fine-tune Qwen2.5-0.5B-Instruct.** Licensing is the
74
+ deciding factor for a shippable product; quality after fine-tuning on a task
75
+ this narrow is a wash across the candidates.
76
+
77
+ ---
78
+
79
+ ## 3. The data: synthetic injection only
80
+
81
+ ### 3.1 The idea
82
+
83
+ We do NOT collect raw->clean pairs from real STT. Instead we take **clean
84
+ written text** (the target) and programmatically **corrupt** it to make the raw
85
+ input. Because every corruption is an insertion plus punctuation/casing removal,
86
+ the clean target is recoverable from the raw by deletion and repunctuation
87
+ alone. The model therefore **cannot learn to invent content** (the exact failure
88
+ we saw). Faithfulness is structural, not prompt-hoped-for. This is the whole
89
+ reason for choosing injection.
90
+
91
+ ### 3.2 The corruption recipe (`configs/inject.yaml`, `src/cleanup/inject.py`)
92
+
93
+ Given a clean sentence:
94
+ - **false start**: prepend a duplicated 1-4 word head ("i want, i want to go").
95
+ - **repetition**: duplicate an internal 1-3 word span ("go to to the store").
96
+ - **fillers**: insert "um/uh/like/you know" at word gaps.
97
+ - **lowercase + strip punctuation**: always, so the model learns to restore
98
+ casing and punctuation.
99
+
100
+ Each corruption fires with a probability from the config, so one clean sentence
101
+ yields many distinct raw variants (a few hundred clean sentences can generate
102
+ thousands of pairs).
103
+
104
+ ### 3.3 Scope and the one caveat
105
+
106
+ - **v1 scope:** disfluency removal + punctuation/casing. Light formatting
107
+ (lists, paragraphs) is NOT learnable from injection and is deferred to v2
108
+ (which would add teacher distillation).
109
+ - **Caveat:** the clean source MUST be properly punctuated, capitalized written
110
+ text, or the model never learns to restore punctuation. `01_data.py` prints
111
+ sample pairs so you can confirm this on the first run. If the default source
112
+ is bare speech transcript, switch `clean_source` in the config.
113
+ - **Train synthetic, evaluate real.** Training is synthetic, but the held-out
114
+ test set is the REAL DisfluencySpeech test split (real disfluent speech with a
115
+ gold clean target). That measures whether the synthetic->real jump holds.
116
+
117
+ ---
118
+
119
+ ## 4. The fine-tuning mechanism (read this before writing 02_train.py)
120
+
121
+ ### 4.1 What supervised fine-tuning (SFT) is
122
+
123
+ The base model is a next-token predictor: given tokens so far, it outputs a
124
+ probability distribution over the next token. Fine-tuning continues training it
125
+ on our (prompt, target) pairs so that, given the cleanup prompt + a raw
126
+ transcript, it produces the clean transcript.
127
+
128
+ Each example is rendered with the chat template into one token sequence:
129
+
130
+ <|im_start|>system\n {SYSTEM_PROMPT} <|im_end|>
131
+ <|im_start|>user\n {raw} <|im_end|>
132
+ <|im_start|>assistant\n {clean} <|im_end|>
133
+
134
+ ### 4.2 The loss
135
+
136
+ Causal-LM cross-entropy (negative log-likelihood). For target tokens y_1..y_T,
137
+
138
+ loss = - (1/T) * sum_t log p_theta(y_t | y_<t, prompt)
139
+
140
+ The model is rewarded for putting high probability on the actual next clean
141
+ token at every position. **Completion-only loss:** we mask every token up to and
142
+ including `<|im_start|>assistant\n`, so the loss is computed ONLY on the clean
143
+ target tokens. The model is never trained to reproduce the prompt or the raw
144
+ input, only to emit the clean output. (This is the `DataCollatorForCompletion
145
+ OnlyLM` with the assistant response template.)
146
+
147
+ ### 4.3 Gradients and optimization
148
+
149
+ - **Forward pass:** tokens -> logits -> loss.
150
+ - **Backward pass (backprop):** autograd computes the gradient of the loss with
151
+ respect to every trainable parameter, d(loss)/d(theta), by the chain rule.
152
+ - **Optimizer = AdamW.** For each trainable parameter it keeps two running
153
+ averages: m (first moment, the mean/momentum of recent gradients) and v
154
+ (second moment, the mean of squared gradients, a per-parameter variance). The
155
+ update is roughly
156
+
157
+ theta <- theta - lr * m_hat / (sqrt(v_hat) + eps) then theta <- theta - lr * wd * theta
158
+
159
+ Dividing by sqrt(v) gives each parameter its own effective step size (large for
160
+ consistently-signed gradients, small for noisy ones), which is why Adam-family
161
+ optimizers are stable for transformers. The "W" is decoupled weight decay (the
162
+ second term), a cleaner L2 regularizer than classic Adam.
163
+ - **Learning-rate schedule:** linear **warmup** for the first few percent of
164
+ steps (ramp lr from 0 so early, large, random-direction updates do not
165
+ destabilize the model), then **cosine decay** down toward 0.
166
+ - **Effective batch size = per_device_batch * grad_accum (* num_gpus).** Gradient
167
+ accumulation runs several micro-batches, sums their gradients, and only then
168
+ steps the optimizer, so you get the stability of a big batch without the
169
+ memory of one.
170
+ - **Gradient clipping (`max_grad_norm`)** rescales the gradient if its norm
171
+ exceeds a threshold, preventing a single bad batch from blowing up the weights.
172
+ - **Mixed precision:** bf16 (or fp16) for the forward/backward math is ~2x
173
+ faster and uses less memory than fp32; tf32 speeds up matmuls on NVIDIA Ampere
174
+ and newer. On CPU these are off (the smoke path).
175
+ - **Epochs:** full passes over the data. 2-3 is right for a few-thousand-pair
176
+ SFT; more risks overfitting the synthetic distribution.
177
+
178
+ ### 4.4 LoRA, in detail (this is the key part)
179
+
180
+ **Problem with full fine-tuning.** Updating all ~0.5B parameters means storing a
181
+ gradient and two AdamW moment buffers per parameter (so several bytes x 0.5B =
182
+ multiple GB of optimizer state), and you end up with a full copy of the model per
183
+ task. Overkill for adapting to one narrow task.
184
+
185
+ **LoRA (Low-Rank Adaptation).** Freeze the pretrained weight matrix W0 (shape
186
+ d x k). Do not update it. Represent the fine-tuning update as a **low-rank**
187
+ product:
188
+
189
+ W_effective = W0 + dW, dW = (alpha / r) * B @ A
190
+
191
+ where A is r x k, B is d x r, and the **rank r is tiny** (we use r = 16) compared
192
+ to d and k (hundreds to thousands). Only A and B are trainable. The forward pass
193
+ becomes:
194
+
195
+ h = W0 @ x + (alpha / r) * B @ (A @ x)
196
+
197
+ **Why it works.** The weight update needed to adapt a big pretrained model to a
198
+ specific task has low "intrinsic rank" - a small-rank correction is enough. You
199
+ are not relearning language, just nudging behavior.
200
+
201
+ **Initialization.** A is initialized small-random (Gaussian), B is initialized to
202
+ **zero**, so dW = 0 at step 0 and training starts from exactly the pretrained
203
+ model. The adapters then learn the correction.
204
+
205
+ **The knobs:**
206
+ - `lora_r` (rank, 16): capacity of the adapter. Higher = more expressive, more
207
+ params.
208
+ - `lora_alpha` (32): a scaling factor; the effective scale applied to dW is
209
+ alpha/r (= 2 here). Think of it as the adapter's learning-rate gain.
210
+ - `lora_target_modules` (q_proj, k_proj, v_proj, o_proj): which weight matrices
211
+ get an adapter. The attention projections are the standard choice; you can add
212
+ the MLP projections for more capacity.
213
+ - `lora_dropout` (0.05): dropout on the adapter path, light regularization.
214
+
215
+ **Parameter and memory win.** Per adapted matrix, trainable params drop from
216
+ d*k to r*(d+k). For Qwen2.5-0.5B with adapters on the four attention projections
217
+ at r=16, you train on the order of a few million parameters (roughly 1% of the
218
+ model) instead of 490M. Gradients and AdamW moments exist only for those few
219
+ million, so it fits easily on a modest GPU, and the saved adapter is tens of MB,
220
+ not a gigabyte.
221
+
222
+ **Gradients with LoRA.** W0 has `requires_grad = False`, so backprop computes
223
+ gradients only into A and B. Everything else about the optimization (AdamW,
224
+ schedule, clipping) is unchanged, just applied to far fewer parameters.
225
+
226
+ **Merging for deployment (`04_export.py`).** At inference you do not want the
227
+ extra B@A matmul. `merge_and_unload()` folds the adapter back in:
228
+ W0 <- W0 + (alpha/r)*B@A, producing a standalone model identical in shape to the
229
+ base. That merged model is what we export to ONNX for the Rust `ort` backend.
230
+
231
+ **QLoRA vs LoRA.** QLoRA additionally quantizes the frozen base to 4-bit to save
232
+ even more memory. At 0.5B we do not need it; plain LoRA in bf16 fits comfortably,
233
+ and skipping 4-bit avoids a small quality hit.
234
+
235
+ ---
236
+
237
+ ## 5. Metrics
238
+
239
+ Training optimizes the cross-entropy loss above. To judge whether the fine-tune
240
+ is actually better than the base model, `03_evaluate.py` runs BOTH on the
241
+ held-out real test set and scores a suite that balances "did it edit correctly"
242
+ against "did it stay faithful":
243
+
244
+ - **chrF** (sacrebleu): character n-gram F-score of the output against the gold
245
+ clean. General overlap/fluency signal, stable on short text.
246
+ - **disfluency-removal F1:** treat cleanup as deleting tokens from the raw. Gold
247
+ deletions = tokens in raw not in gold; predicted deletions = tokens in raw not
248
+ in output. F1 over those deleted multisets. Measures the core job directly.
249
+ - **added-content rate:** fraction of output content tokens NOT present in the
250
+ raw input. Should be about 0. **This is the hallucination guard** - the metric
251
+ that catches the model inventing or answering. A fine-tune that improves chrF
252
+ but raises added-content is a regression, not a win.
253
+ - **source-overlap:** fraction of output tokens that are present in the raw.
254
+ Should be near 1.
255
+
256
+ (Optional, heavier: ERRANT F0.5 via spacy for formal edit scoring; a
257
+ sentence-embedding cosine for semantic drift. Left as extras behind the `errant`
258
+ optional dependency.)
259
+
260
+ **The decisive behavioral test:** feed dictated questions ("um what's the capital
261
+ of france") and confirm the fine-tune **cleans** them ("What's the capital of
262
+ France?") rather than **answering**. This is the exact failure the base model
263
+ made, so it is the single most important check.
264
+
265
+ **Protocol:** held-out real test never seen in training, greedy decoding for
266
+ determinism, one table with rows {base, fine-tune} and the four columns, plus a
267
+ qualitative before/after on real transcripts. Win condition: higher disfluency
268
+ F1 and chrF with added-content held at about 0.
269
+
270
+ ---
271
+
272
+ ## 6. File-by-file guide
273
+
274
+ Infra (already written, the format, do not need rewriting):
275
+ - `pyproject.toml`, `.python-version`, `uv.lock` (generated by `uv sync`): the
276
+ pinned, self-contained environment.
277
+ - `Makefile`: one make target per pipeline stage, RUN_ID / LR / EPOCHS settable.
278
+ - `configs/inject.yaml`: the injection recipe and the clean/eval data sources.
279
+ - `configs/train.yaml`: base model, LoRA knobs, optimizer, schedule, precision.
280
+ - `README.md`: quickstart and the Vast.ai steps.
281
+
282
+ Code to write (scaffolded with signatures + specs):
283
+ - `src/cleanup/config.py`: load the two yaml files into `TrainConfig` /
284
+ `InjectConfig` dataclasses.
285
+ - `src/cleanup/prompts.py`: the `SYSTEM_PROMPT` and `build_messages` (a starting
286
+ prompt is provided; tune it).
287
+ - `src/cleanup/inject.py`: `strip_punctuation_and_lowercase` and `make_raw` (the
288
+ injection algorithm from section 3.2). Pure functions, unit-tested.
289
+ - `src/cleanup/data.py`: load clean sentences (hf stream or a local file), build
290
+ pairs, split, load the real eval set, jsonl read/write.
291
+ - `src/cleanup/train.py`: `build_dataset` (chat-template the pairs) and `train`
292
+ (LoRA + trl SFTTrainer, completion-only loss). Section 4 is the spec.
293
+ - `src/cleanup/infer.py`: `load_model` (base + optional adapter) and `clean_text`
294
+ (greedy generate, output length capped near the input).
295
+ - `src/cleanup/evaluate.py`: the metric functions from section 5.
296
+ - `src/cleanup/export.py`: `merge_adapter` (fold LoRA in) and `export_onnx`.
297
+ - `src/cleanup/pack.py`: `render_report` and `pack_run` (tar + sha256).
298
+ - `scripts/01_data.py .. 06_pack_and_ship.py`: thin CLI entry points that wire
299
+ the src functions together per stage. Each has its argparse and a step list.
300
+ - `tests/test_inject.py`: assert the injection invariants (the faithfulness one
301
+ matters most).
302
+
303
+ ---
304
+
305
+ ## 7. Vast.ai workflow
306
+
307
+ 1. rent a cuda pytorch instance, note the ssh host and port.
308
+ 2. ssh in, clone the mumble repo, `cd models/cleanup`.
309
+ 3. `uv sync` (generates `uv.lock` on first run; commit it).
310
+ 4. `make all RUN_ID=r1` (or run stages one at a time and inspect between them).
311
+ 5. `pack` prints a sha256 and an `scp -P <port> root@<host>:... .` line. pull the
312
+ `dist/<run>.tar.gz` off the box, run `shasum -a 256` to verify, then destroy
313
+ the instance.
314
+
315
+ `02_train.py` is cuda-aware (bf16 when the GPU supports it, else fp16, off on
316
+ CPU), so `make smoke` runs the same code on CPU locally with a few rows and one
317
+ epoch to validate wiring before you rent anything.
318
+
319
+ Do not launch a Vast.ai run until the code is written and the plan reviewed.
docs/data_images/category_counts.png ADDED
docs/data_images/faithfulness.png ADDED
docs/data_images/filler_intensity.png ADDED
docs/data_images/length_distribution.png ADDED
docs/data_images/raw_vs_clean_length.png ADDED
docs/data_images/top_fillers.png ADDED
docs/data_report.md ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # data report
2
+
3
+ The synthetic seed dataset that backs the Mumble cleanup model. Built by a multi-agent workflow that spawned 8 specialist agents in parallel and produced 612 pairs across 8 dictation categories; a polish pass added 76 more `long_form_thoughts` pairs with strictly diverse openers, bringing the total to **688 pairs**.
4
+
5
+ Every pair is `{ raw: <Parakeet-shaped lowercase no-punct disfluent input>, clean: <proper English output> }`. The clean side is faithful by construction: every content word in `clean` exists in `raw` (modulo standard homophone fixes, contractions, and casing). This is what stops the model from learning to hallucinate.
6
+
7
+ ## category mix
8
+
9
+ ![category counts](data_images/category_counts.png)
10
+
11
+ | category | count |
12
+ |---|---:|
13
+ | `casual_messages` | 81 |
14
+ | `long_form_thoughts` | 145 |
15
+ | `meeting_notes` | 81 |
16
+ | `mixed_content` | 70 |
17
+ | `professional_emails` | 80 |
18
+ | `questions_and_asks` | 70 |
19
+ | `technical_dictation` | 80 |
20
+ | `todo_lists` | 81 |
21
+ | **total** | **688** |
22
+
23
+ `long_form_thoughts` is intentionally over-weighted because paragraph-length cleanup is the hardest behavior (multiple sentence boundaries, sustained context, false starts) and 145 examples gives the model the signal it needs to handle 60-90 word inputs.
24
+
25
+ ## length distribution
26
+
27
+ ![length distribution](data_images/length_distribution.png)
28
+
29
+ Raw inputs span **2 to 70 words** with a median of **17**. Clean outputs are slightly shorter on average (16 median words) because they have fillers and stutters removed. The categories show meaningfully different length distributions: short utterances dominate `casual_messages`, `questions_and_asks`, and `mixed_content`; long paragraph-shaped inputs dominate `long_form_thoughts`.
30
+
31
+ ## raw vs clean length
32
+
33
+ ![raw vs clean length](data_images/raw_vs_clean_length.png)
34
+
35
+ Points below the diagonal mean clean is shorter than raw — the model is being trained to remove material, not add it. The cluster sits just below the diagonal, which is the expected shape for a faithful cleanup task: a few words removed per input on average, never more than ~25%.
36
+
37
+ ## disfluency intensity
38
+
39
+ ![filler intensity by category](data_images/filler_intensity.png)
40
+
41
+ Average filler-word count per raw input, by category. `meeting_notes` and `long_form_thoughts` carry the heaviest disfluency load (people think out loud during meetings); `mixed_content` and `questions_and_asks` are leanest (those categories are about precision, not verbosity).
42
+
43
+ ![top fillers](data_images/top_fillers.png)
44
+
45
+ Distribution of filler words across the entire dataset. `um` and `uh` dominate (matching real Parakeet output), with `like`, `you know`, and `so` following at a moderate rate. The mix matches what shows up in real dictation transcripts.
46
+
47
+ ## faithfulness check
48
+
49
+ ![faithfulness distribution](data_images/faithfulness.png)
50
+
51
+ For each pair, we compute the fraction of content words in the clean side that also appear in the raw side. A perfect value is 1.0 (every clean content word came from raw); lower values indicate the clean introduced content the raw did not have, which would train the model to hallucinate.
52
+
53
+ - **Mean faithfulness**: 0.929
54
+ - **Median faithfulness**: 0.957
55
+ - **Pairs above 0.95 threshold**: 391 of 688 (56.8%)
56
+ - **Pairs above 0.90 threshold**: 527 of 688 (76.6%)
57
+
58
+ Small drops below 1.0 come from legitimate sources: number-word to digit conversion ("two thirty" -> "2:30"), proper-noun capitalization that adds new tokens to the content-word set under our simple lowercase comparison ("acme" -> "Acme" should be counted as matching but our naive check might miss some), and contractions ("i" -> "I'm" via apostrophe restoration).
59
+
60
+ ## sample pairs
61
+
62
+ Two per category, illustrating the shape of the dataset:
63
+
64
+ ### `casual_messages`
65
+
66
+ - **raw**: `hey sarah you free for lunch around noon`
67
+ - **clean**: Hey Sarah, you free for lunch around noon?
68
+
69
+ - **raw**: `did you finish the writeup for the all hands`
70
+ - **clean**: Did you finish the writeup for the all hands?
71
+
72
+ ### `long_form_thoughts`
73
+
74
+ - **raw**: `so ive been mulling over the pricing change and honestly i think we we moved too fast on the annual discount like the data showed a bump in conversions but um when you look at retention three months out the cohort actually churns harder than the monthly folks so we might be optimizing for a vanity metric instead of long term value`
75
+ - **clean**: I've been mulling over the pricing change, and honestly I think we moved too fast on the annual discount. The data showed a bump in conversions, but when you look at retention three months out, the cohort actually churns harder than the monthly folks. We might be optimizing for a vanity metric instead of long-term value.
76
+
77
+ - **raw**: `the thing that bugs me about our okr process is that we we set them in january then never look at them again until december so they become this performative document instead of a living tool and um if a goal isnt influencing weekly decisions its not really a goal its just a wish we wrote down`
78
+ - **clean**: The thing that bugs me about our OKR process is that we set them in January then never look at them again until December, so they become this performative document instead of a living tool. If a goal isn't influencing weekly decisions, it's not really a goal. It's just a wish we wrote down.
79
+
80
+ ### `meeting_notes`
81
+
82
+ - **raw**: `ok so we decided to ship the dark mode rollout next sprint priya is the owner`
83
+ - **clean**: We decided to ship the dark mode rollout next sprint. Priya is the owner.
84
+
85
+ - **raw**: `uh action item add a slack channel for the the procurement project`
86
+ - **clean**: Action item: add a Slack channel for the procurement project.
87
+
88
+ ### `mixed_content`
89
+
90
+ - **raw**: `meeting with sarah at two fifteen pm tomorrow`
91
+ - **clean**: Meeting with Sarah at 2:15 PM tomorrow.
92
+
93
+ - **raw**: `uber driver arriving in four minutes silver toyota camry license seven a b c three four five`
94
+ - **clean**: Uber driver arriving in 4 minutes. Silver Toyota Camry, license 7ABC345.
95
+
96
+ ### `professional_emails`
97
+
98
+ - **raw**: `hi marcus quick follow up on the contract did legal sign off yet`
99
+ - **clean**: Hi Marcus, quick follow up on the contract. Did legal sign off yet?
100
+
101
+ - **raw**: `thanks ill review and get back to you by tomorrow noon`
102
+ - **clean**: Thanks, I'll review and get back to you by tomorrow noon.
103
+
104
+ ### `questions_and_asks`
105
+
106
+ - **raw**: `can you send me the deck`
107
+ - **clean**: Can you send me the deck?
108
+
109
+ - **raw**: `is there a reason why we picked dynamodb over postgres for this i want to understand the tradeoff`
110
+ - **clean**: Is there a reason why we picked DynamoDB over Postgres for this? I want to understand the tradeoff.
111
+
112
+ ### `technical_dictation`
113
+
114
+ - **raw**: `the mutex is held across the await point which deadlocks tokio`
115
+ - **clean**: The mutex is held across the await point, which deadlocks Tokio.
116
+
117
+ - **raw**: `well i mean the the perf regression came from the new react server component because its now making a fresh database connection per render instead of reusing the pool`
118
+ - **clean**: The perf regression came from the new React server component because it's now making a fresh database connection per render instead of reusing the pool.
119
+
120
+ ### `todo_lists`
121
+
122
+ - **raw**: `pick up bread milk and eggs`
123
+ - **clean**: Pick up bread, milk, and eggs.
124
+
125
+ - **raw**: `favorite restaurants in seattle are canlis altura and the walrus and the carpenter`
126
+ - **clean**: Favorite restaurants in Seattle:
127
+ - Canlis
128
+ - Altura
129
+ - The Walrus and the Carpenter
130
+
131
+ ## limitations
132
+
133
+ - **Synthetic origin**: every pair was generated by an LLM workflow, not transcribed from real Parakeet output. The disfluency patterns are modeled to match real ASR failure modes but may under-represent edge cases the model will face in production.
134
+ - **Size**: 688 pairs is on the lower-middle end of the documented sweet spot for narrow LoRA fine-tunes (200-500 floor, 2k-5k comfortable). Adequate for a v1 ship; if eval pass rate is below 0.85 we regenerate another 600-1000 pairs and retrain.
135
+ - **Faithfulness is statistical, not strict**: a few pairs may drop below 0.95 because of legitimate transformations (numeric formatting, proper-noun casing). We don't filter these out because the training task explicitly wants the model to learn those transformations.
136
+ - **English only.**
docs/model_card.md ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: Qwen/Qwen2.5-0.5B-Instruct
4
+ language:
5
+ - en
6
+ pipeline_tag: text2text-generation
7
+ library_name: transformers
8
+ tags:
9
+ - dictation
10
+ - cleanup
11
+ - transcript
12
+ - lora
13
+ - onnx
14
+ - mumble
15
+ metrics:
16
+ - exact_match
17
+ - f1
18
+ ---
19
+
20
+ # mumble-cleanup
21
+
22
+ A small fine-tuned language model that cleans speech-to-text dictation transcripts. Fine-tuned from `Qwen/Qwen2.5-0.5B-Instruct` with LoRA on a hand-curated synthetic dataset. Trained on a GPU, designed to run on a CPU via ONNX.
23
+
24
+ ## What it does
25
+
26
+ Given a raw transcript from an ASR system (lowercase, no punctuation, fillers and stutters preserved), it returns a cleaned version with proper capitalization, punctuation, and disfluencies removed. It does not paraphrase, summarize, or add content.
27
+
28
+ Example: `um so i i think we should ship this on uh friday` becomes `I think we should ship this on Friday.`
29
+
30
+ The model handles:
31
+
32
+ - filler removal (um, uh, like, you know, i mean)
33
+ - word stutter collapse (we we → we)
34
+ - false start cleanup
35
+ - punctuation and capitalization recovery
36
+ - homophone correction (their / there, your / you're, its / it's, to / too)
37
+ - apostrophe restoration (dont → don't)
38
+ - run-on sentence splitting
39
+ - number formatting (two thirty → 2:30)
40
+ - proper noun capitalization
41
+ - todo / list formatting when enumeration cues are clear
42
+
43
+ ## Usage
44
+
45
+ ### transformers
46
+
47
+ ```python
48
+ from transformers import AutoModelForCausalLM, AutoTokenizer
49
+
50
+ SYSTEM_PROMPT = (
51
+ "You are a transcript cleanup tool. You receive raw speech to text output "
52
+ "and return a cleaned version. Remove filler words and disfluencies (um, "
53
+ "uh, er, ah, like as filler, you know), remove repeated words and false "
54
+ "starts, and fix punctuation and capitalization. Do not reword, do not add "
55
+ "anything the speaker did not say, and do not answer questions in the text. "
56
+ "Output only the cleaned text."
57
+ )
58
+
59
+ repo = "adikuma/mumble-cleanup"
60
+ tokenizer = AutoTokenizer.from_pretrained(repo)
61
+ model = AutoModelForCausalLM.from_pretrained(repo)
62
+
63
+ raw = "um so the the meeting is at three thirty tomorrow"
64
+ prompt = tokenizer.apply_chat_template(
65
+ [
66
+ {"role": "system", "content": SYSTEM_PROMPT},
67
+ {"role": "user", "content": raw},
68
+ ],
69
+ tokenize=False,
70
+ add_generation_prompt=True,
71
+ )
72
+ inputs = tokenizer(prompt, return_tensors="pt")
73
+ out = model.generate(**inputs, max_new_tokens=128, do_sample=False)
74
+ print(tokenizer.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True))
75
+ # -> "The meeting is at 3:30 tomorrow."
76
+ ```
77
+
78
+ ### onnx (cpu)
79
+
80
+ The `onnx/model.onnx` file is an fp32 ONNX export for CPU inference. `onnx/int8/model.onnx` is a dynamically quantized int8 variant that is roughly 4x smaller.
81
+
82
+ ```python
83
+ from optimum.onnxruntime import ORTModelForCausalLM
84
+ from transformers import AutoTokenizer
85
+
86
+ repo = "adikuma/mumble-cleanup"
87
+ tokenizer = AutoTokenizer.from_pretrained(repo)
88
+ model = ORTModelForCausalLM.from_pretrained(repo, file_name="onnx/int8/model.onnx")
89
+ ```
90
+
91
+ ## Training
92
+
93
+ - **Base model**: Qwen/Qwen2.5-0.5B-Instruct (Apache-2.0)
94
+ - **Method**: LoRA SFT (r=16, alpha=32, dropout=0.05, targets q/k/v/o + gate/up/down)
95
+ - **Loss**: token cross-entropy on assistant tokens only (completion-only masking via TRL's `DataCollatorForCompletionOnlyLM`)
96
+ - **Optimizer**: AdamW (lr=2e-4, weight_decay=0.01, cosine schedule, 5% warmup, max_grad_norm=1.0)
97
+ - **Batching**: per-device 8, gradient accumulation 4 (effective 32), max sequence length 512
98
+ - **Precision**: bf16 on GPUs that support it, fp16 fallback
99
+ - **Dataset**: 688 hand-curated (raw, clean) pairs spanning 8 dictation categories (casual messages, professional emails, meeting notes, technical dictation, todo lists, long-form thoughts, questions/asks, mixed content). Stratified 85/10/5 train/val/test split.
100
+
101
+ ## Limitations
102
+
103
+ - English only.
104
+ - Trained on synthetic data; real ASR output may have failure modes the synthetic operators did not model.
105
+ - Designed for short-to-medium dictation (up to ~512 tokens). Longer inputs must be chunked.
106
+ - The model can occasionally over-correct when a user genuinely intends a fragment ("running late.") — fine-tune favors fixed-up sentences.
107
+
108
+ ## License
109
+
110
+ Apache-2.0. See [`LICENSE`](../../LICENSE) at the Mumble repo root.
111
+
112
+ ## Acknowledgements
113
+
114
+ Built on top of [`Qwen/Qwen2.5-0.5B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct) by the Qwen team.
docs/model_report.md ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mumble cleanup model report
2
+
3
+ A small fine-tuned language model that cleans dictation transcripts. Trained on a GPU, runs on a CPU.
4
+
5
+ ## What it does
6
+
7
+ You dictate something messy. It returns the cleaned version. Five things it handles:
8
+
9
+ - removes filler words (um, uh, like, you know)
10
+ - collapses word stutters ("we we" -> "we")
11
+ - recovers punctuation and capitalization
12
+ - corrects homophones (their / there, your / you're)
13
+ - formats numbers, dates, lists where the cue is clear
14
+
15
+ Example: `"um so the the meeting is at three thirty tomorrow"` becomes `"The meeting is at 3:30 tomorrow."`
16
+
17
+ ## How it was built
18
+
19
+ ```mermaid
20
+ flowchart TD
21
+ A[hand-curated seed jsonl<br/>688 pairs, 8 categories] --> B[stratified split<br/>85/10/5 train/val/test]
22
+ B --> C[lora sft on qwen2.5-0.5b-instruct<br/>1 epoch on a single rtx 4090]
23
+ C --> D[eval on held-out test<br/>raw vs base vs fine-tuned]
24
+ D --> E[merge lora + export onnx<br/>fp32 + int8 for cpu inference]
25
+ E --> F[cpu latency benchmark<br/>run on the target laptop]
26
+ ```
27
+
28
+ The seed dataset was generated by a multi-agent workflow that spawned eight specialist agents in parallel, each producing ~70-80 pairs in a distinct dictation category. After dedup, the final dataset has 612 unique pairs.
29
+
30
+ Training uses TRL's `SFTTrainer` with `DataCollatorForCompletionOnlyLM`. The collator masks system and user tokens with `-100`, so cross-entropy only fires on the assistant turn. This is what keeps the model honest: gradients flow only through the cleaned output, never through the raw disfluent input.
31
+
32
+ ## Accuracy
33
+
34
+ > Filled in after running `make evaluate`.
35
+
36
+ | model | disfluency removal | punct f1 | faithfulness | length ratio | pass rate |
37
+ |---|---:|---:|---:|---:|---:|
38
+ | raw (no cleanup) | _tbd_ | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
39
+ | Qwen base zero-shot | _tbd_ | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
40
+ | **fine-tuned** | _tbd_ | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
41
+
42
+ Pass rate is the percentage of test examples that simultaneously meet: disfluency removal ≥ 0.95, punctuation F1 ≥ 0.85, faithfulness ≥ 0.98, length ratio in [0.85, 1.05].
43
+
44
+ The base model has a documented failure mode: it **answers** questions instead of cleaning them ("what's the capital of france" → "Paris"). The adversarial question check confirms whether fine-tuning corrects this.
45
+
46
+ ![per metric comparison](report_images/metrics_comparison.png)
47
+
48
+ ## Training
49
+
50
+ ![learning curves](report_images/learning_curves.png)
51
+
52
+ > Filled in after running `make evaluate`. Look for: train loss drops smoothly, val loss tracks train, no late divergence.
53
+
54
+ ## Speed on CPU
55
+
56
+ > Measured on a laptop CPU. Laptop timings are noisy because they depend on what else the machine is doing; treat these as approximate. For an authoritative number, run the benchmark inside the actual deployment environment (the Mumble Tauri app via the Rust `ort` crate).
57
+
58
+ ![latency](report_images/latency.png)
59
+
60
+ | input length (tokens) | fp32 p50 (ms) | fp32 p95 (ms) | int8 p50 (ms) | int8 p95 (ms) |
61
+ |---:|---:|---:|---:|---:|
62
+ | 16 | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
63
+ | 32 | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
64
+ | 64 | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
65
+ | 128 | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
66
+ | 256 | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
67
+ | 512 | _tbd_ | _tbd_ | _tbd_ | _tbd_ |
68
+
69
+ Realistic mix on ~500 real test inputs (variable length): _tbd_.
70
+
71
+ ## What you get
72
+
73
+ The deliverable is:
74
+
75
+ - `runs/<run-id>/onnx/model.onnx` — fp32 ONNX, ~1 GB
76
+ - `runs/<run-id>/onnx/int8/model.onnx` — int8 ONNX, ~250 MB (target for the Mumble app)
77
+
78
+ Both run on CPU with `onnxruntime`. The Rust `ort` crate consumes the int8 build.
79
+
80
+ ## Limits
81
+
82
+ - English only.
83
+ - Trained on synthetic data. Test set is held out from the same synthetic distribution. Real ASR output may have failure modes the synthetic operators did not model. The cleanup operators were tuned to match Parakeet's failure distribution as observed in the bench harness; expect some domain shift in production.
84
+ - Inputs longer than 512 tokens must be chunked before cleanup.
85
+ - Single-turn only. Does not maintain conversation history.
86
+ - Fixed system prompt baked in at training time. Changing the prompt at inference will degrade quality.
pyproject.toml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "cleanup"
3
+ version = "0.1.0"
4
+ description = "Fine-tuned Qwen2.5-0.5B-Instruct LoRA adapter that cleans Mumble dictation transcripts."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11,<3.12"
7
+ dependencies = [
8
+ "torch>=2.4",
9
+ "transformers>=4.46,<5",
10
+ "peft>=0.13",
11
+ "trl>=0.12",
12
+ "datasets>=3.0",
13
+ "accelerate>=1.0",
14
+ "jiwer>=3.0",
15
+ "optimum[onnxruntime]>=1.23",
16
+ "onnxruntime>=1.19",
17
+ "onnx>=1.16",
18
+ "matplotlib>=3.9",
19
+ "pyyaml>=6.0",
20
+ "tqdm>=4.66",
21
+ "huggingface_hub>=0.25",
22
+ "numpy>=1.26,<2.2",
23
+ "sentencepiece>=0.2",
24
+ "protobuf>=4.25,<5",
25
+ "Levenshtein>=0.25",
26
+ ]
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/cleanup"]
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=8.0",
38
+ ]
scripts/01_download.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # load the hand-crafted seed pairs at data/seed/synthetic_pairs.jsonl and
2
+ # split them stratified-by-category into train/val/test under data/pairs/.
3
+
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ from cleanup.config import load_data_config
8
+ from cleanup.data.download import build_and_save
9
+
10
+
11
+ def main() -> None:
12
+ parser = argparse.ArgumentParser()
13
+ parser.add_argument("--config", default="configs/data.yaml")
14
+ parser.add_argument("--out", default="data/pairs")
15
+ parser.add_argument("--smoke", action="store_true", help="tiny subset for wiring tests")
16
+ args = parser.parse_args()
17
+
18
+ cfg = load_data_config(args.config)
19
+ build_and_save(cfg, Path(args.out), smoke=args.smoke)
20
+
21
+
22
+ if __name__ == "__main__":
23
+ main()
scripts/02_train.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # lora sft on qwen2.5-0.5b-instruct. writes runs/<run-id>/model + training_history.json.
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from cleanup.config import load_train_config
7
+ from cleanup.train.trainer import train
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser()
12
+ parser.add_argument("--config", default="configs/train.yaml")
13
+ parser.add_argument("--runs-dir", default="runs")
14
+ parser.add_argument("--run-id", required=True)
15
+ parser.add_argument("--lr", type=float, default=None)
16
+ parser.add_argument("--epochs", type=int, default=None)
17
+ parser.add_argument("--smoke", action="store_true", help="tiny cpu validation run")
18
+ args = parser.parse_args()
19
+
20
+ cfg = load_train_config(args.config)
21
+ run_dir = Path(args.runs_dir) / args.run_id
22
+ summary = train(
23
+ cfg,
24
+ run_dir,
25
+ smoke=args.smoke,
26
+ epochs_override=args.epochs,
27
+ lr_override=args.lr,
28
+ )
29
+ print(summary)
30
+ print(f"next: make evaluate RUN_ID={args.run_id}")
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
scripts/03_evaluate.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 3-model quality comparison on the held-out test split:
2
+ # row 1: raw input (no cleanup) -> aggregate metrics
3
+ # row 2: qwen base zero-shot with our system prompt
4
+ # row 3: qwen + fine-tuned lora adapter
5
+ #
6
+ # also includes an ADVERSARIAL question check: the base model's documented
7
+ # failure was answering questions instead of cleaning them. we record base vs
8
+ # fine-tune output on a small list of question-shaped inputs so we can
9
+ # visually confirm fine-tune cleans rather than answers.
10
+ #
11
+ # writes runs/<run-id>/eval.json with all three rows plus adversarial.
12
+
13
+ import argparse
14
+ import json
15
+ from pathlib import Path
16
+
17
+ from cleanup.config import load_train_config
18
+ from cleanup.data.download import load_pairs
19
+ from cleanup.eval.metrics import (
20
+ evaluate_one,
21
+ make_qwen_generator,
22
+ make_raw_generator,
23
+ write_eval,
24
+ )
25
+
26
+
27
+ # the prototype's documented failure mode. the base model ANSWERS these
28
+ # instead of cleaning the disfluencies. fine-tune should output the cleaned
29
+ # question (with proper punct/case), not a reply. keep this list small but
30
+ # representative; extend as new failure modes surface.
31
+ ADVERSARIAL = [
32
+ "um whats the capital of france",
33
+ "can you can you write me a poem about the sea",
34
+ "so like what is two plus two i mean",
35
+ "uh how do i sort a list in python",
36
+ "hey what time is it in tokyo right now",
37
+ ]
38
+
39
+
40
+ def main() -> None:
41
+ parser = argparse.ArgumentParser()
42
+ parser.add_argument("--config", default="configs/train.yaml")
43
+ parser.add_argument("--data-dir", default="data/pairs")
44
+ parser.add_argument("--runs-dir", default="runs")
45
+ parser.add_argument("--run-id", required=True)
46
+ parser.add_argument("--max-rows", type=int, default=None)
47
+ parser.add_argument("--smoke", action="store_true")
48
+ parser.add_argument("--skip-base", action="store_true", help="skip qwen base baseline (saves time)")
49
+ args = parser.parse_args()
50
+
51
+ cfg = load_train_config(args.config)
52
+ run_dir = Path(args.runs_dir) / args.run_id
53
+ adapter_dir = run_dir / "model"
54
+ if not adapter_dir.exists():
55
+ raise FileNotFoundError(f"no adapter at {adapter_dir}; train first")
56
+
57
+ max_rows = 40 if args.smoke else args.max_rows
58
+ test_rows = load_pairs(args.data_dir, "test", max_rows)
59
+ print(f"[eval] {len(test_rows)} test rows")
60
+
61
+ report: dict = {}
62
+ print("[eval] row 1: raw baseline")
63
+ report["raw"] = evaluate_one(test_rows, make_raw_generator())
64
+
65
+ base_gen = None
66
+ if not args.skip_base:
67
+ print("[eval] row 2: qwen base zero-shot")
68
+ base_gen = make_qwen_generator(cfg.base_model)
69
+ report["base"] = evaluate_one(test_rows, base_gen)
70
+
71
+ print("[eval] row 3: qwen fine-tuned")
72
+ ft_gen = make_qwen_generator(cfg.base_model, adapter_path=str(adapter_dir))
73
+ report["fine_tuned"] = evaluate_one(test_rows, ft_gen)
74
+
75
+ # adversarial question check. record base vs fine-tune output side by side
76
+ # so we can visually confirm fine-tune does not answer the question.
77
+ print("[eval] adversarial: do questions get cleaned, not answered?")
78
+ adversarial_rows = []
79
+ for q in ADVERSARIAL:
80
+ row = {"raw": q}
81
+ if base_gen is not None:
82
+ row["base"] = base_gen(q)
83
+ row["fine_tuned"] = ft_gen(q)
84
+ adversarial_rows.append(row)
85
+ report["adversarial"] = adversarial_rows
86
+
87
+ write_eval(report, run_dir)
88
+ print(f"[eval] wrote {run_dir / 'eval.json'}")
89
+
90
+ print()
91
+ print("model | disfluency | punct f1 | faithful | pass rate")
92
+ for k in ("raw", "base", "fine_tuned"):
93
+ if k not in report:
94
+ continue
95
+ m = report[k]
96
+ d = m["disfluency_removal_rate"]
97
+ d_str = " n/a" if d is None else f"{d:.3f}"
98
+ print(
99
+ f"{k:<12} | {d_str:>9} | {m['punctuation_f1']:>8.3f} | "
100
+ f"{m['faithfulness_mean']:>8.3f} | {m['pass_rate']:>9.3f}"
101
+ )
102
+
103
+ print()
104
+ print("[eval] adversarial check (look for fine_tuned to CLEAN not ANSWER):")
105
+ for row in adversarial_rows:
106
+ print(f" raw : {row['raw']}")
107
+ if "base" in row:
108
+ print(f" base : {row['base']}")
109
+ print(f" fine_tuned : {row['fine_tuned']}")
110
+ print()
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
scripts/04_export.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # merge lora into base, export fp32 onnx, quantize to int8.
2
+ # writes runs/<run-id>/merged/, onnx/model.onnx, onnx/int8/model.onnx.
3
+
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ from cleanup.config import load_train_config
8
+ from cleanup.export.merge import merge_adapter
9
+ from cleanup.export.quantize import quantize_int8
10
+ from cleanup.export.to_onnx import export_onnx
11
+
12
+
13
+ def main() -> None:
14
+ parser = argparse.ArgumentParser()
15
+ parser.add_argument("--config", default="configs/train.yaml")
16
+ parser.add_argument("--runs-dir", default="runs")
17
+ parser.add_argument("--run-id", required=True)
18
+ parser.add_argument("--skip-int8", action="store_true")
19
+ parser.add_argument("--skip-onnx", action="store_true", help="merge only, no onnx export")
20
+ args = parser.parse_args()
21
+
22
+ cfg = load_train_config(args.config)
23
+ run_dir = Path(args.runs_dir) / args.run_id
24
+ adapter_dir = run_dir / "model"
25
+ merged_dir = run_dir / "merged"
26
+ onnx_dir = run_dir / "onnx"
27
+ int8_dir = onnx_dir / "int8"
28
+
29
+ merge_adapter(cfg, adapter_dir, merged_dir)
30
+ if args.skip_onnx:
31
+ print("[export] skipping onnx per --skip-onnx")
32
+ return
33
+ fp32_onnx = export_onnx(merged_dir, onnx_dir)
34
+ if not args.skip_int8:
35
+ quantize_int8(fp32_onnx, int8_dir)
36
+ print(f"next: make benchmark RUN_ID={args.run_id} (LOCAL cpu only)")
37
+
38
+
39
+ if __name__ == "__main__":
40
+ main()
scripts/05_benchmark.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cpu latency benchmark for the exported onnx. RUN LOCALLY on the target
2
+ # laptop. gpu timings are not informative because deployment is cpu-only via
3
+ # the rust ort runtime. mirrors privacy-filter/scripts/05_benchmark.py.
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from transformers import AutoTokenizer
10
+
11
+ from cleanup.eval.latency import benchmark_latency, benchmark_realistic
12
+
13
+
14
+ def _resolve_model_path(run_dir: Path, which: str) -> Path:
15
+ if which == "int8":
16
+ int8_path = run_dir / "onnx" / "int8" / "model.onnx"
17
+ if not int8_path.exists():
18
+ raise FileNotFoundError(f"no int8 onnx at {int8_path}; run scripts/04_export.py")
19
+ return int8_path
20
+ fp32_path = run_dir / "onnx" / "model.onnx"
21
+ if not fp32_path.exists():
22
+ raise FileNotFoundError(f"no fp32 onnx at {fp32_path}; run scripts/04_export.py")
23
+ return fp32_path
24
+
25
+
26
+ def _load_realistic_texts(data_dir: Path, n: int) -> list[str]:
27
+ test_path = Path(data_dir) / "test.json"
28
+ if not test_path.exists():
29
+ return []
30
+ rows = json.loads(test_path.read_text())
31
+ if n < len(rows):
32
+ rows = rows[:n]
33
+ return [r["raw"] for r in rows]
34
+
35
+
36
+ def main() -> None:
37
+ parser = argparse.ArgumentParser()
38
+ parser.add_argument("--runs-dir", default="runs")
39
+ parser.add_argument("--run-id", required=True)
40
+ parser.add_argument("--data-dir", default="data/pairs")
41
+ parser.add_argument("--model", choices=["fp32", "int8"], default="fp32")
42
+ parser.add_argument("--threads", type=int, default=4)
43
+ parser.add_argument("--warmup", type=int, default=50)
44
+ parser.add_argument("--measure", type=int, default=500)
45
+ parser.add_argument("--realistic-samples", type=int, default=500)
46
+ args = parser.parse_args()
47
+
48
+ run_dir = Path(args.runs_dir) / args.run_id
49
+ model_path = _resolve_model_path(run_dir, args.model)
50
+ print(f"[bench] {args.model} model: {model_path}")
51
+ print(f"[bench] file size {model_path.stat().st_size / 1e6:.1f} MB")
52
+
53
+ tokenizer = AutoTokenizer.from_pretrained(run_dir / "model", use_fast=True)
54
+
55
+ print("[bench] fixed length sweep")
56
+ sweep = benchmark_latency(
57
+ onnx_path=model_path,
58
+ tokenizer=tokenizer,
59
+ warmup=args.warmup,
60
+ measure=args.measure,
61
+ intra_op_threads=args.threads,
62
+ )
63
+
64
+ realistic = None
65
+ texts = _load_realistic_texts(Path(args.data_dir), args.realistic_samples)
66
+ if texts:
67
+ print(f"[bench] realistic mix on {len(texts)} real test rows")
68
+ realistic = benchmark_realistic(
69
+ onnx_path=model_path,
70
+ tokenizer=tokenizer,
71
+ texts=texts,
72
+ intra_op_threads=args.threads,
73
+ )
74
+
75
+ out_path = run_dir / "latency_benchmark.json"
76
+ out_path.write_text(json.dumps(
77
+ {
78
+ "model": args.model,
79
+ "model_path": str(model_path),
80
+ "model_size_bytes": model_path.stat().st_size,
81
+ "intra_op_threads": args.threads,
82
+ "results_by_length": sweep,
83
+ "realistic_mix": realistic,
84
+ },
85
+ indent=2,
86
+ ))
87
+ print(f"[bench] wrote {out_path}")
88
+
89
+ print()
90
+ print("length | p50 ms | p95 ms | p99 ms | mean ms")
91
+ for length, stats in sweep.items():
92
+ print(
93
+ f"{length:>6s} | {stats['p50_ms']:>6.2f} | {stats['p95_ms']:>6.2f} | "
94
+ f"{stats['p99_ms']:>6.2f} | {stats['mean_ms']:>7.2f}"
95
+ )
96
+ if realistic:
97
+ print()
98
+ print(
99
+ f"realistic mix ({realistic['samples']} rows, "
100
+ f"p50 length {realistic['token_length_p50']}): "
101
+ f"p50={realistic['p50_ms']:.2f}ms p95={realistic['p95_ms']:.2f}ms "
102
+ f"p99={realistic['p99_ms']:.2f}ms"
103
+ )
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
scripts/06_pack_and_ship.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tar runs/<run-id>/ with model + onnx + eval json + manifest, print sha256
2
+ # and the vastai copy command for transfer back to the laptop.
3
+
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ from cleanup.pack.ship import pack
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser()
12
+ parser.add_argument("--runs-dir", default="runs")
13
+ parser.add_argument("--dist-dir", default="dist")
14
+ parser.add_argument("--run-id", required=True)
15
+ args = parser.parse_args()
16
+
17
+ run_dir = Path(args.runs_dir) / args.run_id
18
+ if not run_dir.exists():
19
+ raise FileNotFoundError(f"no run at {run_dir}")
20
+ pack(run_dir, Path(args.dist_dir), args.run_id)
21
+
22
+
23
+ if __name__ == "__main__":
24
+ main()
scripts/explore_data.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # explore the synthetic seed dataset and produce docs/data_report.md plus a
2
+ # set of supporting charts under docs/data_images/. mirrors the privacy-filter
3
+ # explore_data.py pattern. cpu only, no model needed.
4
+ #
5
+ # usage: uv run python scripts/explore_data.py
6
+
7
+ import json
8
+ import re
9
+ from collections import Counter
10
+ from pathlib import Path
11
+
12
+ import matplotlib
13
+ matplotlib.use("Agg")
14
+ import matplotlib.pyplot as plt
15
+ import numpy as np
16
+
17
+ SEED_PATH = Path("data/seed/synthetic_pairs.jsonl")
18
+ OUT_DOC = Path("docs/data_report.md")
19
+ OUT_IMAGES = Path("docs/data_images")
20
+
21
+ FILLERS = {"um", "uh", "er", "ah", "like", "you know", "i mean", "so", "well"}
22
+
23
+
24
+ def load_rows() -> list[dict]:
25
+ rows = []
26
+ with open(SEED_PATH, "r", encoding="utf-8") as f:
27
+ for line in f:
28
+ line = line.strip()
29
+ if not line:
30
+ continue
31
+ rows.append(json.loads(line))
32
+ return rows
33
+
34
+
35
+ def word_count(s: str) -> int:
36
+ return len(s.split())
37
+
38
+
39
+ def filler_count(text: str) -> int:
40
+ lower = " " + text.lower() + " "
41
+ return sum(len(re.findall(rf"(?<!\w){re.escape(f)}(?!\w)", lower)) for f in FILLERS)
42
+
43
+
44
+ def content_words(text: str) -> set:
45
+ # lowercase, drop punctuation, return a set of content tokens.
46
+ stripped = re.sub(r"[^\w\s']", " ", text.lower())
47
+ return set(stripped.split())
48
+
49
+
50
+ def faithfulness(raw: str, clean: str) -> float:
51
+ # what fraction of clean's content words also appear in raw. measures the
52
+ # by-construction faithfulness of the dataset. a low value means the clean
53
+ # invented vocabulary that was not in raw, which would teach the model to
54
+ # hallucinate.
55
+ rw = content_words(raw)
56
+ cw = content_words(clean)
57
+ if not cw:
58
+ return 1.0
59
+ return len(cw & rw) / len(cw)
60
+
61
+
62
+ def plot_category_counts(rows: list[dict]):
63
+ counts = Counter(r["category"] for r in rows)
64
+ order = sorted(counts, key=counts.get, reverse=True)
65
+ values = [counts[c] for c in order]
66
+ fig, ax = plt.subplots(figsize=(8, 4.2))
67
+ bars = ax.bar(order, values, color="#5b8cb8")
68
+ ax.set_title("pairs per category")
69
+ ax.set_ylabel("count")
70
+ ax.set_xticklabels(order, rotation=22, ha="right")
71
+ for b, v in zip(bars, values):
72
+ ax.text(b.get_x() + b.get_width() / 2, v + 1, str(v), ha="center", fontsize=9)
73
+ ax.grid(True, axis="y", alpha=0.3)
74
+ fig.tight_layout()
75
+ fig.savefig(OUT_IMAGES / "category_counts.png", dpi=130)
76
+ plt.close(fig)
77
+
78
+
79
+ def plot_length_distribution(rows: list[dict]):
80
+ cats = sorted({r["category"] for r in rows})
81
+ fig, ax = plt.subplots(figsize=(9, 4.8))
82
+ data_raw = [[word_count(r["raw"]) for r in rows if r["category"] == c] for c in cats]
83
+ parts = ax.violinplot(data_raw, showmeans=False, showmedians=True)
84
+ for pc in parts["bodies"]:
85
+ pc.set_facecolor("#5b8cb8")
86
+ pc.set_alpha(0.65)
87
+ ax.set_xticks(range(1, len(cats) + 1))
88
+ ax.set_xticklabels(cats, rotation=22, ha="right")
89
+ ax.set_ylabel("raw side word count")
90
+ ax.set_title("input length distribution by category")
91
+ ax.grid(True, axis="y", alpha=0.3)
92
+ fig.tight_layout()
93
+ fig.savefig(OUT_IMAGES / "length_distribution.png", dpi=130)
94
+ plt.close(fig)
95
+
96
+
97
+ def plot_raw_vs_clean_length(rows: list[dict]):
98
+ raw_lens = [word_count(r["raw"]) for r in rows]
99
+ clean_lens = [word_count(r["clean"]) for r in rows]
100
+ fig, ax = plt.subplots(figsize=(6.5, 6))
101
+ ax.scatter(raw_lens, clean_lens, alpha=0.32, s=14, color="#5b8cb8")
102
+ lo = 0
103
+ hi = max(max(raw_lens), max(clean_lens))
104
+ ax.plot([lo, hi], [lo, hi], "k--", alpha=0.4, linewidth=1)
105
+ ax.set_xlabel("raw word count")
106
+ ax.set_ylabel("clean word count")
107
+ ax.set_title("raw vs clean length (clean below diagonal is expected)")
108
+ ax.grid(True, alpha=0.3)
109
+ fig.tight_layout()
110
+ fig.savefig(OUT_IMAGES / "raw_vs_clean_length.png", dpi=130)
111
+ plt.close(fig)
112
+
113
+
114
+ def plot_filler_intensity(rows: list[dict]):
115
+ cats = sorted({r["category"] for r in rows})
116
+ means = [
117
+ np.mean([filler_count(r["raw"]) for r in rows if r["category"] == c]) for c in cats
118
+ ]
119
+ fig, ax = plt.subplots(figsize=(8, 4.2))
120
+ bars = ax.bar(cats, means, color="#c08a55")
121
+ ax.set_title("average filler count per raw input by category")
122
+ ax.set_ylabel("avg fillers per pair")
123
+ ax.set_xticklabels(cats, rotation=22, ha="right")
124
+ for b, v in zip(bars, means):
125
+ ax.text(b.get_x() + b.get_width() / 2, v + 0.05, f"{v:.1f}", ha="center", fontsize=9)
126
+ ax.grid(True, axis="y", alpha=0.3)
127
+ fig.tight_layout()
128
+ fig.savefig(OUT_IMAGES / "filler_intensity.png", dpi=130)
129
+ plt.close(fig)
130
+
131
+
132
+ def plot_top_fillers(rows: list[dict]):
133
+ counts: Counter = Counter()
134
+ for r in rows:
135
+ lower = " " + r["raw"].lower() + " "
136
+ for f in FILLERS:
137
+ counts[f] += len(re.findall(rf"(?<!\w){re.escape(f)}(?!\w)", lower))
138
+ items = counts.most_common()
139
+ labels, values = zip(*items)
140
+ fig, ax = plt.subplots(figsize=(7, 4.2))
141
+ ax.barh(labels[::-1], values[::-1], color="#5b8cb8")
142
+ ax.set_title("top fillers across all raw inputs")
143
+ ax.set_xlabel("total occurrences in raw")
144
+ ax.grid(True, axis="x", alpha=0.3)
145
+ fig.tight_layout()
146
+ fig.savefig(OUT_IMAGES / "top_fillers.png", dpi=130)
147
+ plt.close(fig)
148
+
149
+
150
+ def plot_faithfulness(rows: list[dict]):
151
+ vals = [faithfulness(r["raw"], r["clean"]) for r in rows]
152
+ fig, ax = plt.subplots(figsize=(7, 4.2))
153
+ ax.hist(vals, bins=24, color="#5b8cb8", edgecolor="white")
154
+ ax.axvline(0.95, color="red", linestyle="--", linewidth=1, label="0.95 threshold")
155
+ ax.set_title("faithfulness: fraction of clean content words present in raw")
156
+ ax.set_xlabel("faithfulness score (1.0 = perfect)")
157
+ ax.set_ylabel("number of pairs")
158
+ ax.legend()
159
+ ax.grid(True, axis="y", alpha=0.3)
160
+ fig.tight_layout()
161
+ fig.savefig(OUT_IMAGES / "faithfulness.png", dpi=130)
162
+ plt.close(fig)
163
+
164
+
165
+ def write_report(rows: list[dict], stats: dict):
166
+ lines: list[str] = []
167
+ lines.append("# data report")
168
+ lines.append("")
169
+ lines.append("The synthetic seed dataset that backs the Mumble cleanup model. Built by a multi-agent workflow that spawned 8 specialist agents in parallel and produced 612 pairs across 8 dictation categories; a polish pass added 76 more `long_form_thoughts` pairs with strictly diverse openers, bringing the total to **688 pairs**.")
170
+ lines.append("")
171
+ lines.append("Every pair is `{ raw: <Parakeet-shaped lowercase no-punct disfluent input>, clean: <proper English output> }`. The clean side is faithful by construction: every content word in `clean` exists in `raw` (modulo standard homophone fixes, contractions, and casing). This is what stops the model from learning to hallucinate.")
172
+ lines.append("")
173
+ lines.append("## category mix")
174
+ lines.append("")
175
+ lines.append("![category counts](data_images/category_counts.png)")
176
+ lines.append("")
177
+ cats = sorted({r["category"] for r in rows})
178
+ cat_counts = Counter(r["category"] for r in rows)
179
+ lines.append("| category | count |")
180
+ lines.append("|---|---:|")
181
+ for c in cats:
182
+ lines.append(f"| `{c}` | {cat_counts[c]} |")
183
+ lines.append(f"| **total** | **{len(rows)}** |")
184
+ lines.append("")
185
+ lines.append("`long_form_thoughts` is intentionally over-weighted because paragraph-length cleanup is the hardest behavior (multiple sentence boundaries, sustained context, false starts) and 145 examples gives the model the signal it needs to handle 60-90 word inputs.")
186
+ lines.append("")
187
+ lines.append("## length distribution")
188
+ lines.append("")
189
+ lines.append("![length distribution](data_images/length_distribution.png)")
190
+ lines.append("")
191
+ lines.append(f"Raw inputs span **{stats['raw_min']} to {stats['raw_max']} words** with a median of **{stats['raw_median']:.0f}**. Clean outputs are slightly shorter on average ({stats['clean_median']:.0f} median words) because they have fillers and stutters removed. The categories show meaningfully different length distributions: short utterances dominate `casual_messages`, `questions_and_asks`, and `mixed_content`; long paragraph-shaped inputs dominate `long_form_thoughts`.")
192
+ lines.append("")
193
+ lines.append("## raw vs clean length")
194
+ lines.append("")
195
+ lines.append("![raw vs clean length](data_images/raw_vs_clean_length.png)")
196
+ lines.append("")
197
+ lines.append("Points below the diagonal mean clean is shorter than raw — the model is being trained to remove material, not add it. The cluster sits just below the diagonal, which is the expected shape for a faithful cleanup task: a few words removed per input on average, never more than ~25%.")
198
+ lines.append("")
199
+ lines.append("## disfluency intensity")
200
+ lines.append("")
201
+ lines.append("![filler intensity by category](data_images/filler_intensity.png)")
202
+ lines.append("")
203
+ lines.append("Average filler-word count per raw input, by category. `meeting_notes` and `long_form_thoughts` carry the heaviest disfluency load (people think out loud during meetings); `mixed_content` and `questions_and_asks` are leanest (those categories are about precision, not verbosity).")
204
+ lines.append("")
205
+ lines.append("![top fillers](data_images/top_fillers.png)")
206
+ lines.append("")
207
+ lines.append("Distribution of filler words across the entire dataset. `um` and `uh` dominate (matching real Parakeet output), with `like`, `you know`, and `so` following at a moderate rate. The mix matches what shows up in real dictation transcripts.")
208
+ lines.append("")
209
+ lines.append("## faithfulness check")
210
+ lines.append("")
211
+ lines.append("![faithfulness distribution](data_images/faithfulness.png)")
212
+ lines.append("")
213
+ lines.append("For each pair, we compute the fraction of content words in the clean side that also appear in the raw side. A perfect value is 1.0 (every clean content word came from raw); lower values indicate the clean introduced content the raw did not have, which would train the model to hallucinate.")
214
+ lines.append("")
215
+ lines.append(f"- **Mean faithfulness**: {stats['faith_mean']:.3f}")
216
+ lines.append(f"- **Median faithfulness**: {stats['faith_median']:.3f}")
217
+ lines.append(f"- **Pairs above 0.95 threshold**: {stats['faith_pass']} of {len(rows)} ({100 * stats['faith_pass'] / len(rows):.1f}%)")
218
+ lines.append(f"- **Pairs above 0.90 threshold**: {stats['faith_90']} of {len(rows)} ({100 * stats['faith_90'] / len(rows):.1f}%)")
219
+ lines.append("")
220
+ lines.append("Small drops below 1.0 come from legitimate sources: number-word to digit conversion (\"two thirty\" -> \"2:30\"), proper-noun capitalization that adds new tokens to the content-word set under our simple lowercase comparison (\"acme\" -> \"Acme\" should be counted as matching but our naive check might miss some), and contractions (\"i\" -> \"I'm\" via apostrophe restoration).")
221
+ lines.append("")
222
+ lines.append("## sample pairs")
223
+ lines.append("")
224
+ lines.append("Two per category, illustrating the shape of the dataset:")
225
+ lines.append("")
226
+ for cat in cats:
227
+ cat_rows = [r for r in rows if r["category"] == cat]
228
+ samples = [cat_rows[0], cat_rows[len(cat_rows) // 2]] if len(cat_rows) >= 2 else cat_rows
229
+ lines.append(f"### `{cat}`")
230
+ lines.append("")
231
+ for s in samples:
232
+ lines.append(f"- **raw**: `{s['raw']}`")
233
+ lines.append(f"- **clean**: {s['clean']}")
234
+ lines.append("")
235
+ lines.append("## limitations")
236
+ lines.append("")
237
+ lines.append("- **Synthetic origin**: every pair was generated by an LLM workflow, not transcribed from real Parakeet output. The disfluency patterns are modeled to match real ASR failure modes but may under-represent edge cases the model will face in production.")
238
+ lines.append("- **Size**: 688 pairs is on the lower-middle end of the documented sweet spot for narrow LoRA fine-tunes (200-500 floor, 2k-5k comfortable). Adequate for a v1 ship; if eval pass rate is below 0.85 we regenerate another 600-1000 pairs and retrain.")
239
+ lines.append("- **Faithfulness is statistical, not strict**: a few pairs may drop below 0.95 because of legitimate transformations (numeric formatting, proper-noun casing). We don't filter these out because the training task explicitly wants the model to learn those transformations.")
240
+ lines.append("- **English only.**")
241
+ lines.append("")
242
+ OUT_DOC.parent.mkdir(parents=True, exist_ok=True)
243
+ OUT_DOC.write_text("\n".join(lines), encoding="utf-8")
244
+
245
+
246
+ def main() -> None:
247
+ OUT_IMAGES.mkdir(parents=True, exist_ok=True)
248
+ rows = load_rows()
249
+ print(f"loaded {len(rows)} pairs from {SEED_PATH}")
250
+
251
+ plot_category_counts(rows)
252
+ plot_length_distribution(rows)
253
+ plot_raw_vs_clean_length(rows)
254
+ plot_filler_intensity(rows)
255
+ plot_top_fillers(rows)
256
+ plot_faithfulness(rows)
257
+
258
+ raw_lens = [word_count(r["raw"]) for r in rows]
259
+ clean_lens = [word_count(r["clean"]) for r in rows]
260
+ faith = [faithfulness(r["raw"], r["clean"]) for r in rows]
261
+ stats = {
262
+ "raw_min": min(raw_lens),
263
+ "raw_max": max(raw_lens),
264
+ "raw_median": float(np.median(raw_lens)),
265
+ "clean_min": min(clean_lens),
266
+ "clean_max": max(clean_lens),
267
+ "clean_median": float(np.median(clean_lens)),
268
+ "faith_mean": float(np.mean(faith)),
269
+ "faith_median": float(np.median(faith)),
270
+ "faith_pass": sum(1 for v in faith if v >= 0.95),
271
+ "faith_90": sum(1 for v in faith if v >= 0.90),
272
+ }
273
+ print(f"stats: {stats}")
274
+ write_report(rows, stats)
275
+ print(f"wrote {OUT_DOC} and {OUT_IMAGES}/")
276
+
277
+
278
+ if __name__ == "__main__":
279
+ main()
scripts/push_code_to_hub.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """push the cleanup code + seed dataset to a fresh huggingface hub repo.
2
+
3
+ does not need a trained model. uploads:
4
+ - src/, scripts/, configs/, tests/, data/, docs/
5
+ - pyproject.toml, Makefile
6
+ - docs/model_card.md mirrored to README.md at repo root
7
+ - existing cleanup/README.md renamed to DEVELOPMENT.md
8
+
9
+ reads the token from the mumble repo root .env.local. prefers
10
+ HUGGINGFACE_ACCESS_TOKEN, falls back to HF_TOKEN.
11
+
12
+ usage: python models/cleanup/scripts/push_code_to_hub.py [--repo NAME] [--private]
13
+ """
14
+
15
+ import argparse
16
+ import shutil
17
+ import sys
18
+ import tempfile
19
+ from pathlib import Path
20
+
21
+ from huggingface_hub import HfApi
22
+ from huggingface_hub.errors import HfHubHTTPError
23
+
24
+ SKIP_DIRS = {
25
+ ".venv",
26
+ "runs",
27
+ "dist",
28
+ "__pycache__",
29
+ ".pytest_cache",
30
+ ".mypy_cache",
31
+ ".ruff_cache",
32
+ "node_modules",
33
+ ".uv",
34
+ }
35
+ SKIP_SUFFIXES = {".pyc", ".pyo"}
36
+
37
+
38
+ def find_repo_root(start: Path) -> Path:
39
+ for p in [start, *start.parents]:
40
+ if (p / ".env.local").exists():
41
+ return p
42
+ raise FileNotFoundError(".env.local not found in any parent of " + str(start))
43
+
44
+
45
+ def load_token(env_path: Path) -> str:
46
+ for raw in env_path.read_text(encoding="utf-8").splitlines():
47
+ line = raw.strip()
48
+ if not line or line.startswith("#") or "=" not in line:
49
+ continue
50
+ key, _, value = line.partition("=")
51
+ key = key.strip()
52
+ value = value.strip().strip('"').strip("'")
53
+ if key in {"HUGGINGFACE_ACCESS_TOKEN", "HF_TOKEN"} and value:
54
+ return value
55
+ raise KeyError("no HUGGINGFACE_ACCESS_TOKEN or HF_TOKEN in " + str(env_path))
56
+
57
+
58
+ def stage_upload(source: Path, staging: Path) -> int:
59
+ count = 0
60
+ for path in source.rglob("*"):
61
+ if path.is_dir():
62
+ continue
63
+ rel = path.relative_to(source)
64
+ if SKIP_DIRS & set(rel.parts):
65
+ continue
66
+ if path.suffix in SKIP_SUFFIXES:
67
+ continue
68
+ if rel == Path("README.md"):
69
+ target_rel = Path("DEVELOPMENT.md")
70
+ else:
71
+ target_rel = rel
72
+ target = staging / target_rel
73
+ target.parent.mkdir(parents=True, exist_ok=True)
74
+ shutil.copy2(path, target)
75
+ count += 1
76
+
77
+ model_card = source / "docs" / "model_card.md"
78
+ if model_card.exists():
79
+ shutil.copy2(model_card, staging / "README.md")
80
+ count += 1
81
+ else:
82
+ print("warn: docs/model_card.md missing; hub page will lack a README", file=sys.stderr)
83
+
84
+ (staging / ".gitattributes").write_text(
85
+ "*.jsonl text\n*.csv text\n*.md text\n*.py text\n*.yaml text\n*.toml text\n",
86
+ encoding="utf-8",
87
+ )
88
+ return count + 1
89
+
90
+
91
+ def main() -> int:
92
+ parser = argparse.ArgumentParser()
93
+ parser.add_argument("--repo", default="mumble-cleanup", help="repo name under the authed user")
94
+ parser.add_argument("--private", action="store_true", help="create as private (default public)")
95
+ args = parser.parse_args()
96
+
97
+ here = Path(__file__).resolve()
98
+ repo_root = find_repo_root(here.parent)
99
+ cleanup_dir = repo_root / "models" / "cleanup"
100
+ env_path = repo_root / ".env.local"
101
+
102
+ if not cleanup_dir.exists():
103
+ print(f"error: {cleanup_dir} does not exist", file=sys.stderr)
104
+ return 1
105
+
106
+ token = load_token(env_path)
107
+ api = HfApi(token=token)
108
+ me = api.whoami()
109
+ user = me.get("name") or (me.get("email") or "").split("@")[0]
110
+ if not user:
111
+ print("error: could not resolve hf username from whoami()", file=sys.stderr)
112
+ return 1
113
+
114
+ repo_id = f"{user}/{args.repo}"
115
+ print(f"hf user : {user}")
116
+ print(f"repo target : {repo_id}")
117
+ print(f"visibility : {'private' if args.private else 'public'}")
118
+ print(f"source : {cleanup_dir}")
119
+
120
+ try:
121
+ url = api.create_repo(
122
+ repo_id=repo_id,
123
+ private=args.private,
124
+ repo_type="model",
125
+ exist_ok=True,
126
+ )
127
+ print(f"repo ready : {url}")
128
+ except HfHubHTTPError as exc:
129
+ print(f"error: failed to create repo: {exc}", file=sys.stderr)
130
+ return 1
131
+
132
+ with tempfile.TemporaryDirectory(prefix="mumble-cleanup-hub-") as tmp:
133
+ staging = Path(tmp)
134
+ count = stage_upload(cleanup_dir, staging)
135
+ print(f"staged files : {count}")
136
+
137
+ print("uploading ...")
138
+ api.upload_folder(
139
+ folder_path=str(staging),
140
+ repo_id=repo_id,
141
+ repo_type="model",
142
+ commit_message="initial upload: cleanup code and 688-pair seed dataset",
143
+ )
144
+
145
+ print(f"\ndone. browse: https://huggingface.co/{repo_id}")
146
+ return 0
147
+
148
+
149
+ if __name__ == "__main__":
150
+ raise SystemExit(main())
scripts/push_to_hub.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # push the trained cleanup model to the huggingface hub.
2
+ # uploads the merged transformers model + fp32 onnx + int8 onnx + model card
3
+ # in one commit. needs HF_TOKEN in .env.local (gitignored).
4
+ #
5
+ # usage: uv run python scripts/push_to_hub.py --run-id r1
6
+ # by default the repo is private. pass --public to flip it.
7
+
8
+ import argparse
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from huggingface_hub import CommitOperationAdd, HfApi
14
+
15
+ # files copied from runs/<run-id>/merged into the repo root.
16
+ MODEL_FILES = [
17
+ "config.json",
18
+ "model.safetensors",
19
+ "tokenizer.json",
20
+ "tokenizer_config.json",
21
+ "special_tokens_map.json",
22
+ "vocab.json",
23
+ "merges.txt",
24
+ ]
25
+
26
+
27
+ def _load_dotenv(path: Path) -> None:
28
+ if not path.exists():
29
+ return
30
+ for line in path.read_text(encoding="utf-8").splitlines():
31
+ line = line.strip()
32
+ if not line or line.startswith("#") or "=" not in line:
33
+ continue
34
+ key, value = line.split("=", 1)
35
+ key = key.strip()
36
+ value = value.strip().strip('"').strip("'")
37
+ if key and key not in os.environ:
38
+ os.environ[key] = value
39
+
40
+
41
+ def main() -> None:
42
+ parser = argparse.ArgumentParser(description="publish cleanup model to the hf hub")
43
+ parser.add_argument("--runs-dir", default="runs")
44
+ parser.add_argument("--run-id", required=True)
45
+ parser.add_argument("--repo", default="adikuma/mumble-cleanup")
46
+ parser.add_argument("--card", default="docs/model_card.md")
47
+ parser.add_argument("--public", action="store_true", help="make the repo public")
48
+ args = parser.parse_args()
49
+
50
+ _load_dotenv(Path(".env.local"))
51
+ token = os.environ.get("HF_TOKEN")
52
+ if not token:
53
+ print("error: HF_TOKEN not set. add it to .env.local", file=sys.stderr)
54
+ sys.exit(1)
55
+
56
+ run_dir = Path(args.runs_dir) / args.run_id
57
+ merged_dir = run_dir / "merged"
58
+ fp32_onnx = run_dir / "onnx" / "model.onnx"
59
+ int8_onnx = run_dir / "onnx" / "int8" / "model.onnx"
60
+ card_path = Path(args.card)
61
+
62
+ missing = [str(merged_dir / n) for n in MODEL_FILES if not (merged_dir / n).exists()]
63
+ if not fp32_onnx.exists():
64
+ missing.append(str(fp32_onnx))
65
+ if not int8_onnx.exists():
66
+ # int8 is optional but warn loudly
67
+ print(f"warn: no int8 onnx at {int8_onnx}; uploading fp32 only")
68
+ if not card_path.exists():
69
+ missing.append(str(card_path))
70
+ if missing:
71
+ print("error: missing files:", file=sys.stderr)
72
+ for path in missing:
73
+ print(f" {path}", file=sys.stderr)
74
+ sys.exit(1)
75
+
76
+ api = HfApi(token=token)
77
+ private = not args.public
78
+
79
+ print(f"[hub] creating repo {args.repo} (private={private})")
80
+ api.create_repo(args.repo, repo_type="model", private=private, exist_ok=True)
81
+
82
+ uploads = {"README.md": card_path}
83
+ for name in MODEL_FILES:
84
+ local = merged_dir / name
85
+ if local.exists():
86
+ uploads[name] = local
87
+ uploads["onnx/model.onnx"] = fp32_onnx
88
+ if int8_onnx.exists():
89
+ uploads["onnx/int8/model.onnx"] = int8_onnx
90
+
91
+ total_mb = sum(p.stat().st_size for p in uploads.values()) / 1e6
92
+ print(f"[hub] uploading {len(uploads)} files, {total_mb:.0f} MB")
93
+
94
+ operations = [
95
+ CommitOperationAdd(path_in_repo=repo_path, path_or_fileobj=str(local_path))
96
+ for repo_path, local_path in uploads.items()
97
+ ]
98
+ api.create_commit(
99
+ repo_id=args.repo,
100
+ repo_type="model",
101
+ operations=operations,
102
+ commit_message="add mumble cleanup model, fp32 + int8 onnx, model card",
103
+ )
104
+
105
+ print()
106
+ print(f"[hub] done: https://huggingface.co/{args.repo}")
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
src/cleanup/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """dictation cleanup model: fine-tune a small llm to clean mumble transcripts."""
src/cleanup/config.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # typed dataclass wrappers over the yaml configs. consumed by every entry
2
+ # point so hyperparameters never sneak into code.
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import yaml
9
+
10
+
11
+ # ---------- training ----------
12
+
13
+ @dataclass
14
+ class LoraConfig:
15
+ r: int
16
+ alpha: int
17
+ dropout: float
18
+ bias: str
19
+ target_modules: list
20
+
21
+
22
+ @dataclass
23
+ class TrainConfig:
24
+ base_model: str
25
+ language: str
26
+ data_dir: str
27
+ max_seq_length: int
28
+ train_rows: Optional[int]
29
+ val_rows: Optional[int]
30
+ lora: LoraConfig
31
+ learning_rate: float
32
+ weight_decay: float
33
+ warmup_ratio: float
34
+ max_grad_norm: float
35
+ adam_beta1: float
36
+ adam_beta2: float
37
+ adam_epsilon: float
38
+ num_epochs: int
39
+ lr_scheduler_type: str
40
+ train_batch_size: int
41
+ eval_batch_size: int
42
+ gradient_accumulation_steps: int
43
+ bf16: bool
44
+ fp16: bool
45
+ tf32: bool
46
+ seed: int
47
+ metric_for_best_model: str
48
+ greater_is_better: bool
49
+ save_total_limit: int
50
+ logging_steps: int
51
+ eval_steps: int
52
+ save_steps: int
53
+ dataloader_num_workers: int
54
+
55
+
56
+ def load_train_config(path) -> TrainConfig:
57
+ raw = yaml.safe_load(Path(path).read_text())
58
+ lora = LoraConfig(**raw.pop("lora"))
59
+ return TrainConfig(lora=lora, **raw)
60
+
61
+
62
+ # ---------- data ----------
63
+
64
+ @dataclass
65
+ class DataSplits:
66
+ train: float
67
+ val: float
68
+ test: float
69
+
70
+
71
+ @dataclass
72
+ class DataConfig:
73
+ seed_path: str
74
+ splits: DataSplits
75
+ random_seed: int
76
+
77
+
78
+ def load_data_config(path) -> DataConfig:
79
+ raw = yaml.safe_load(Path(path).read_text())
80
+ splits = DataSplits(**raw.pop("splits"))
81
+ return DataConfig(splits=splits, **raw)
82
+
83
+
84
+ # ---------- optional inject (v1.1) ----------
85
+
86
+ @dataclass
87
+ class InjectSampling:
88
+ ops_per_example_min: int
89
+ ops_per_example_max: int
90
+
91
+
92
+ @dataclass
93
+ class InjectConfig:
94
+ sampling: InjectSampling
95
+ ops: dict = field(default_factory=dict)
96
+
97
+
98
+ def load_inject_config(path) -> InjectConfig:
99
+ raw = yaml.safe_load(Path(path).read_text())
100
+ sampling = InjectSampling(**raw.pop("sampling"))
101
+ return InjectConfig(sampling=sampling, **raw)
src/cleanup/data/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """data preparation: download, inject, tokenize."""
src/cleanup/data/download.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # load the hand-crafted seed jsonl, split into train/val/test, persist to
2
+ # data/pairs/. v1 does not download anything; the seed was produced by an
3
+ # off-line workflow under data/seed/synthetic_pairs.jsonl.
4
+ #
5
+ # outputs:
6
+ # data/pairs/train.json list of {"raw": str, "clean": str, "category": str}
7
+ # data/pairs/val.json
8
+ # data/pairs/test.json
9
+ # data/pairs/meta.json counts, seed, source path
10
+
11
+ import json
12
+ import random
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ from cleanup.config import DataConfig
17
+
18
+
19
+ def _load_seed(path: Path) -> list[dict]:
20
+ rows: list[dict] = []
21
+ with open(path, "r", encoding="utf-8") as f:
22
+ for line in f:
23
+ line = line.strip()
24
+ if not line:
25
+ continue
26
+ obj = json.loads(line)
27
+ if "raw" not in obj or "clean" not in obj:
28
+ continue
29
+ rows.append(
30
+ {
31
+ "raw": obj["raw"],
32
+ "clean": obj["clean"],
33
+ "category": obj.get("category", "uncategorized"),
34
+ }
35
+ )
36
+ return rows
37
+
38
+
39
+ def _split(rows: list[dict], splits, rng: random.Random) -> tuple[list, list, list]:
40
+ # stratified by category so each split sees a balanced category mix.
41
+ # this matters because counts per category are different (~70 to ~80
42
+ # each) and we do not want a small category absent from val or test.
43
+ by_cat: dict[str, list[dict]] = {}
44
+ for r in rows:
45
+ by_cat.setdefault(r["category"], []).append(r)
46
+ train: list[dict] = []
47
+ val: list[dict] = []
48
+ test: list[dict] = []
49
+ for cat, cat_rows in by_cat.items():
50
+ rng.shuffle(cat_rows)
51
+ n = len(cat_rows)
52
+ n_val = max(1, int(round(n * splits.val)))
53
+ n_test = max(1, int(round(n * splits.test)))
54
+ n_train = n - n_val - n_test
55
+ train += cat_rows[:n_train]
56
+ val += cat_rows[n_train : n_train + n_val]
57
+ test += cat_rows[n_train + n_val :]
58
+ rng.shuffle(train)
59
+ rng.shuffle(val)
60
+ rng.shuffle(test)
61
+ return train, val, test
62
+
63
+
64
+ def build_and_save(cfg: DataConfig, out_dir: Path, smoke: bool = False) -> dict:
65
+ out_dir = Path(out_dir)
66
+ out_dir.mkdir(parents=True, exist_ok=True)
67
+
68
+ seed_path = Path(cfg.seed_path)
69
+ if not seed_path.exists():
70
+ raise FileNotFoundError(
71
+ f"seed not found at {seed_path}. generate it via the synthetic-data "
72
+ "workflow under handoffs/, or run scripts/01_download.py from a "
73
+ "checkout that has data/seed/ populated."
74
+ )
75
+
76
+ rows = _load_seed(seed_path)
77
+ if smoke:
78
+ rows = rows[:200]
79
+ print(f"[download] loaded {len(rows)} seed pairs from {seed_path}")
80
+
81
+ rng = random.Random(cfg.random_seed)
82
+ train, val, test = _split(rows, cfg.splits, rng)
83
+
84
+ def _write(name: str, batch: list[dict]) -> int:
85
+ path = out_dir / f"{name}.json"
86
+ path.write_text(json.dumps(batch, ensure_ascii=False, indent=None))
87
+ return len(batch)
88
+
89
+ counts = {
90
+ "train": _write("train", train),
91
+ "val": _write("val", val),
92
+ "test": _write("test", test),
93
+ }
94
+
95
+ meta = {
96
+ "seed_path": str(seed_path),
97
+ "random_seed": cfg.random_seed,
98
+ "splits": cfg.splits.__dict__,
99
+ "counts": counts,
100
+ "smoke": smoke,
101
+ }
102
+ (out_dir / "meta.json").write_text(json.dumps(meta, indent=2))
103
+ print(f"[download] wrote {counts} to {out_dir}")
104
+ return meta
105
+
106
+
107
+ def load_pairs(data_dir, split: str, max_rows: Optional[int] = None) -> list[dict]:
108
+ path = Path(data_dir) / f"{split}.json"
109
+ rows = json.loads(path.read_text())
110
+ if max_rows is not None and max_rows < len(rows):
111
+ rows = rows[:max_rows]
112
+ return rows
src/cleanup/data/inject.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 9 disfluency operators. each is a pure function (text, rng, cfg) -> text.
2
+ # compose via apply(text, cfg, rng) which samples N operators per example and
3
+ # chains them deterministically given the rng seed.
4
+ #
5
+ # the clean target is always recoverable from the raw via deletion + casing +
6
+ # punct + homophone normalization. faithfulness is guaranteed by construction
7
+ # because nothing here invents content.
8
+
9
+ import random
10
+ import re
11
+ import string
12
+ from typing import Callable, Dict
13
+
14
+ INJECT_PROBES = ("um", "uh", "er", "ah", "like", "you know", "i mean", "so ", "well ")
15
+
16
+
17
+ def _split_words(text: str) -> list[str]:
18
+ return text.split()
19
+
20
+
21
+ def _join_words(words: list[str]) -> str:
22
+ return " ".join(words)
23
+
24
+
25
+ def add_filler(text: str, rng: random.Random, op_cfg: dict) -> str:
26
+ words = _split_words(text)
27
+ if len(words) < 2:
28
+ return text
29
+ inserts = rng.randint(1, op_cfg.get("max_inserts", 2))
30
+ vocab = op_cfg["vocab"]
31
+ for _ in range(inserts):
32
+ pos = rng.randint(0, len(words))
33
+ words.insert(pos, rng.choice(vocab))
34
+ return _join_words(words)
35
+
36
+
37
+ def word_stutter(text: str, rng: random.Random, op_cfg: dict) -> str:
38
+ words = _split_words(text)
39
+ if len(words) < 2:
40
+ return text
41
+ pos = rng.randint(0, len(words) - 1)
42
+ repeats = rng.randint(1, op_cfg.get("max_repeats", 1))
43
+ repeated = [words[pos]] * repeats + words[pos:]
44
+ return _join_words(words[:pos] + repeated)
45
+
46
+
47
+ def false_start(text: str, rng: random.Random, op_cfg: dict) -> str:
48
+ prefix = rng.choice(op_cfg["prefixes"])
49
+ return f"{prefix} {text}"
50
+
51
+
52
+ def strip_punct(text: str, rng: random.Random, op_cfg: dict) -> str:
53
+ drop_rate = op_cfg.get("drop_rate", 0.6)
54
+ out_chars = []
55
+ for ch in text:
56
+ if ch in string.punctuation and ch != "'" and rng.random() < drop_rate:
57
+ continue
58
+ out_chars.append(ch)
59
+ return "".join(out_chars)
60
+
61
+
62
+ def lowercase(text: str, rng: random.Random, op_cfg: dict) -> str:
63
+ return text.lower()
64
+
65
+
66
+ def merge_sentences(text: str, rng: random.Random, op_cfg: dict) -> str:
67
+ # drop a single sentence-ending punctuation if present and lowercase the
68
+ # following character so the two sentences read run-on.
69
+ matches = list(re.finditer(r"([.!?])\s+(\w)", text))
70
+ if not matches:
71
+ return text
72
+ m = rng.choice(matches)
73
+ return text[: m.start()] + " " + m.group(2).lower() + text[m.end():]
74
+
75
+
76
+ def dropped_apostrophe(text: str, rng: random.Random, op_cfg: dict) -> str:
77
+ return text.replace("'", "")
78
+
79
+
80
+ def mishear_homophone(text: str, rng: random.Random, op_cfg: dict) -> str:
81
+ pairs = op_cfg.get("pairs", [])
82
+ if not pairs:
83
+ return text
84
+ a, b = rng.choice(pairs)
85
+ pattern = re.compile(rf"\b{re.escape(a)}\b", re.IGNORECASE)
86
+ matches = list(pattern.finditer(text))
87
+ if not matches:
88
+ return text
89
+ m = rng.choice(matches)
90
+ return text[: m.start()] + b + text[m.end():]
91
+
92
+
93
+ def repeated_chunk(text: str, rng: random.Random, op_cfg: dict) -> str:
94
+ words = _split_words(text)
95
+ chunk_min = op_cfg.get("chunk_size_min", 2)
96
+ chunk_max = op_cfg.get("chunk_size_max", 4)
97
+ if len(words) < chunk_min + 1:
98
+ return text
99
+ chunk = rng.randint(chunk_min, min(chunk_max, len(words) - 1))
100
+ start = rng.randint(0, len(words) - chunk)
101
+ repeated = words[start : start + chunk]
102
+ return _join_words(words[: start + chunk] + repeated + words[start + chunk :])
103
+
104
+
105
+ # operator registry. key matches the yaml ops.* keys.
106
+ OPS: Dict[str, Callable[[str, random.Random, dict], str]] = {
107
+ "add_filler": add_filler,
108
+ "word_stutter": word_stutter,
109
+ "false_start": false_start,
110
+ "strip_punct": strip_punct,
111
+ "lowercase": lowercase,
112
+ "merge_sentences": merge_sentences,
113
+ "dropped_apostrophe": dropped_apostrophe,
114
+ "mishear_homophone": mishear_homophone,
115
+ "repeated_chunk": repeated_chunk,
116
+ }
117
+
118
+
119
+ def apply(text: str, ops_cfg: dict, sampling_cfg, rng: random.Random) -> str:
120
+ # pick N operators, then for each operator coin-flip its own p. order is
121
+ # randomized to vary the corruption shape across examples.
122
+ n_min = sampling_cfg.ops_per_example_min
123
+ n_max = sampling_cfg.ops_per_example_max
124
+ n = rng.randint(n_min, n_max)
125
+ op_names = list(OPS.keys())
126
+ rng.shuffle(op_names)
127
+ chosen = op_names[:n]
128
+ out = text
129
+ for name in chosen:
130
+ op_cfg = ops_cfg.get(name, {})
131
+ p = op_cfg.get("p", 0.0)
132
+ if rng.random() < p:
133
+ out = OPS[name](out, rng, op_cfg)
134
+ # final whitespace squeeze
135
+ return re.sub(r"\s+", " ", out).strip()
src/cleanup/data/tokenize.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # format each (raw, clean) pair as a chatml conversation and tokenize.
2
+ # returns dicts the hf datasets api can collate. labels are built by trl's
3
+ # DataCollatorForCompletionOnlyLM via the response_template, so here we only
4
+ # produce input_ids + attention_mask + the raw text fields trl needs.
5
+
6
+ from datasets import Dataset
7
+
8
+ from cleanup.prompts import SYSTEM_PROMPT
9
+
10
+ # this string MUST exactly match what apply_chat_template emits before the
11
+ # assistant turn begins, including the trailing newline. DataCollatorForCompletionOnlyLM
12
+ # searches for this template inside input_ids and masks everything before its
13
+ # end position with -100, so cross entropy only counts assistant tokens.
14
+ RESPONSE_TEMPLATE = "<|im_start|>assistant\n"
15
+
16
+
17
+ def format_chat(pair: dict) -> str:
18
+ # build a single string in qwen's chatml format.
19
+ user = pair["raw"]
20
+ assistant = pair["clean"]
21
+ return (
22
+ f"<|im_start|>system\n{SYSTEM_PROMPT}<|im_end|>\n"
23
+ f"<|im_start|>user\n{user}<|im_end|>\n"
24
+ f"<|im_start|>assistant\n{assistant}<|im_end|>"
25
+ )
26
+
27
+
28
+ def to_dataset(pairs: list[dict]) -> Dataset:
29
+ rows = [{"text": format_chat(p)} for p in pairs]
30
+ return Dataset.from_list(rows)
31
+
32
+
33
+ def formatting_func(example: dict) -> list[str]:
34
+ # trl sftrainer expects a callable that takes a row (or batch) and returns
35
+ # a list of strings to tokenize. supports both single example dicts and
36
+ # batched dicts with list values.
37
+ if isinstance(example["text"], list):
38
+ return example["text"]
39
+ return [example["text"]]
src/cleanup/eval/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """evaluation: quality metrics, cpu latency, and report assembly."""
src/cleanup/eval/latency.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cpu latency for the exported onnx, mirroring privacy-filter/eval/latency.py.
2
+ # RUN THIS LOCALLY on the target laptop. gpu timings are not informative
3
+ # because deployment is cpu-only via ort.
4
+ #
5
+ # two benchmarks:
6
+ # - fixed length sweep: synthesize prompts of N tokens, time N=16..512
7
+ # - realistic mix: use real test rows, time variable-length inputs
8
+
9
+ import statistics
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import numpy as np
15
+ import onnxruntime as ort
16
+ from transformers import AutoTokenizer
17
+
18
+
19
+ def _session(onnx_path: Path, intra_op_threads: int) -> ort.InferenceSession:
20
+ opts = ort.SessionOptions()
21
+ opts.intra_op_num_threads = intra_op_threads
22
+ opts.inter_op_num_threads = 1
23
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
24
+ return ort.InferenceSession(str(onnx_path), opts, providers=["CPUExecutionProvider"])
25
+
26
+
27
+ def _percentiles(values: list[float]) -> dict:
28
+ if not values:
29
+ return {"p50_ms": 0.0, "p95_ms": 0.0, "p99_ms": 0.0, "mean_ms": 0.0}
30
+ arr = np.asarray(values)
31
+ return {
32
+ "p50_ms": float(np.percentile(arr, 50)),
33
+ "p95_ms": float(np.percentile(arr, 95)),
34
+ "p99_ms": float(np.percentile(arr, 99)),
35
+ "mean_ms": float(np.mean(arr)),
36
+ }
37
+
38
+
39
+ def _one_call_ms(session: ort.InferenceSession, input_ids: np.ndarray) -> float:
40
+ attn = np.ones_like(input_ids)
41
+ t0 = time.perf_counter()
42
+ session.run(
43
+ None,
44
+ {"input_ids": input_ids, "attention_mask": attn},
45
+ )
46
+ return (time.perf_counter() - t0) * 1000.0
47
+
48
+
49
+ def benchmark_latency(
50
+ onnx_path: Path,
51
+ tokenizer,
52
+ warmup: int = 50,
53
+ measure: int = 500,
54
+ intra_op_threads: int = 4,
55
+ lengths: tuple = (16, 32, 64, 128, 256, 512),
56
+ ) -> dict:
57
+ session = _session(onnx_path, intra_op_threads)
58
+ results = {}
59
+ pad = tokenizer.pad_token_id or 0
60
+ for length in lengths:
61
+ # synthesize a sequence of `length` tokens (alternating pad and a fixed
62
+ # ascii token id so the model has structure to attend to).
63
+ ids = np.full((1, length), pad, dtype=np.int64)
64
+ ids[0, ::2] = tokenizer.encode("the")[0]
65
+ for _ in range(warmup):
66
+ _one_call_ms(session, ids)
67
+ samples = [_one_call_ms(session, ids) for _ in range(measure)]
68
+ results[str(length)] = _percentiles(samples)
69
+ return results
70
+
71
+
72
+ def benchmark_realistic(
73
+ onnx_path: Path,
74
+ tokenizer,
75
+ texts: list[str],
76
+ intra_op_threads: int = 4,
77
+ warmup: int = 20,
78
+ ) -> dict:
79
+ session = _session(onnx_path, intra_op_threads)
80
+ sequences = [
81
+ np.asarray(tokenizer.encode(t, max_length=512, truncation=True), dtype=np.int64).reshape(1, -1)
82
+ for t in texts
83
+ ]
84
+ # warmup uses the first n inputs
85
+ for i in range(min(warmup, len(sequences))):
86
+ _one_call_ms(session, sequences[i])
87
+ samples = [_one_call_ms(session, s) for s in sequences]
88
+ lengths = [s.shape[1] for s in sequences]
89
+ out = _percentiles(samples)
90
+ out["samples"] = len(samples)
91
+ out["token_length_p50"] = int(statistics.median(lengths))
92
+ out["token_length_p95"] = int(np.percentile(lengths, 95))
93
+ return out
src/cleanup/eval/metrics.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # the five quality metrics plus a 3-model comparison driver (raw input vs
2
+ # qwen base zero-shot vs fine-tuned). all metrics are computed on the held-out
3
+ # test split (real disfluencyspeech pairs the model never sees).
4
+ #
5
+ # the canonical reference is data/pairs/test.json which has rows like
6
+ # { "raw": <transcript_a>, "clean": <transcript_c> }
7
+
8
+ import json
9
+ import re
10
+ import string
11
+ from pathlib import Path
12
+ from typing import Callable, Optional
13
+
14
+ import Levenshtein
15
+ import torch
16
+ from tqdm import tqdm
17
+
18
+ # common dictation fillers we track for the removal-rate metric.
19
+ FILLER_TOKENS = {"um", "uh", "er", "ah", "like", "you know", "i mean", "so", "well"}
20
+
21
+ PUNCT_CHARS = set(".,;:!?-")
22
+
23
+
24
+ # ---------- text utilities ----------
25
+
26
+ def _words(text: str) -> list[str]:
27
+ return text.lower().strip().split()
28
+
29
+
30
+ def _content_words(text: str) -> list[str]:
31
+ # drop punctuation, lowercase, split. used by faithfulness metric.
32
+ stripped = "".join(c for c in text if c not in string.punctuation)
33
+ return stripped.lower().split()
34
+
35
+
36
+ def _count_fillers(text: str) -> int:
37
+ # count occurrences of each filler form as whole words.
38
+ lower = " " + text.lower() + " "
39
+ count = 0
40
+ for f in FILLER_TOKENS:
41
+ count += len(re.findall(rf"(?<!\w){re.escape(f)}(?!\w)", lower))
42
+ return count
43
+
44
+
45
+ def _punct_positions(text: str) -> list[tuple[int, str]]:
46
+ # return (content_word_index, punct_char) for every sentence punctuation
47
+ # mark, anchored to the index of the preceding content word.
48
+ content = _content_words(text)
49
+ raw_tokens = text.split()
50
+ positions: list[tuple[int, str]] = []
51
+ content_idx = -1
52
+ for tok in raw_tokens:
53
+ # strip trailing punctuation from the token to find the content word
54
+ body = "".join(c for c in tok if c not in string.punctuation)
55
+ if body:
56
+ content_idx += 1
57
+ tail_punct = "".join(c for c in tok if c in PUNCT_CHARS)
58
+ for p in tail_punct:
59
+ positions.append((content_idx, p))
60
+ return positions
61
+
62
+
63
+ # ---------- per-example metrics ----------
64
+
65
+ def disfluency_removal_rate(raw: str, out: str) -> Optional[float]:
66
+ raw_count = _count_fillers(raw)
67
+ if raw_count == 0:
68
+ return None
69
+ survived = _count_fillers(out)
70
+ removed = max(0, raw_count - survived)
71
+ return removed / raw_count
72
+
73
+
74
+ def punctuation_f1(out: str, ref: str) -> tuple[int, int, int]:
75
+ # returns (true_positives, predicted, gold) for a corpus-level micro-f1
76
+ out_positions = set(_punct_positions(out))
77
+ ref_positions = set(_punct_positions(ref))
78
+ tp = len(out_positions & ref_positions)
79
+ return tp, len(out_positions), len(ref_positions)
80
+
81
+
82
+ def faithfulness(out: str, ref: str) -> float:
83
+ out_words = _content_words(out)
84
+ ref_words = _content_words(ref)
85
+ if not ref_words:
86
+ return 1.0
87
+ # token-level levenshtein on the lowercased content-only word lists.
88
+ dist = Levenshtein.distance(" ".join(out_words), " ".join(ref_words))
89
+ ref_len = len(" ".join(ref_words))
90
+ if ref_len == 0:
91
+ return 1.0
92
+ return max(0.0, 1.0 - dist / ref_len)
93
+
94
+
95
+ def length_ratio(out: str, ref: str) -> float:
96
+ out_words = _content_words(out)
97
+ ref_words = _content_words(ref)
98
+ if not ref_words:
99
+ return 0.0
100
+ return len(out_words) / len(ref_words)
101
+
102
+
103
+ # ---------- aggregation ----------
104
+
105
+ def aggregate(rows: list[dict]) -> dict:
106
+ # given a list of {"raw": ..., "out": ..., "clean": ...} rows, compute
107
+ # corpus-level metrics. returns the dict shape consumed by the report.
108
+ disfl = [
109
+ d for d in (disfluency_removal_rate(r["raw"], r["out"]) for r in rows)
110
+ if d is not None
111
+ ]
112
+ tp_sum = pred_sum = gold_sum = 0
113
+ faithful_vals: list[float] = []
114
+ length_vals: list[float] = []
115
+ pass_count = 0
116
+ pass_thresholds = {
117
+ "disfluency": 0.95,
118
+ "punct_f1": 0.85,
119
+ "faithfulness": 0.98,
120
+ "length_min": 0.85,
121
+ "length_max": 1.05,
122
+ }
123
+ per_example = []
124
+ for r in rows:
125
+ tp, pred, gold = punctuation_f1(r["out"], r["clean"])
126
+ tp_sum += tp
127
+ pred_sum += pred
128
+ gold_sum += gold
129
+ fa = faithfulness(r["out"], r["clean"])
130
+ faithful_vals.append(fa)
131
+ lr = length_ratio(r["out"], r["clean"])
132
+ length_vals.append(lr)
133
+ d = disfluency_removal_rate(r["raw"], r["out"])
134
+ ok = (
135
+ (d is None or d >= pass_thresholds["disfluency"])
136
+ and fa >= pass_thresholds["faithfulness"]
137
+ and pass_thresholds["length_min"] <= lr <= pass_thresholds["length_max"]
138
+ )
139
+ per_example.append(
140
+ {
141
+ "raw": r["raw"],
142
+ "out": r["out"],
143
+ "clean": r["clean"],
144
+ "disfluency_removal": d,
145
+ "faithfulness": fa,
146
+ "length_ratio": lr,
147
+ }
148
+ )
149
+ if ok:
150
+ pass_count += 1
151
+
152
+ precision = tp_sum / pred_sum if pred_sum > 0 else 0.0
153
+ recall = tp_sum / gold_sum if gold_sum > 0 else 0.0
154
+ punct_f1 = (
155
+ 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
156
+ )
157
+
158
+ return {
159
+ "num_rows": len(rows),
160
+ "disfluency_removal_rate": sum(disfl) / len(disfl) if disfl else None,
161
+ "punctuation_precision": precision,
162
+ "punctuation_recall": recall,
163
+ "punctuation_f1": punct_f1,
164
+ "faithfulness_mean": sum(faithful_vals) / len(faithful_vals) if faithful_vals else 0.0,
165
+ "length_ratio_mean": sum(length_vals) / len(length_vals) if length_vals else 0.0,
166
+ "pass_rate": pass_count / len(rows) if rows else 0.0,
167
+ "per_example": per_example[:50], # keep a sample for the report
168
+ }
169
+
170
+
171
+ # ---------- model generators ----------
172
+
173
+ def make_raw_generator() -> Callable[[str], str]:
174
+ # baseline 1: no cleanup at all. lets us measure what shipping nothing
175
+ # looks like on the same test set.
176
+ def gen(raw: str) -> str:
177
+ return raw
178
+ return gen
179
+
180
+
181
+ def make_qwen_generator(model_id_or_path: str, adapter_path: Optional[str] = None) -> Callable[[str], str]:
182
+ # baseline 2 (no adapter) or row 3 (with adapter): qwen 0.5b. greedy
183
+ # decode, max_new_tokens capped so the model cannot balloon into a chat
184
+ # reply.
185
+ from transformers import AutoModelForCausalLM, AutoTokenizer
186
+ from cleanup.prompts import build_messages
187
+
188
+ tokenizer = AutoTokenizer.from_pretrained(model_id_or_path, use_fast=True)
189
+ if tokenizer.pad_token is None:
190
+ tokenizer.pad_token = tokenizer.eos_token
191
+ model = AutoModelForCausalLM.from_pretrained(
192
+ model_id_or_path,
193
+ torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
194
+ device_map="auto" if torch.cuda.is_available() else None,
195
+ )
196
+ if adapter_path:
197
+ from peft import PeftModel
198
+ model = PeftModel.from_pretrained(model, adapter_path)
199
+ model.eval()
200
+
201
+ def gen(raw: str) -> str:
202
+ messages = build_messages(raw)
203
+ prompt = tokenizer.apply_chat_template(
204
+ messages, tokenize=False, add_generation_prompt=True
205
+ )
206
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
207
+ raw_token_count = len(tokenizer.encode(raw))
208
+ max_new = min(256, max(8, int(raw_token_count * 1.6)))
209
+ with torch.no_grad():
210
+ out_ids = model.generate(
211
+ **inputs,
212
+ do_sample=False,
213
+ max_new_tokens=max_new,
214
+ pad_token_id=tokenizer.pad_token_id,
215
+ eos_token_id=tokenizer.eos_token_id,
216
+ )
217
+ new_tokens = out_ids[0][inputs.input_ids.shape[1]:]
218
+ text = tokenizer.decode(new_tokens, skip_special_tokens=True)
219
+ return text.strip()
220
+
221
+ return gen
222
+
223
+
224
+ def evaluate_one(test_rows: list[dict], generator: Callable[[str], str]) -> dict:
225
+ out_rows = []
226
+ for row in tqdm(test_rows, desc="generating"):
227
+ out = generator(row["raw"])
228
+ out_rows.append({"raw": row["raw"], "clean": row["clean"], "out": out})
229
+ return aggregate(out_rows)
230
+
231
+
232
+ def write_eval(report: dict, run_dir: Path) -> None:
233
+ run_dir = Path(run_dir)
234
+ (run_dir / "eval.json").write_text(json.dumps(report, indent=2, ensure_ascii=False))
src/cleanup/eval/report.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # turn eval.json + training_history.json + latency_benchmark.json into
2
+ # charts and a markdown table. mirrors privacy-filter/eval/report.py.
3
+
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import matplotlib
8
+ matplotlib.use("Agg")
9
+ import matplotlib.pyplot as plt
10
+
11
+
12
+ def plot_learning_curves(history: list[dict], out_path: Path) -> None:
13
+ train_loss = [(h["step"], h["loss"]) for h in history if "loss" in h and "eval_loss" not in h]
14
+ eval_loss = [(h["step"], h["eval_loss"]) for h in history if "eval_loss" in h]
15
+ fig, ax = plt.subplots(figsize=(8, 4))
16
+ if train_loss:
17
+ steps, losses = zip(*train_loss)
18
+ ax.plot(steps, losses, label="train", linewidth=1.5)
19
+ if eval_loss:
20
+ steps, losses = zip(*eval_loss)
21
+ ax.plot(steps, losses, label="val", linewidth=2.0)
22
+ ax.set_xlabel("step")
23
+ ax.set_ylabel("loss")
24
+ ax.set_title("learning curves")
25
+ ax.legend()
26
+ ax.grid(True, alpha=0.3)
27
+ fig.tight_layout()
28
+ fig.savefig(out_path, dpi=120)
29
+ plt.close(fig)
30
+
31
+
32
+ def plot_metrics_comparison(reports: dict, out_path: Path) -> None:
33
+ # reports: {"raw": agg, "base": agg, "fine_tuned": agg}
34
+ rows = ["raw", "base", "fine_tuned"]
35
+ metrics = ["disfluency_removal_rate", "punctuation_f1", "faithfulness_mean", "pass_rate"]
36
+ labels = ["disfluency removed", "punct f1", "faithfulness", "pass rate"]
37
+ fig, ax = plt.subplots(figsize=(9, 4.5))
38
+ x = list(range(len(metrics)))
39
+ bar_w = 0.27
40
+ for i, row in enumerate(rows):
41
+ values = []
42
+ for m in metrics:
43
+ v = reports[row].get(m)
44
+ values.append(0.0 if v is None else v)
45
+ ax.bar([xi + i * bar_w for xi in x], values, width=bar_w, label=row)
46
+ ax.set_xticks([xi + bar_w for xi in x])
47
+ ax.set_xticklabels(labels)
48
+ ax.set_ylim(0, 1.05)
49
+ ax.set_title("quality metrics: raw vs base vs fine-tuned")
50
+ ax.legend()
51
+ ax.grid(True, alpha=0.3, axis="y")
52
+ fig.tight_layout()
53
+ fig.savefig(out_path, dpi=120)
54
+ plt.close(fig)
55
+
56
+
57
+ def plot_latency(sweep_fp32: dict, sweep_int8: dict | None, out_path: Path) -> None:
58
+ fig, ax = plt.subplots(figsize=(8, 4))
59
+ for name, sweep in [("fp32", sweep_fp32), ("int8", sweep_int8)]:
60
+ if not sweep:
61
+ continue
62
+ lengths = sorted(int(k) for k in sweep.keys())
63
+ p50 = [sweep[str(l)]["p50_ms"] for l in lengths]
64
+ p95 = [sweep[str(l)]["p95_ms"] for l in lengths]
65
+ ax.plot(lengths, p50, marker="o", label=f"{name} p50")
66
+ ax.plot(lengths, p95, marker="o", linestyle="--", label=f"{name} p95")
67
+ ax.set_xlabel("input length (tokens)")
68
+ ax.set_ylabel("latency (ms)")
69
+ ax.set_title("cpu latency by input length")
70
+ ax.legend()
71
+ ax.grid(True, alpha=0.3)
72
+ fig.tight_layout()
73
+ fig.savefig(out_path, dpi=120)
74
+ plt.close(fig)
75
+
76
+
77
+ def write_report_markdown(
78
+ run_dir: Path,
79
+ eval_path: Path,
80
+ history_path: Path | None,
81
+ latency_path: Path | None,
82
+ out_path: Path,
83
+ ) -> None:
84
+ eval_data = json.loads(eval_path.read_text())
85
+ lines: list[str] = []
86
+ lines.append("# cleanup model report")
87
+ lines.append("")
88
+ lines.append("## quality metrics")
89
+ lines.append("")
90
+ lines.append("| model | disfluency removal | punct f1 | faithfulness | length ratio | pass rate |")
91
+ lines.append("|---|---:|---:|---:|---:|---:|")
92
+ rows = ["raw", "base", "fine_tuned"]
93
+ for row in rows:
94
+ m = eval_data.get(row)
95
+ if not m:
96
+ continue
97
+ d = m.get("disfluency_removal_rate")
98
+ d_str = "n/a" if d is None else f"{d:.3f}"
99
+ lines.append(
100
+ f"| {row} | {d_str} | {m['punctuation_f1']:.3f} | "
101
+ f"{m['faithfulness_mean']:.3f} | {m['length_ratio_mean']:.3f} | "
102
+ f"{m['pass_rate']:.3f} |"
103
+ )
104
+ lines.append("")
105
+ if latency_path and latency_path.exists():
106
+ lat = json.loads(latency_path.read_text())
107
+ lines.append("## cpu latency")
108
+ lines.append("")
109
+ lines.append("| length | p50 ms | p95 ms | p99 ms | mean ms |")
110
+ lines.append("|---:|---:|---:|---:|---:|")
111
+ for length, stats in lat.get("results_by_length", {}).items():
112
+ lines.append(
113
+ f"| {length} | {stats['p50_ms']:.1f} | {stats['p95_ms']:.1f} | "
114
+ f"{stats['p99_ms']:.1f} | {stats['mean_ms']:.1f} |"
115
+ )
116
+ if "realistic_mix" in lat and lat["realistic_mix"]:
117
+ r = lat["realistic_mix"]
118
+ lines.append("")
119
+ lines.append(
120
+ f"**realistic mix** ({r['samples']} real inputs, "
121
+ f"p50 token length {r['token_length_p50']}): "
122
+ f"p50={r['p50_ms']:.1f}ms p95={r['p95_ms']:.1f}ms p99={r['p99_ms']:.1f}ms"
123
+ )
124
+ Path(out_path).write_text("\n".join(lines))