Skip to main content

Privacy controls (voiceprints and inferred gender)

OpenTranscribe can recognize the same person across recordings by their voice, and it can infer a speaker's gender from their voice. Both work on biometric-style data, so each has a switch. This page explains the layers of that policy, what turning a switch off does, and the separate, irreversible action that deletes the data a switch only hides.

This page also seeds the data inventory for issue #1132, the voiceprint privacy lifecycle (consent records, per-profile expiry, export), which moved to v0.9.0.

What the two switches control​

SwitchWhen on (the default)When off
Voiceprint matching (cross-file speaker recognition)Each speaker in a recording gets a voiceprint. New speakers are matched against saved voiceprints, name suggestions come from those matches, and speakers are grouped into clusters across recordings.No voiceprint is created, stored, matched, suggested from or clustered. Speakers are still told apart within a recording (SPEAKER_00, SPEAKER_01), because that uses temporary data that is never stored.
Inferred genderA model infers male or female for each speaker from the voice (and the speaker-name step may compare that with a first name).Nothing is inferred, shown or used. A gender a person confirmed by hand is theirs and is kept.

Gender inference is on by default. Wherever gender is shown or configured, the interface says that it is inferred automatically from voice and can be turned off, and links to the switch.

The four layers​

The two questions (may voiceprints exist, may gender be inferred) are answered for one person in one organization (or their personal workspace) by combining four layers. The policy is restrict-only: every layer can switch a feature off, and no layer can switch on something a layer above it turned off. Any layer that is off turns the feature off.

LayerWho sets itWhereDefault
PlatformThe operator of a managed edition, through an extensionNot a screen. A derived deployment registers a resolver at an extension seamNo opinion
InstanceA super adminSettings, Privacy & Redaction, Redaction Policy tab, card "Speaker recognition and inferred attributes"Both on
OrganizationAn organization administratorSettings, organization group, Privacy policy (managed editions only; a community install has no organizations)Both on; an organization with no saved choice inherits
UserEach personVoiceprint switch: Settings, Speaker Identification, "Recognize speakers across my recordings". Gender switch: the same page, Voice Attributes, "Predict gender from voice"Both on

The first layer that is off is reported as locked_by (platform, instance, organization, user, consent or file), so the interface can say who to ask. A user whose organization turned a feature off sees their switch disabled with the reason, and the API refuses to change it (409 policy_locked).

Fail closed. Every error path reads as "off". A platform resolver that raises, a policy read that fails, a recording that cannot be found, or a consent hook that raises or returns anything but True denies the feature rather than allowing it.

Consent. Core ships no consent record. An extension can register a consent hook that must approve creating a voiceprint, and approve letting someone other than the owner match against a saved profile. With no hook registered, consent allows. A denial by a hook is reported as locked_by: "consent" for voiceprints. Consent records and their user interface are part of #1132.

Per-recording voice identification (a fifth, narrower control)​

Each recording carries its own voice identification state, set when it is uploaded and changeable by its owner later. It is restrict-only like the layers above: it can turn voiceprints off for one recording and can never turn on what the platform, the deployment, the organization or the person has turned off. It is checked after those layers and before any consent hook, and is reported as locked_by: "file".

StateMeaning
unspecifiedNothing was stated. This is every existing recording, and changes nothing
grantedThe uploader stated that the speakers agreed. It is a recorded claim: OpenTranscribe stores the statement, when it was made and who made it, and does not verify it. It enables nothing by itself
declinedNo voiceprints are created for this recording

At upload. The wizard's Speakers step has an unchecked box, "I state that the speakers in this recording agreed to voice identification". Checking it records granted; leaving it unchecked records nothing. It is not shown while voiceprints are off for you. A batch upload applies the one choice to every file. Uploads that skip the presigned flow (URL imports, watch sources, the in-browser recorder) record unspecified.

Opting a recording out later. The owner (nobody else, not even an organization administrator) can turn voice identification off for a recording from its speaker panel. That first records declined, which stops new voiceprints at once (the OpenSearch write guard re-reads the state on every write), and then queues the file-scoped voiceprint purge with the reason opt_out. The purge deletes the recording's voiceprint documents, recomputes each profile and cluster average from the voices that remain, and is measured like every other purge. Names and links a person set themselves are kept. Opting back in records granted but restores nothing: reprocess the speakers to create new voiceprints.

Averages. A profile or cluster centroid never includes a voice from a declined recording, even while that recording's document is still readable (an opt-out whose purge has not run, or failed).

Requiring a statement. The system setting privacy.voiceprint_upload_consent_required (Settings, Privacy & Redaction, Redaction Policy tab, "Require a statement before voiceprints are created") is off by default. When on, a recording is treated as declined unless it is granted. Every existing recording is unspecified, so turning it on hides the voiceprints of all existing recordings. It deletes nothing, and there is deliberately no automatic purge behind a settings toggle.

