Speaker profiles v1 (Phase 1)¶
Longitudinal speaker records are canonical versioned project files under
speaker_profiles_dir (default data_dir/speaker_profiles/; override with
TRANSCRIPTX_SPEAKER_PROFILES_DIR). Profiles store real display names — treat
the tree as local PII. Prefer a path outside the git clone for real use; the
repo-local default is gitignored. All writes pass through SpeakerProfileService.
Any later SQLite/index is a rebuildable query accelerator only.
This contract freezes Phase 1 identity, storage layout, operation journaling, fingerprints, aggregates, and date precedence. Implementation stages must not weaken fail-closed rules below.
Related storage roots: docs/runtime/STORAGE.md.
Canonical vs derived¶
Kind |
Location |
Durable? |
|---|---|---|
Profiles, live links, events, operation journals |
|
Yes — canonical (PII; do not commit) |
Project operation lock |
|
Lock only |
Listing / aggregate caches / optional file index |
|
No — disposable |
Auto-identify fusion artefacts |
|
No — review dumps only ( |
Managed transcript JSON |
|
Canonical for content/metrics |
Speaker-map sidecars |
|
Current display labels / ignore lists only |
Deleting .cache/ must not lose profiles, links, events, or journals.
Phase 1 starts without SQLite. A derived file index may be added only after documented reference-environment measurement (Stage 8); absolute millisecond thresholds are not CI acceptance gates.
Layout¶
speaker_profiles_dir/ # default: data_dir/speaker_profiles/; env: TRANSCRIPTX_SPEAKER_PROFILES_DIR
profiles/{profile_id}.speaker_profile.json
links/{link_file_key}.speaker_link.json
events/{idempotency_id}.speaker_event.json # filename stem == event idempotency key
operations/{operation_id}.op.json
operations/{operation_id}/staging/ # after-images while active
operations/{operation_id}/backup/ # before-images while active
.cache/ # disposable only
identify/{managed_transcript_id}.identify.v1.json
state_dir/
speaker_profiles.lock # project operation lock only
Symlink policy¶
Reject a symlinked speaker_profiles root. Reject symlink/alias escapes for
directories, files, staging, and operation paths: resolve and require realpath
under the canonical root.
Schema IDs (frozen)¶
Artifact |
|
Filename suffix |
|---|---|---|
Profile |
|
|
Live link |
|
|
Event |
|
|
Operation |
|
|
Wire version / schema_version fields are integers frozen at 1 for Phase 1.
Transcript identity¶
managed_transcript_id = str(uuid.UUID(import_id)) → lowercase hyphenated
8-4-4-4-12 form. Reject non-UUID import ids. Never treat “hex as stored” as
ambiguous alternate forms.
ManagedTranscriptResolver maps managed_transcript_id → exactly one admitted
managed library transcript path. Fail closed when:
duplicate
import_idacross admitted sidecarsmissing / invalid import sidecar
current_json_filenamedoes not match the transcript file beside the mirrored sidecar (stale)resolved path outside
transcripts_dirlibrary / not admittedsymlink escape of library roots (same realpath discipline)
observed_transcript_relpath is an immutable audit snapshot written at link
time only. Never used for resolution. Always resolve the current path via
resolver + import_id.
Profile linking eligibility: managed-library only. Ad-hoc / run-output JSON may use local naming on Speaker Identification, but must not create profile links.
Occurrence keys and fingerprints¶
Natural key: (managed_transcript_id, local_speaker_key) where
local_speaker_key = normalize_diarized_id(raw segment speaker). Never display
name / speaker_db_id.
link_file_key: SHA-256 of UTF-8 canonical JSON
["speaker_occurrence_key.v1", managed_transcript_id, local_speaker_key]
with separators=(",", ":"), ensure_ascii=False, allow_nan=False.
Normalisation collision gate: distinct raw speakers collapsing to one normalised
key → SpeakerKeyCollisionError; block linking; integrity warning.
Fingerprint occurrence_fingerprint.v1¶
Ordered matching raw segments; fields start, end, text, speaker only.
Timestamp canonicalisation (frozen) before hash: for each of start/end,
if value is int or float (finite), convert to float then format with
exactly 6 decimal places via format(x, ".6f") (e.g. 1 and 1.0 →
"1.000000"). Non-finite → segment excluded from fingerprint input and treated
as timing-invalid for metrics. Strings that parse as finite floats are accepted
then canonicalised the same way; unparsable → timing-invalid.
Test vectors required for 1 vs 1.0 vs "1.0".
Fingerprint mismatch on read: effective needs_review; no mutation.
Supersession is a separate journalled op.
Live links, events, operations¶
Live link v1: one confirmed live file per occurrence key. No suggested/rejected until voice phase.
Unlink: journalled op writes
link_unlinkedevent (after-image) + deletes live link (with before-image backup). No tombstone file. Absence = unlinked.Corrupt live link at the occurrence’s hashed path:
repair_required/ block new link and intersecting reads until repair quarantines or restores a valid file. Not “safely unlinked”.Event filename stem =
idempotency_id. Payload may also carryevent_idequal to the same UUID. Lookup by path; no full-tree scan for idempotency.Every mutation API accepts
operation_idempotency_key. Retry of create-profile-and-link, unlink, relink, merge, archive, migration, supersession, profile update → replay original result; do not create duplicates.Portable operation protocol applies to every mutation that touches more than one canonical file or that pairs a domain write with an event.
Phases:
prepared→staged→transaction_committed→finalized→complete|failed|needs_repair.transaction_committedonly when all planned profile, link, deletion, and event outcomes already match after-state.Read gating: only ops in
completeor proven-aborted unblock affected records.partial, ambiguous, orrecovery_failed→needs_repair; intersecting profile/link reads blocked.Optimistic concurrency on profile edits:
expected_content_sha256of the profile file. Mismatch →StaleUpdateError.No new links to archived/merged profiles; merged redirect with cycle detection.
Local naming vs linking separate; combined create+link is one profile-store op then best-effort sidecar name. Naming failure →
PartialSuccess+CacheInvalidationSignalfor committed parts; sidecar not in profile op atomicity.Ignored speakers: reject new links while ignored. Already-linked then ignored: visible and flagged
ignored=true, excluded from headline aggregate totals and share denominators unlessinclude_ignored=True.observed_label: audit only; UI resolves current sidecar label.Language variants: no auto profile-link inheritance.
No new analysis module; do not reuse
speaker_id_to_db_id.
Phase 1.5 additions¶
Profile accent_color¶
Optional accent_color on transcriptx.speaker_profile.v1: uppercase #RRGGBB or null
(auto name-hash at display time). Create without an accent assigns an unused
palette colour (then freeform #RRGGBB if the palette is exhausted). Update
supports clear_accent. GUI may pick any validated hex via colour wheel.
Profile avatar (optional photo)¶
Additive optional fields on the same transcriptx.speaker_profile.v1 (no schema_id bump):
Field |
Rule |
|---|---|
|
null, or exactly |
|
null, or lowercase hex SHA-256 of normalised WebP bytes |
|
null, or exactly |
All three null or all three set — partial sets are contract errors. Pre-avatar files omit the keys; readers default to null (no migrate-on-read).
Bytes live under speaker_profiles_dir/profiles/assets/{profile_id}/avatar.webp
(canonical PII media — include in backups of the profiles tree; never .cache/).
set_avatar / clear_avatar are journalled multi-file ops (asset + profile +
event). Upload admission: ≤2 MiB; JPEG/PNG/WebP; reject animated; EXIF
orientation then strip metadata; alpha composited on white; square ≤512 WebP.
Failed admission/commit is non-destructive. Reads re-verify hash; mismatch /
missing / corrupt → unavailable (UI initials chip) without breaking Speakers.
Merge: target avatar wins when present; otherwise adopt source asset onto
target path and clear source pointer; always clear source avatar fields on the
merged source record and delete displaced source asset. Archive/unarchive keep
assets. There is no hard profile-delete API in Phase 1; orphan assets under
profiles/assets/ are reported by integrity (avatar_orphan) for manual
cleanup, not auto-deleted. Integrity also reports avatar_missing,
avatar_hash_mismatch, avatar_corrupt.
Privacy: face photos are sensitive PII. Prefer
TRANSCRIPTX_SPEAKER_PROFILES_DIR outside the git clone. Manual recovery: run
integrity scan + recover_operation; do not hand-edit pointers. Include
profiles/assets/ in any backup of speaker_profiles_dir.
UI: fixed-size circular chip — photo or accent+initials; absence must not leave empty image holes.
Appearance flag precedence¶
Single winner: repair_required → missing_source → collision →
needs_review → ignored → ok. Higher flags must not be overwritten.
Aggregates and charts¶
Public headline_eligible is shared by aggregates and time-series builders.
Time-series emit separate headline and all series (no mixed-eligibility
point flag). AggregationSnapshot is the Speakers listing/aggregation entry
(one-pass links + memoized TranscriptBundle per managed transcript). Corrupt
canonical/operation files and blocking ops mark the snapshot incomplete —
partial totals must not be presented as complete.
Link APIs¶
link_existing_profile: unlinked occurrence → existing active profile.relink: requires a live link; same-owner is a service-level no-op; cross-owner requires expected link/owner preconditions.unlink/ fingerprint supersede bind expected link id/hash/fingerprint; already-current fingerprint is a no-op; reject collision/ignored on supersede.
Integrity¶
run_integrity_scan returns typed blocking details (recovery_class, affected
paths, entity intersections) and corrupt profile/link/event/operation paths.
Mutations assert intersecting entities are readable under the project lock.
Operation receipts must persist full cache-invalidation metadata for replay;
recover_operation invalidates affected profile/link caches.
Completed-operation retention¶
After complete: delete staging/ and backup/ bytes; retain compact operation
receipt in the .op.json. Never cleanup ops that are active or needs_repair.
Aggregate definitions (raw local speaker key)¶
New calculator; reuse valid_segment_duration only; not compute_speaker_stats.
Metric |
Rule |
|---|---|
Words |
|
Turns |
Matching segment count; no coalescing |
Durations |
|
speaking_share |
Duration-only when denominator > 0; else |
turn_share |
Separate field |
Headline profile aggregates sum only appearances that are not needs_review,
not missing_source, not collision-affected, and not currently ignored.
Excluded rows listed separately with pending_review_count,
missing_source_count, ignored_linked_count.
Appearance date precedence (frozen — verified against codebase)¶
Transcript document
source.imported_atif parseable ISO datetime → dateElse import sidecar
imported_atif parseableElse
null(sort nulls last; UI “Unknown date”)
Do not reference nonexistent recording_date / session_date.
Do not use filesystem mtime. Future session-date fields require a contract bump.
Verification note (2026-07): transcript_schema.py exposes source.imported_at
only among date-like source fields; import sidecars expose imported_at. No
metadata.recording_date / session_date fields exist today.
Cache invalidation¶
Service returns CacheInvalidationSignal (frozen dataclass: scopes such as
speaker_profiles, speaker_links, transcript_summaries, optional ids).
Web layer maps signal → clear @st.cache_data helpers. Core must not import
Streamlit.
Lock ordering¶
Acquire
state_dir/speaker_profiles.lock(project op).Per-file IO only via
locked_path/write_json_atomic_locked(process-local → FileLock).Never manually nest incompatible FileLocks around atomic writers.
Not in Phase 1¶
Suggested/rejected link states (voice phase)
New analysis module ID
Required SQLite
Migrate-on-read
Rename-transaction coupling for link keys
GUI ranking of name/alias/voice candidates at Speaker Identification is
display-only. Writes remain confirmed live links (create_profile_and_link,
link_existing_profile, relink). Voice scores never auto-confirm.
Phase 1.6 profile analytics pack¶
Descriptive Speakers-detail trends and co-appearance partners built from
AggregationSnapshot only. Not a new analysis module. Not Charts Gallery.
Chart payloads are derived and disposable; never canonical.
Eligibility¶
Public series_eligible(row, *, include_ignored) (alias of headline_eligible)
gates headline series. Uncertain flags always exclude: needs_review,
missing_source, collision, repair_required. Ignored rows gated solely by
include_ignored. All-appearances series includes every row for the profile.
Partial availability¶
Period and partner values use constituents with valid evidence and report
availability=partial plus an evidence note when siblings lack timing.
unavailable / null only when zero valid evidence remains after dedupe.
Timing-valid turns¶
A turn is timing-valid when valid_segment_duration yields finite d >= 0
(including 0.0 for end==start). timing_valid_turn_count counts those turns.
Zero-duration turns enter avg/median; WPM requires duration_seconds > 0.
Non-finite or negative timing never reaches UI (unavailable + integrity warning).
Dedupe¶
Within a period: drop duplicate link_id; collapse (managed_transcript_id, local_speaker_key); sum distinct keys per transcript for additive numerators;
count densoms and partner sessions once per managed_transcript_id.
Speaking share¶
One helper compute_period_speaking_share for date/month/quarter: sum finite
subject durations ÷ sum unique finite densoms > 0. Never average daily shares.
Grains and labels¶
appearance_date | month (YYYY-MM) | quarter (YYYY-Qn); unknown →
unknown / Unknown date, sorted last. Use AppearanceRow.appearance_date
only; do not reparse imported_at in longitudinal builders.
Ordering (frozen): points ascending (sort_key, period_id) with unknown
last; provenance tuples (source_appearance_ids, managed_transcript_ids,
partner shared transcript ids) lexicographically sorted and unique; partners
ranked by shared-transcript count desc, subject minutes desc (nulls last),
display name asc, profile_id asc; integrity warning codes sorted unique.
Dual series¶
Pack always returns typed headline TrendBundle. all_appearances is present
iff requested; independently typed provenance and coverage counters.
Partners¶
Co-appearance / shared sessions only. Rank by shared transcript count, then
subject speaking minutes on those transcripts, then name/profile_id.
Partial minutes: sum valid subject minutes + evidence note; null only if no
valid duration. Exclude dangling, merged-owner, unknown-status, and duplicate
live links from rankings; emit pack integrity_warnings; never crash.
Freshness¶
Shared build_profile_freshness_token inputs: profile identity/status/
updated_at; per appearance link id, transcript id, local key, fingerprint,
flag, ignored, appearance date, metrics digest; referenced densoms. Pack and
aggregates must use the same builder.
Pack contracts¶
Known profile, no appearances → empty success pack.
Unknown profile → typed not-found error.
Merged → existing Speakers redirect; pack refuses if called.
Cache¶
Disk analytics cache deferred until profiling proves need. If added: versioned
atomic disposable files under .cache/; freshness-key miss is sufficient for
correctness. Speakers detail rebuilds snapshot each render.
CacheInvalidationSignal scopes today: speaker_profiles, speaker_links,
transcript_summaries — do not claim coverage the service does not emit.
Locations pack (Speakers detail)¶
Derived Speakers-detail map of geocoded NER location mentions across linked appearances. Not a new analysis module. Not Charts Gallery. Pack payloads are derived and disposable; never canonical.
Inputs¶
AggregationSnapshotappearances (same eligibility as Phase 1.6 headline series viaseries_eligible/include_ignored)Newest run under each appearance session that contains NER locations JSON (
find_ner_locations_path: canonicalner/data/global/*_ner-locations.json, legacyner/ner-locations.json, or nested*ner-locations.json)Speaker-map resolution so mentions are attributed to the profile’s local speaker key on that transcript
Output¶
ProfileLocationsPack: profile_id, shared freshness_token,
include_ignored, ordered mentions (ProfileLocationMention: name, lat/lon,
sentence, session/run ids, segment index, start time, transcript ids/labels,
appearance date), plus appearances_without_ner and unresolved_mentions
counters. status is ok or empty.
Contracts¶
Known profile, no eligible mentions → empty success pack (
status=empty)Unknown profile → typed not-found error
Merged profile → pack refuses (
ProfileAnalyticsMergedError)Missing NER artifacts for an appearance increment
appearances_without_ner; do not fail the pack
UI¶
Speakers detail renders an optional Folium map when mentions exist; empty / partial states surface the without-NER / unresolved counters.
Interactions / equity pack (Speakers detail)¶
Derived Speakers-detail rollup of interactions + equity fields across linked appearances. Not a new analysis module. Not Charts Gallery. Pack payloads are derived and disposable; never canonical. Complements Phase 1.6 partners (co-appearance only) with interruption / response / floor evidence.
Inputs¶
AggregationSnapshotappearances (same eligibility as Phase 1.6 headline series viaseries_eligible/include_ignored)Newest run under each appearance session that contains
interactions/data/global/*_speaker_summary.json(legacyinteractions/speaker_summary.jsonor nested*_speaker_summary.json)Speaker-map / display-name / alias match keys (same discipline as locations)
Output¶
ProfileInteractionsPack: per-appearance interruption/response counts,
dominance, optional floor_share / interruption_asymmetry /
response_latency_mean, headline totals and means, plus
appearances_without_interactions. status is ok or empty.
Contracts¶
Known profile, no usable summaries → empty success pack (
status=empty)Unknown / merged profile → same typed errors as locations pack
Missing interactions artifacts increment
appearances_without_interactions; do not fail the pack
Sentiment pack (Speakers detail)¶
Derived Speakers-detail rollup of sentiment means across linked appearances. Not a new analysis module. Not Charts Gallery. Pack payloads are derived and disposable; never canonical.
Inputs¶
Same appearance eligibility as Phase 1.6 / locations
Newest run with segment rows (
sentiment/data/global/*_sentiment.json, excluding*_with_sentiment.json) preferred; otherwise*_sentiment_summary.json(speaker_results) for compound (pos/neu/neg treated unavailable when summary stubs them as zero)Speaker match keys as locations
Output¶
ProfileSentimentPack: per-appearance compound / pos / neu / neg
means, polarity bucket counts when rows are available, weighted headline
means and polarity shares, plus appearances_without_sentiment.
Contracts¶
Known profile, no usable artifacts → empty success pack (
status=empty)Unknown / merged profile → same typed errors as locations pack
Missing sentiment artifacts increment
appearances_without_sentiment; do not fail the pack