feat(backend): infer transcript speaker labels from meeting notes
When a rich meeting note names a participant but the transcript still shows “Speaker 2”, the same notes response can now supply a high-confidence speaker mapping. SUMMARY_SPEAKER_LABELS_ENABLED defaults off. The existing notes call returns bindings for participants and the owner; an optional transaction after note persistence applies supported mappings and creates strongly grounded people. No extra identity LLM call or client UI is added.
Body prose about the owner remains in third person, naming the owner once per sentence (“David would welcome a meeting”), avoiding “David said David…” and “David said they…”. The owner's name stays forbidden in the title; non-owner people retain name-or-“they” wording and the gender-inference prohibition.
Authority and failure behavior
- Candidate admission uses
named_speaker_prompts_allowed: owner identification is free; other humans require paid eligibility. Manual endpoints retain their permissions. - The transaction reloads current manual decisions, source text, lifecycle state, and existing labels. Manual labels/rejections/unassignments and all existing identities win. Duplicate claims on the same scoped key, conflicting full introductions, ambiguous people, AI agents, and invalid evidence decline. Every explicit lead-in is scanned: a prior matching/bare-copula hit cannot hide a later contradictory introduction. “My name is David Nguyen” cannot prove “David”. Apostrophe prefixes are extended before length validation, internal name particles remain inside the span, and greetings/discourse words or ambiguous continuations decline rather than guessing a boundary. A particle after two non-particle name tokens declines the whole span (for example, “Eddie Thai of Google” or “Joan of Arc of France”), so it authorizes neither creation nor a label and remains contrary evidence. Complex real names with a later particle may be conservatively declined.
- Evidence must reference the bound key and one shared
speaker_id_scope; only segments of that key in the cited scope are labeled. Other scopes reusing its number remain untouched. Independently evidenced scoped voices can reuse a number. - Context can attach an existing exact-name person without the name appearing in the cited text. Context alone never authorizes creation: a new person needs a verified full-name introduction, or a full real name (two or more tokens, or a complete unspaced CJK name of 2–6 characters) from an actual calendar/call participant roster plus high-confidence binding evidence. Background mentions and first-name-only rosters do not authorize creation. Roster links remain supported; alias-only matches require the whole proposed name passing
full_real_namewith a stricter CJK minimum of three characters; a two-character CJK alias cannot resolve a longer stored name, while exact own-name matches remain eligible, including two-character stored names. Straight and curly apostrophes normalize identically. Catalog reads are bounded at 501 per transaction attempt; over 500 people declines named admission. - Contextual owner identification remains available. Existing
is_useror main'sspeaker_identity_status=userelsewhere reserves the owner; main'snot_usersignal on the bound scope vetoes owner admission.no_matchis a failed acoustic match, not a confident non-owner signal. No new owner signal is invented. - People and labels commit atomically, with
summary_inferredprovenance and evidence. Errors preserve the already saved note. Inferred identities never create teaching jobs or modify voiceprints; a model's “high” is not calibrated training authority. The existing manual teaching paths remain the way to authorize learning. - The extraction subclass/private evidence preserves the disabled schema and participant wire. Existing
person_id/is_userconsumers show the names. No deployment, flag flip, historical relabeling, or production-data read is included.
Alternatives rejected: a second proposer/verifier call (extra context and latency), regex-only naming (misses meeting context), forced roster-to-voice bijections (mixed/silent attendees), teaching from inference (reusable voiceprint poisoning), and a new client surface. Open speaker PRs were checked, particularly #10954's separate resolver/verifier/teaching proposal and #20722's voice-match scoring lane; this builds on main and adds no dependency on either.
Cost measurement
The offline experiment runs the real prefix and notes builders/parser with synthetic responses and installed o200k_base cache data read-only. No live model or customer transcript is used.
| Synthetic transcript target | Humans incl. owner | Added identity input | Added fixture output | Owner-wording input |
|---|---|---|---|---|
| 80 words | 2 | 380 | 78 | 103 |
| 1,800 words | 2 | 380 | 78 | 103 |
| 16,000 words | 5 | 380 | 183 | 103 |
This is several dozen output tokens per human (about 35 per additional participant), plus 380 constant input tokens. Revised wording adds 103 independently, including when identity is off (483 total input tokens when enabled). The feature adds no extra model call; the pre-existing bounded presentation-repair call is unchanged.
Current notes routing names OpenAI gpt-6-luna. Price source: backend/llm_gateway/config/cost_rate_cards.yaml, openai.gpt-6-luna.2026-09-22: $0.10 input / $0.60 output per million uncached short-context tokens. The repository explicitly labels these provisional operator-directed rates, pending official published prices. Official list-price verification remains unverified because this lane is offline.
At those rates, identity alone is $0.0848–$0.1478/1,000 conversations; both changes are $0.0951–$0.1581/1,000. Monthly formula: N × (483 × input_price + completion_delta × output_price) / 1,000,000 when all calls are enabled rich notes; separately account for the 103-token wording delta on disabled rich calls. The existing KB free-tier economics record (2026-09-21) reports 19,918 macOS events/30d as a volume proxy, not verified global eligible calls. Assuming all 19,918 were eligible gives $1.89–$3.15/month; an explicit 100,000-call assumption gives $9.51–$15.81/month. Actual eligible volume and participant mix were not read from production.
These are authored fixture completion counts and a tokenizer proxy, not provider-billed usage. Cache/formatting/reasoning effects, Firestore reads/retries, and precision remain unmeasured. The card's provisional >272K input tier is $0.20/$0.90 and can reprice the whole request. Full inputs, formulas, reproduction, and dev qualification are in backend/docs/summary-speaker-labels.md.
Validation and rollout limits
The final suite includes 97 feature tests, 260 related notes/manual/task-capture tests, additional shared-detector/person-creation checks, and 3 offline cost probes. Nineteen regression cases failed before the admission/prompt correction; two additional compound-name cases failed before their boundary fix. Round 2 reproduced fifteen failing cases before correction, covering hidden later introductions, one-letter ASCII/curly apostrophe prefixes, name particles, padded/ambiguous spans, CJK roster creation, and CJK alias resolution without duplicates. A real-name decline control already passed before correction. Round 3 reproduced twelve failures before correction, covering padded affiliation/location spans, greetings and From/At variants, apostrophe normalization, and short CJK alias resolution. Tests prove padded spans leave both people and labels untouched, return None, and preserve exact two-character stored-name matching. Latin spans now require capitals except internal particles; uncertain lowercase spans decline. The new free-plan owner-plus-guest test passed both before and after: it traps catalog reads and creates, proving the previously untested entitlement claim. Tests also cover full-introduction boundaries, creation authority, contextual exact-person attachment, scoped evidence, short aliases, main's owner signals, compressed transcript codecs, current-document/read-before-write semantics, manual decisions, stale/lifecycle fences, error isolation, note-copy/JSON boundaries, and absence of training authority. Final pass counts and type/preflight/push results are recorded in the lane report.
StrictFirestore does not model transaction contention or commit retries; those behaviors were not exercised and are not claimed as covered. No real conversation, live endpoint, or released-client rendering was exercised. Before any production enablement, use a separately authorized dev runtime_env overlay, independently labeled consented recordings, and measure owner/person precision with confidence intervals, coverage, false creation, manual corrections, transaction failures, latency, and actual billed token deltas. Agree thresholds before looking at results and verify official price. No voiceprint teaching is enabled by this change.
Product invariants
- INV-MEM-4: canonical memory promotion/apply authority is unchanged; this stage only writes transcript labels and people after durable note persistence.
- INV-TASK-2: existing task capture admission and Suggested authority remain unchanged and are covered by the related suite.
Failure-Class: none
The reviewed registry classes do not describe speaker-name proof, creation authority, or scope-qualified transcript labels. This lane forbids .github/ changes, so no new definition is added; the regression cases provide the concrete prevention surface.
Line-Count-Exception: backend/utils/llm/conversation_processing.py | 1961 -> 1976 | Select the flagged extraction schema and retain private roster evidence at the existing notes-call seam; hoist the speaker-rules import to module scope. A broad orchestration refactor is outside this lane.
Line-Count-Exception: backend/utils/conversations/process_conversation.py | 3571 -> 3628 | Invoke the optional stage only after authoritative completed persistence; snapshot the pre-stage label projection for canonical memory extraction, stamp the note-generation digest, and refresh people after inferred creation. A broad orchestration refactor is outside this lane.
CI
The reported backend and generated-client failures also occur on the current main base; this PR does not change the affected tests, API specs, or generated clients. Main run 37340248576 already fails the desktop TTS and TypeScript generator test files. The Desktop Backend Contracts workflow has no main run; its failing Swift check reports 479 generated methods versus 478 checked in. See .agent-briefs/laneB-ci-report.md for per-check evidence.
🤖 Generated with Claude Code