Rolling back. The migration refuses to downgrade while any recording is declined, because an older build ignores the column and would create voiceprints for opted-out recordings. List them with the partial index ix_media_file_voiceprint_declined, or set OT_ALLOW_CONSENT_DOWNGRADE=1 knowingly.

What "off" does​

Turning a switch off hides data and stops new data. It never deletes anything. Switching the feature back on makes existing data visible again, and new recordings get voiceprints again. Deleting is the separate purge.

Each decision is taken for the owner and organization of the record in question (the recording or profile), not for whoever is looking at it or whoever started a job.

WhereWhat happens while voiceprints are offWhat happens while gender is off
Pipeline plan (at upload and reprocess)The speaker-recognition step is not scheduled. The pipeline view shows an informational notice: speaker recognition across recordings is off for this recording, speakers are still told apart within it. The diarizer is told not to return voiceprint vectors, so they never leave the worker.The diarization sidecar is not asked for gender.
Entry gates (every stage, fresh read, no cache)Each stage that produces or uses a voiceprint checks again when it starts, so a recording already in flight when a switch flipped is stopped too. A stopped stage reports "skipped by policy", not an error.The same check in the gender attribute task, the sidecar gender write and the speaker-name step.
Write backstop (every OpenSearch write)Every write of a speaker, profile or cluster document is checked again. A refused write is logged once per scope and returns without writing. The answer is cached for 10 seconds per process, so a switch change reaches every process within 10 seconds. A failed check denies and is not cached.Not applicable (gender is stored in the database, not in a voiceprint document).
Reads and serializationVoiceprint-based name suggestions are not made. A stored suggestion that a voiceprint match wrote is left out of responses, with its confidence and detail, whoever verified the speaker (the name a person gave stays). Cluster lists are empty for that scope. A name that only a voiceprint match supplied is also left out of every speaker label (see Where a voice-matched name is withheld). A transcript-derived AI suggestion is still offered.Inferred gender is returned as null wherever a response embeds a speaker (see Where inferred gender is withheld). A profile's gender is voted on by confirmed genders only.
Actions that would create or recompute voiceprintsThe cluster actions re-cluster, analyze outliers, promote, merge and split answer 409 voiceprints_disabled with locked_by. The admin profile-embedding repair answers the same when every profile is off, and otherwise skips the profiles that are off.Not applicable.
Batch jobsThe admin v4 embedding migration, the consistency repair, the speaker merge task's voiceprint averaging, cluster tasks, profile rebuilds, retroactive matching and batch_speaker_matching skip scopes that are off and report a skipped_by_policy count.The admin gender migration and the combined speaker analysis skip files whose owner or organization forbids it, and a forced reset leaves those files' values alone.
ClustersA speaker is not placed in a cluster, and the cluster reads return nothing for the scope.Gender no longer influences cluster joins or evictions unless a person confirmed it.
Suggestions and the speaker-name LLMProfile and voice suggestions are not computed.The voice-versus-name gender comparison is not added to the speaker-name prompt.
SharingAnother user's profile that you can reach through a shared collection is dropped from your matching candidates when its owner's scope has voiceprints off, or when a consent hook denies sharing.Not applicable.

Plan previews follow the same rule as real recordings, with one difference in whose organization counts. A request that names no recording, such as GET /user-settings/pipeline or the preview that runs before you switch transcription providers, is judged in the organization the request is working in, so a settings page never shows a speaker-recognition step as enabled while that organization forbids it. A real recording is judged in the organization stored on its own row, whichever organization the person who started the job has active.

Two behaviors are independent of the switches. First, the gender comparison is never added to the speaker-name prompt for a remote LLM provider, even when gender inference is on; a local model (a loopback, private-network or compose-hostname endpoint) still receives it. Second, the DIAR_NATIVE_GENDER environment switch of the diarization sidecar remains an additional hard off.

What stays visible. Named speaker profiles, avatars, speaker assignments that a person verified and genders that a person confirmed are user assertions, not inferred data. Switching a layer off does not hide them, and a purge does not delete them.

An admin migration run while a scope is off. The v4 embedding migration skips files whose scope has voiceprints off and reports skipped_by_policy when it finishes. After the new index goes live it holds no voiceprints for those scopes. Their old documents stay in the preserved backup index and are not restored. Rebuild them later, after switching the layer back on, with the existing re-run speaker identification reprocess stage.

Where a voice-matched name is withheld​

A speaker's displayed name is a name a person typed or verified, or else a name a voiceprint match suggested with high confidence. While voiceprints are off for a recording's owner and organization, the second source is ignored everywhere a name is printed, and the speaker shows under the diarizer's label (SPEAKER_00) until a person names it. Every place goes through one function, canonical_speaker_label_for_row in backend/app/utils/speaker_labels.py, which asks the policy only when the voiceprint suggestion is what would decide the name:

  • the file detail and the paginated segments, including resolved_display_name, status_text (the "High confidence match" line) and resolved_speaker_name; the status is now computed after the redaction, not before,
  • the speaker list and detail,
  • every transcript export (txt, json, csv, md, srt, vtt), the Word and PDF documents, the player captions and the subtitle downloads,
  • the search index (chunks, and the digest and facts built from the same label, whenever they are rebuilt) and the speaker roster that chat resolves "who said ..." against.

A name a person typed or verified is not a voiceprint's and stays; a voice suggestion recorded on a speaker a person verified is not part of that name and is withheld too. Switching a voiceprint layer on or off, for a person, an organization or the deployment, queues the existing debounced re-index for the files whose name only a voiceprint supplied (and regenerates their facts), so the search index follows the switch in both directions without waiting for another edit to the file. Only a switch that actually changed value does this.

A voiceprint can name a speaker on its own: an automatic match at upload, a retroactive match after you name another speaker, or a cluster join. Such a row looks exactly like a name a person confirmed, so every speaker records who wrote its name and profile link (identity_source):

Recorded asMeaningWhile voiceprints are offA purge
voiceprintA voiceprint wrote the name and the profile linkThe name, its "verified" mark and the link are hidden; the speaker shows its diarizer labelReverted to unnamed, unlinked and unverified
voiceprint_linkA voiceprint added only the link, to a name that was already thereThe link, and the voice suggestion and match confidence that came with it, are hidden; the name staysThe link is removed together with that suggestion and confidence; the name and its verified mark stay
humanA person typed, accepted, assigned or confirmed it, or promoted a cluster to a profileThe name, mark and link are never hidden; a voice suggestion left on the speaker isThe identity is not touched; a voice suggestion left on the speaker is cleared
noneWritten before 0.6.0, or never namedThe same as human (treated as a person's)The same as human

What a person does overrides what a machine did. Typing a name, accepting a suggestion, assigning or creating a profile, verifying, batch-verifying and promoting a cluster make the identity human. Skipping a voice suggestion in the Inbox only marks it reviewed: the suggestion stays a voiceprint's, so it is hidden while voiceprints are off and a purge clears it (the speaker returns to the Inbox), unlike accepting it. Renaming a profile renames only the speakers whose link to it is visible, so a link a voiceprint made does not receive the new name while it is hidden. Clearing a name keeps a machine link as a link (voiceprint_link). Unassigning, rejecting or deleting a profile removes the link and leaves whatever name remains as unknown. Merging two speakers keeps the survivor's own name, link and origin: a merge does not confirm a machine's name.

Hidden means hidden in what is shown, not deleted: the speaker list and the speaker name directory (the gallery and chat speaker pickers; a hidden name is listed under the diarizer label only when unlabeled speakers are requested), a profile's occurrences (a recording whose only link is a machine's is not an occurrence while voiceprints are off), the "other recordings" list of a speaker and its administrator debug twins (each recording is judged by its own recording's switch, and the match confidence is withheld), the speaker preview, the speaker-merge notification, topic-suggestion prompts, exports, captions, the search index and every other place a name is printed. Turning voiceprints back on shows them again.

What else is recorded about an identity and a suggestion​

The database keeps the full provenance of what is on a speaker, not only whether a person or a voiceprint wrote it. Each speaker records, for its current identity:

RecordedMeaning
identity_source_atWhen the current source was recorded
identity_source_user_idThe person who made or confirmed it. Empty when a task wrote it (a voiceprint match, a cluster join) or when the account has since been deleted
identity_source_detailWhere it came from. A person: how (typed, accepted_suggestion, profile_assigned, profile_created, batch_assign, cluster_promoted, cluster_merged, created, confirmed) and, when they accepted a pending suggestion, which one (its source and confidence, and for an AI guess the provider and model). A voiceprint: voice_match, profile_match, profile_propagation or cluster_join with the confidence, the threshold it had to clear, the matched profile or speaker (ids), the embedding model and the task

and, for the recommendation in suggested_name / suggestion_source, suggestion_detail: a voice or profile match (the same match record) or an AI guess (provider, model, confidence, the task). An AI guess is only ever stored as a suggestion; it is never applied by itself.

These hold ids and identifiers only: no name and no transcript text, and they are never written to the search index or sent to a language model. The speaker API returns them read-only as identity (source, at, by, detail) and suggestion (source, confidence, detail). by names the person only to the file's owner, to an administrator of the file's organization and to a platform administrator whose reach covers the file: in a single-tenant deployment any administrator, in a multi-tenant deployment only under a support-access grant for the file's tenant (the use is recorded like any other platform access). A platform administrator who reaches a file through an ordinary share gets source and at only, like anyone else who can see the file. When the person's account is being erased (the account row can outlive the request while a legal hold lasts), by is {"erased": true} with no id, name or email.

They follow the identity they describe. While a file's voiceprints are off, the record of a voiceprint or voiceprint_link identity (it names the profile or speaker the voice matched) and the detail of a voice suggestion are hidden with the name and link: identity is empty and the suggestion is gone, also when the suggestion sits on a speaker a person verified (new voice suggestions are not recorded on such a speaker; older ones are hidden and purged all the same). A person's own identity record is never hidden. A purge clears them in the same statement that reverts the identity or removes the voice suggestion, counts them (identity_provenance_cleared, suggestion_details_cleared) and checks that none are left (speaker_identity_provenance, voiceprint_suggestion_details); a person's identity record, and the detail of an AI suggestion, stay. The record of an identity a person made keeps no voice comparison either: when they accept a voice suggestion it notes only that the suggestion was a voice match, not its similarity score. Reverting an identity to unknown (unlinking, rejecting, deleting a profile) forgets the record with it.

This is the current state, not a history. Only the latest source, time, person and origin are kept; what was there before is overwritten. A complete, append-only history of who changed which name and when is tracked in issue #1336 (v0.7.0), and a review of the queries these columns need at scale in #1337. Identities that existed before 0.6.0 have no time or person; the identities the upgrade recognised as voiceprint-made carry {"origin": "v449_backfill"}.

Not covered. Summaries, topic suggestions and other text a language model wrote earlier can contain a name that a voiceprint supplied. That text is model output, not a label, so switching voiceprints off does not rewrite it; regenerate the summary while voiceprints are off to get one without the name. Names on recordings that existed before 0.6.0 follow the upgrade rule in Upgrading to 0.6.0.

What off hides, and what it keeps​

When voiceprints are off for a recording, a speaker name that a voiceprint match wrote is replaced by the generic speaker label (for example SPEAKER_00) everywhere the name would be shown or searched:

  • the transcript, exports and captions,
  • the speaker list and the gallery speaker chips, and filtering by speaker,
  • chat's "who said" lookup,
  • the speaker labels and summary text in the search index,
  • the speaker-detection prompts,
  • profile appearance counts.

A profile link that a voiceprint added is hidden and not counted. A name a person typed, accepted or confirmed is never hidden, with or without a machine-added link. Nothing is deleted by hiding: turning the switch back on shows everything again.

Hiding is immediate in responses. The search index is rebuilt in the background (usually within minutes, longer for a very large library); if the rebuild cannot run, the periodic search maintenance finishes it. The rebuild is recorded as the privacy.labels.resync audit event (see Audit events).

Summaries already written​

Summaries and topics that a language model generated earlier are stored as written and may still name a person whom a voiceprint identified. Their copy in the search index and in chat is masked while voiceprints are off, but the stored text is not rewritten. To remove the name from the stored text, regenerate the summary after turning voiceprints off. Only the full name recorded on the speaker is masked, not a first name on its own.

Where inferred gender is withheld​

While gender is off for a recording's owner and organization, a response never carries the inferred value, wherever it embeds a speaker. A segment embeds its full speaker, so the speaker list and each segment's speaker are redacted together, by one function (redact_response_speakers in backend/app/api/endpoints/files/crud.py). It covers:

  • the file detail (GET /files/{file_uuid}),
  • GET /files/{file_uuid}/segments,
  • the response of a segment text edit (PUT /files/{file_uuid}/transcript/segments/{segment_uuid}),
  • the response of a segment speaker reassignment (PUT /transcripts/segments/{segment_uuid}/speaker).

The stored attribute data keeps the inference under three keys of attribute_confidence: gender (the model's confidence), alignment and alignment_hint (the voice-versus-first-name comparison, which is itself derived from the inferred gender). All three are withheld together, and the alignment fields the API serves (gender_alignment, gender_alignment_hint) disappear with them. The purge drops the same three keys.

Purging stored data​

A purge deletes what a switch only hides. It is irreversible.

Targets. Choose voiceprints, inferred_gender or both.

TargetDeletedKept
voiceprintsNames and links a voiceprint made revert to unnamed and unlinked (see Names and links a voiceprint made), and those speakers leave promoted clusters (a promoted cluster left empty is deleted, except that a person's own purge keeps an emptied cluster another member owns and deletes only its centroid document). Speaker, profile and cluster-centroid documents in every speaker index (the legacy backup index included), selected by the speakers, profiles and clusters of the purged scope; an organization purge also deletes the documents carrying that organization's own stamp. Speaker match rows with either side in the scope. Unpromoted clusters whose members are all in the scope. Voiceprint-based name suggestions, with their confidence and detail, on any speaker, also one a person verified; that speaker's name, verified mark, profile link and identity record are left as they are (the affected speakers' status and label are refreshed and their recordings are queued for a search reindex). Each profile's embedding count and last-update time are reset.Profiles, names a person typed or confirmed, avatars, speaker assignments a person verified, and the records of promoted clusters (their centroid documents are deleted). Another tenant's rows and documents
inferred_genderOn speakers whose gender no person confirmed: the predicted gender, the three attribute_confidence keys gender, alignment and alignment_hint, and the time the attributes were predictedGenders a person confirmed, and the gender stored on a speaker profile

A gender purge does not clear a profile's gender. speaker_profile.predicted_gender is set only when a person confirms a gender, so it is a person's statement and not an inference. The inferred_gender target leaves it in place, and it is not deleted by any purge. Only the values the model inferred on individual speakers are removed.

Confirmation. The request must carry the literal "confirm": "PURGE". The interface makes the person tick a box and type PURGE. The API answers 422 to anything else.

Switch first. A target is refused with 409 policy_still_enabled unless its switch is off at the purging layer or above, because otherwise new data would grow straight back. A purge counts the layers from the instance down to the purging scope: an organization administrator can purge once the organization or the instance has the feature off, and a person can purge their own data once their own switch, their organization or the instance has it off. A person whose organization turned a feature off can therefore still delete their own data, though they can no longer change their own switch. The check fails closed, and the background task checks it again when it starts.

Two passes. The task deletes once, then repeats the same deletion about 60 seconds later. The second pass catches a write that raced the switch (the write backstop caches for up to 10 seconds). The task stays in_progress until the second pass ends. A cluster centroid document that the first pass could not delete is handed to the second pass (its cluster is already gone from the database, so only the first pass knows its id), which deletes and measures it again. If a store could not be fully purged, the task ends failed: run the purge again.

Backups and snapshots. A purge deletes from the live database and search indexes only. Database backups and OpenSearch snapshots keep their copies until they rotate out, so a restore can bring purged voiceprints back. After restoring a backup, run the purge again.

Who may purge what.

ScopeWhoRouteWhat it covers
Your organizationAn organization administratorPOST /api/org-admin/policy/purgeEvery member's data stamped with the organization
YourselfAny signed-in userPOST /api/privacy/me/purgeYour own data in the active scope (the organization you are working in, or your personal workspace)
Personal workspacesA super adminPOST /api/admin/privacy-policy/purge with "scope": "personal"Every user's personal-workspace data, never an organization's
EverythingA super admin, single-tenant deployments onlyThe same route with "scope": "all"Literally all speaker data on the instance. In a multi-tenant deployment (any active organization, or TENANCY_MODE=multi) this answers 403 purge_scope_not_allowed: a platform administrator must not destroy tenant data, and each organization purges its own

Destroying the voiceprints of specific files​

There is one more scope, files, with no route of its own: it is what a retention schedule uses to destroy the voiceprints of named recordings, whoever owns them. It runs through the same two passes, residual check and audit event as every other purge, with three differences:

  • It never deletes a profile. Each profile one of those speakers was linked to keeps its row, its name and its links. Its embedding is recomputed from the speakers it still has, once the purged speakers' voiceprint documents are confirmed gone; a profile left with no voiceprint has its embedding document erased (it is rebuilt when a voiceprint links to it again). If the deletion could not be confirmed, the embedding is erased rather than recomputed, so it can never average a deleted voice back in.
  • Every voiceprint of those files goes, including one on a speaker a person linked to a profile; the speaker row, its name and its link stay. Those speakers leave the promoted clusters whose centroid averaged them in, and the centroids are rebuilt.
  • It does not depend on a privacy switch. It is a retention destruction, recorded in the audit event as purge_reason: "retention" with purge_scope.kind: "files" and a purge_scope.file_count (never file names). Without that reason a files purge is refused.

What a purge selects, and what it never touches. Speakers belong to their recording's organization (or personal workspace), profiles to the organization they are stamped with, and a cluster to its members. A purge deletes voiceprint documents by the ids of those rows, because a document written before the tenant backfill has no stamp: stamp-based deletion alone would miss it in an organization purge and could reach it from a member's personal purge. An organization purge additionally deletes (and measures) every document that carries the organization's own stamp, which reaches documents the database has no row for (orphans, and documents keyed by the old numeric speaker id). A personal, member or whole-instance-personal purge never deletes by stamp, so a document with no stamp is out of its reach. A document stamped with the organization is the organization's even if its speaker row says otherwise. Two edge cases follow from the same rule:

  • A speaker match that joins a speaker in the purged scope to the same person's speaker in another scope is deleted (it is voice-derived data of the purged scope) and counted separately as cross_scope_matches. The other scope's speaker row is not modified.
  • A cluster that holds speakers of two scopes (possible only for clusters created before the tenant gates) is not deleted: it loses the purged scope's members and its centroid is rebuilt from the rest (cross_scope_clusters_trimmed).

Another tenant's rows, profiles and documents are never touched.

Measured and verified. Each pass returns the exact count removed from each store (what OpenSearch reports as deleted, never the number of ids asked for), then looks again, in a fresh database session and after an index refresh, for what it was supposed to remove in the scope: voiceprint-made identities, voiceprint suggestions, speaker matches, unpromoted cluster members, profile embedding counters, and the speaker, profile and cluster documents of the scope's ids (for inferred_gender: speakers that still carry an inferred gender). Every value must be 0. A non-zero value after the final pass makes the purge fail (status failed, audit outcome FAILURE, reason residual); after the first pass it is recorded and the second pass gets to finish it. A measure that could not be taken, for example because OpenSearch was unreachable, is reported as unverified, never as zero, and also fails the purge.

Read a purge back from the audit log. Every purge writes one event per target (privacy.voiceprints.purge, privacy.gender.purge) for each phase: requested, each pass completed, aborted or failed. Open the events of the purge's task id (shown in the dialog) and read the details of the completed passes:

FieldMeaning
task_id, phase, targetsWhich purge and pass, and what it was asked to delete
scopekind (organization, user, instance_personal, instance_all) and the organization_id and user_id it covered
counts.postgresspeaker_identities_reverted, speaker_links_removed, suggestions_cleared, identity_provenance_cleared, suggestion_details_cleared, speaker_matches_deleted, cross_scope_matches, speakers_unclustered, cluster_members_deleted, clusters_deleted, cross_scope_clusters_trimmed, promoted_cluster_members_removed, promoted_clusters_deleted, profiles_reset, statuses_refreshed, gender_speakers_cleared
counts.opensearchspeaker_docs_deleted, profile_docs_deleted, cluster_docs_deleted
residual, residual_ok, residual_unverifiedWhat the check found (all zeros when residual_ok is true) and which measures could not be taken
reasonresidual when anything was left, store_errors when a store reported a failed delete

The event's own fields carry the actor (user_id and the username), the organization stamp and, when an administrator purged someone else's scope, target_user_id. The request event also records the source IP and user agent. Events hold counts and ids only, never a name.

Every purge writes audit events (see below) and records counts only, never a name.

Voiceprint retention (opt-in)​

An operator can have voiceprints destroyed once nobody has used them for a set time. It is off by default: the system setting privacy.voiceprint_retention_months is 0. A value such as 24 turns on a daily task (privacy.voiceprint_retention_sweep, 05:25).

A voiceprint is used when a person links or unlinks a speaker to a profile, edits the profile, accepts or rejects a suggestion for it, or when an automatic voiceprint match writes a link. Viewing a profile, computing a suggestion and maintenance (re-embedding, cluster rebuilds, migrations, purges) do not count. The time is speaker_profile.last_interaction_at. Because automatic matches count, a voice that keeps being recognised stays in use.

A file is due when it was uploaded before the cutoff, every profile linked to one of its speakers was last used before the cutoff (an unlinked speaker counts as due), nobody holds the file (legal or organization hold), and it still has voiceprints. Due files go to the file-scoped voiceprint purge (at most 200 per day). That purge removes the files' speaker voiceprints and recomputes each affected profile from its remaining speakers; it never deletes a profile. A stale profile left with no speakers is listed in the task result for review, not deleted: deleting a named person record is a human decision.

The file-scoped purge ships separately; until it is installed the task logs that nothing was destroyed and returns unavailable.

API reference​

All routes are under /api. Request and response shapes are in backend/openapi.json. Error bodies that carry a code look like {"detail": {"code": "...", ...}}.

Effective policy​

GET /privacy/effective (any signed-in user). Optional query file_uuid: the answer is for that recording's owner and organization. A recording you cannot see, or one that does not exist, is a 404 exactly like the file page. Without it, the answer is for you in your active organization.

{
"organization_uuid": "5b1f0c7e-...",
"voiceprint_matching": { "enabled": false, "locked_by": "organization" },
"gender_inference": { "enabled": true, "locked_by": null }
}

organization_uuid is null for personal scope. locked_by is null exactly when enabled is true.

A recording's voice identification​

PATCH /files/{file_uuid}/voiceprint-consent with {"state": "granted" | "declined"} (the owner only; anyone else, or an unknown file, is a 404; refused under a support grant). Answer:

{
"state": "declined",
"previous_state": "granted",
"changed": true,
"purge_task_id": "0b2c...",
"reprocess_to_create_voiceprints": false
}

declined always queues the purge (a repeated declined is how a lost purge is retried; poll GET /privacy/purge/{purge_task_id}). If the purge cannot be queued the state is saved and the answer is 503. POST /files/prepare accepts voiceprint_consent (granted or declined; 422 otherwise) and POST /files/complete accepts the same field but only restricts: declined is applied, granted never widens what prepare recorded and is ignored, not refused, because clients retry complete. The file detail response carries voiceprint_consent and voiceprint_consent_at. GET|PUT /admin/privacy-policy gain voiceprint_upload_consent_required.

Your own voiceprint switch​

GET /privacy/me and PUT /privacy/me. Refused under a support-access grant.

{ "voiceprint_matching_enabled": true, "locked_by": null }

PUT takes {"voiceprint_matching_enabled": false} (a real boolean; "no" or 0 is a 422) and returns the same shape. locked_by is platform, instance or organization when a layer above you has the feature off; PUT then answers 409 {"code": "policy_locked", "locked_by": "..."}.

Your own gender switch​

The gender switch is the existing per-user speaker attribute setting, GET /user-settings/speaker-attributes and PUT /user-settings/speaker-attributes. Both resolve for the organization the request is working in (the personal workspace when there is none).

{
"detection_enabled": true,
"gender_detection_enabled": false,
"gender_locked_by": "organization",
"age_detection_enabled": true,
"show_attributes_on_cards": true,
"auto_llm_speaker_identification": true
}

gender_detection_enabled is the effective value: it is false when a layer above you forbids gender inference, whatever you chose, and also false when you turned it off yourself. Your stored choice is untouched, so it applies again once the restriction is lifted. gender_locked_by is platform, instance, organization or null. Your own switch is never reported as a lock, so it is null when only you turned the feature off. It is read-only: a PUT ignores it, and a PUT stores your choice even while a layer above has the feature off (unlike the voiceprint switch, it does not answer 409).

Instance layer (super admin)​

GET /admin/privacy-policy and PUT /admin/privacy-policy.

{ "voiceprint_matching_enabled": true, "gender_inference_enabled": true }

PUT is partial (send either or both; at least one is required, and values must be booleans). Each changed switch is audited as admin.settings.change.

Organization layer (organization administrator)​

GET /org-admin/policy and PATCH /org-admin/policy. The organization is always the caller's own; there is no organization parameter. Gated by the org.defaults capability: when it is off the route answers 404, even for a platform superuser. Refused under a support-access grant. A community install has no organizations, and the capability is off, so these routes answer 404 there.

{
"voiceprint_matching_enabled": true,
"gender_inference_enabled": true,
"instance": { "voiceprint_matching_enabled": true, "gender_inference_enabled": true },
"effective": {
"voiceprint_matching": { "enabled": true, "locked_by": null },
"gender_inference": { "enabled": true, "locked_by": null }
},
"redaction_floor": {
"forced_categories": [],
"forced_pii_entities": [],
"forced_custom_words": [],
"force_toxicity_threshold": null,
"force_export_redacted": false,
"force_redact_before_llm": false
},
"updated_at": null
}

voiceprint_matching_enabled and gender_inference_enabled are the organization's own switches; instance shows what the deployment allows (a switch the operator turned off cannot be turned on here); effective is what applies once the layers above are counted. updated_at is null until an administrator saves a policy. redaction_floor is null while the redaction.policy capability is off for the request.

The organization redaction floor has an API but no screen in this release: the organization privacy panel shows the two switches only. A floor screen is part of #1279, planned for v0.7.0. Until then an administrator sets a floor through the API, and the stored floor is combined with the deployment's floor (strictest wins).

PATCH is partial and needs at least one field: voiceprint_matching_enabled, gender_inference_enabled and/or redaction_floor. Sending redaction_floor while redaction.policy is off answers 404. A floor can only add to the deployment's floor (see Content redaction). Validation errors are 422: categories must be pii, toxicity or profanity; PII entities must be supported entities; custom words are at most 200 words of at most 100 characters; the toxicity threshold is between 0 and 1 (an explicit null clears it); other floor fields do not accept null. Each PATCH writes an org.policy.change audit event listing field names only, never values, because a custom word can be sensitive.

Admin migration and consistency checks​

The admin migration and consistency routes report what the policy makes a batch job skip. The policy adds fields to existing routes and creates no new route.

GET /embeddings/migration/status (super admin) carries policy_skipped, so the admin sees what a migration would skip before starting it:

{
"files_total": 240,
"files_skipped": 31,
"scopes_total": 12,
"scopes_skipped": 2,
"locked_by": ["organization"]
}

files_total counts completed recordings, and files_skipped those whose owner and organization have voiceprints off. locked_by lists the layers responsible.

POST /embeddings/migration/start answers 409 {"code": "voiceprints_disabled", "locked_by": "<layer>"} only when every recording is in a forbidden scope, because then nothing could be migrated. Otherwise it starts, and its response carries the same policy_skipped object (only when something is skipped) naming what the run will leave out.

The embedding consistency check reports skipped_by_policy in the orchestrator's return value and its stored last run, in the progress payload of a repair, and in the dry-run counts (GET /admin/embedding-consistency/counts). A speaker with no document in a scope whose voiceprints are off is the policy working, not damage, so it is not counted in main_missing or v4_missing and no repair is queued for it.

Purge routes and status​

Request bodies for the three purge routes:

{ "targets": ["voiceprints", "inferred_gender"], "confirm": "PURGE" }

POST /admin/privacy-policy/purge also needs "scope": "personal" or "all". Each answers 202:

{ "task_id": "3f6c1c0e-..." }

Errors: 409 {"code": "policy_still_enabled", "targets": ["voiceprints"]} (the listed targets are still on); 403 {"code": "purge_scope_not_allowed"} (scope all in a multi-tenant deployment); 422 (confirm is not PURGE, or no target); 503 when the task could not be queued (nothing was deleted).

GET /privacy/purge/{task_id} returns the status of a purge you started. Anyone else's, or an unknown id, is a 404.

{
"task_id": "3f6c1c0e-...",
"status": "completed",
"counts": {
"postgres": { "speaker_identities_reverted": 12, "speaker_matches_deleted": 40, "clusters_deleted": 17 },
"opensearch": { "speaker_docs_deleted": 412, "profile_docs_deleted": 9, "cluster_docs_deleted": 17 }
},
"residual": { "speaker_identities_machine": 0, "speaker_docs": 0, "profile_docs": 0 },
"residual_ok": true,
"residual_unverified": [],
"audit": [{ "event_type": "privacy.voiceprints.purge", "task_id": "3f6c1c0e-..." }]
}

status is pending, in_progress, completed, failed or skipped. counts is null until a pass has finished, is grouped per store (postgres, opensearch) and sums both passes; the keys are listed under Read a purge back from the audit log. residual is what the latest pass measured afterwards; residual_ok is true only when everything was measured and is zero, and anything left makes status failed. audit lists the audit events (one per target) that record the purge.

Audit events​

An organization administrator sees the organization's events in Settings, organization group, Audit log (titled "Organization audit log"; managed editions with the org.audit_logs capability, route GET /org-admin/audit-logs). The three events below are stamped with the organization, so they appear there and in its event filter, labelled "Changed the privacy policy", "Deleted stored voiceprints" and "Deleted inferred gender data".

EventWhen
admin.settings.changeAn instance switch changed (setting name, old and new value)
org.policy.changeAn organization policy PATCH (field names only; stamped with the organization)
privacy.voiceprints.purge, privacy.gender.purgeOne event per target for each phase of a purge: requested, each pass completed, aborted or failed. The completed passes carry the purge_scope, the exact counts per store and the residual check (see Read a purge back from the audit log). Counts and ids only; stamped with the organization when there is one
privacy.file_consent.changeA recording's voice identification state changed (file id, old and new state, via). Never a file name
privacy.labels.resyncA voiceprint switch changed and the speaker labels in the search index are being rebuilt. Written when the rebuild is requested and again when it finishes. details hold the switch, its previous and new value, the scope, and the counts of files selected, queued, already pending and failed. An unfinished rebuild is marked incomplete and retried by the periodic search maintenance. Never names a person or a file

Data inventory​

What the two features store, and what each purge target removes. This is the starting inventory for #1132.

DataStoreCreated whenHidden byDeleted by
Speaker voiceprint (one per speaker in a recording)OpenSearch speaker index (speakers alias, speakers_v3 / speakers_v4)Speaker identification after transcriptionVoiceprint switchvoiceprints
Profile embedding (average of a profile's voiceprints)OpenSearch speaker index; speaker_profile.embedding_count, last_embedding_updateA speaker is linked to a profileVoiceprint switchvoiceprints
Cluster centroidOpenSearch speaker index; speaker_cluster, speaker_cluster_member, speaker.cluster_idCross-file clusteringVoiceprint switchvoiceprints (unpromoted clusters)
Speaker matchspeaker_matchMatching a speaker against othersVoiceprint switchvoiceprints
Voiceprint-based name suggestionspeaker.suggested_name, suggestion_source (voice_match, profile_match), confidenceMatchingVoiceprint switch (when unverified)voiceprints (when unverified)
Inferred gender and confidencespeaker.predicted_gender, attribute_confidence, attributes_predicted_atThe gender step, or the diarizer sidecarGender switchinferred_gender (when not confirmed)
Confirmed gender, named profiles, avatars, verified assignmentsDatabaseA person's actionNeverNever by a purge

For operators​

  • Remote LLM speaker-ID prompts no longer receive the gender cross-reference. The speaker-name step used to add a voice-versus-first-name comparison ("voice: female 92%") to its prompt. It now adds it only when the configured LLM is local (a loopback, private-network or compose-hostname endpoint); a remote provider gets the prompt without it, even with gender inference on. The check fails closed: any ambiguity reads as remote. Sending an inferred biometric attribute to a third party is a data-egress event, and this follows the same local-versus-remote rule as masked transcript text.
  • A community install has no organization layer. org.defaults is off there, so /api/org-admin/policy and its purge route answer 404, and only the instance and user layers apply.
  • After upgrading an install that uses organizations, run the tenant backfill described in Upgrading to 0.6.0. It also re-stamps the cluster documents of single-organization clusters that an earlier release rewrote without their organization. Clusters whose members span scopes stay unstamped until the next re-cluster.

Self-service account erasure​

Separate from the voiceprint and gender switches, a super administrator can let people erase their own account (Settings > Privacy & Redaction > policy tab). It is off by default, because on an employer-run deployment erasure is the controller's process.

SettingKeyDefaultRange
Self-service account erasuregdpr.self_erasure_enabledoffon/off
Waiting period (days)gdpr.self_erasure_grace_days140 to 21

The cap exists because the one-month deadline for handling the request (GDPR Art. 12(3)) starts at the request, not at the end of the waiting period. A request is an entry in the erasure ledger, so it has one deadline clock and survives a database restore (the on-disk journal re-opens it as a request, or as cancelled, never as an immediate erasure). The person gets a request receipt (ledger ID plus a sha256 digest); the erasure receipt is the separate, measured record that follows. Both settings are audited as admin.settings.change. User-facing steps: Erase my account.