# Persona, Avatar, and Voice Packs

Every voice the customer hears, every face they see, and every persona they
converse with is governed by one **cross-cutting catalog with consent, lineage,
and lifecycle metadata**. This catalog is not a per-domain concern: a single
registry binds a persona's role and policy pack, its voice profile, its avatar
pack, and the consent artifacts that license its likeness, then enforces a
release gate before any of it reaches a member. This page sits beside
[Cross-Domain Support](./cross-domain-support.md) and
[Trust, Safety, and Privacy](./trust-safety-and-privacy.md); the product scope
is
[`V1/features.md` § Persona, Avatar, and Voice Packs](../features.md#persona-avatar-and-voice-packs)
and the backlog is §14.

The headline fact, often missed: the registry is **genuinely, deeply
implemented**, far beyond a contract skeleton. `libs/oshun/persona-registry/`
holds a fingerprinted `PersonaRecord` registry, a frozen catalog of the seven
canonical roles with capability ceilings, real `AvatarPack` / `VoiceProfile` /
`ConsentLedger` contracts, ~14 individually tested evaluation modules, drift
detectors, disclosure-visibility gates, and end-to-end signoff tests. What is
not yet real — actual avatar rendering and lip-sync against ground-truth video,
and live cloned-voice TTS output — is called out below **with which kind of
not-yet it is**, because "provider-gated" covers three different states and only
one of them is a key away; the registry is the _contract and policy_ layer, not
the render farm.

## The registry — `libs/oshun/persona-registry/`

The package `@oshun/persona-registry` is the contract layer: pure data plus pure
functions (get, list, filter, validate, fingerprint), no IO, no clock —
timestamps are inputs, not outputs. The registry is modeled as an **immutable
snapshot** (`PersonaRegistrySnapshot`): a map of records keyed by a stable
`PersonaId`, plus an `orderedIds` index and a deterministic **fingerprint**
(`fingerprintPersonaRegistrySnapshot`) so distributed callers can verify they
agree on the same registry state without a deep-equal.

An architecturally important design choice: the registry is **dependency-free by
intent**. Its `index.ts` explicitly _does not_ import
`@oshun/persona-policy-lilith`; instead each `PersonaRecord` carries an opaque
`policyPackReference` string that the _caller_ resolves against the Lilith
policy package. This keeps the registry consumable by any domain without pulling
in the policy engine, and keeps the policy substrate swappable.

The five files the architecture hub names all exist and are real:
`realism-impersonation-thresholds.ts`, `voice-provider-abstraction.ts`,
`avatar-pack.ts`, `watermark-provenance.ts`, `launch-multimodal-assignments.ts`.

### `PersonaRecord` — the canonical record

The registry's own record type is **`PersonaRecord`** (`index.ts`), not a type
named `Persona`. (A `Persona` _does_ exist, but it is the Zod-inferred contract
type at `libs/contracts/src/common/persona.ts` — `z.infer<typeof PersonaSchema>`
— a different artifact in a different package. The registry's record is
`PersonaRecord`.) Its core fields:

| Field                            | Meaning                                                                                       |
| -------------------------------- | --------------------------------------------------------------------------------------------- |
| `id` / `displayName` / `summary` | Stable URL-safe id (lowercase, digits, hyphens, 3–64 chars) + labels                          |
| `family`                         | One of the nine `PERSONA_FAMILY_KINDS`                                                        |
| `status`                         | One of the `PERSONA_APPROVAL_STATUSES` (lifecycle state)                                      |
| `surfaces`                       | Subset of `PERSONA_SURFACES` (`tara`, `arete`, `metis`, `studio`, `admin`, shell surfaces, …) |
| `policyPackReference`            | Opaque Lilith policy-pack reference the caller resolves                                       |
| `ownerTeam` / `rationale`        | Governance owner + human-readable rationale surfaced in admin                                 |
| `lineage`                        | Declared tradition lineage slugs the persona may speak from (e.g. `theravada-vipassana`)      |
| `scope`                          | Explicit bound on the conversational territory the persona may inhabit                        |

The coarse `PERSONA_FAMILY_KINDS` taxonomy has **nine** members —
`contemplative`, `coach`, `explainer`, `guide`, `scholar`, `support`,
`moderator`, `reviewer`, `executive` — distinct from the seven _roles_ below.

### The seven canonical roles

`roles/catalog.ts` defines exactly **seven** canonical roles, in order:
`teacher`, `coach`, `explainer`, `steward`, `comparative`, `narrator`,
`assistant` — matching
[`V1/features.md` § Persona Taxonomy](../features.md#persona-taxonomy-tone-band-catalog-and-crisis-recovery-journey)
and the hub. `roles.test.ts` asserts the `Set` of exactly those seven. The
catalog is **frozen at build time** (runtime modification forbidden), and each
role declares a **capability ceiling**, a **default tone band**, and a set of
**hard-bans**:

| Role          | Capability ceiling (examples)                                                                | Default tone    | Notable hard-bans                                                       |
| ------------- | -------------------------------------------------------------------------------------------- | --------------- | ----------------------------------------------------------------------- |
| `teacher`     | `voice-tts`, `avatar-render`, `living-scene-render`, `memory-read/write`, `scheduled-invite` | `contemplative` | `cross-lineage-mix`, `medical/financial/legal-advice`, `sales-language` |
| `coach`       | `voice-tts`, `avatar-render`, `memory-read/write`, `scheduled-invite`                        | `reflective`    | `cross-lineage-mix`, `commanding-imperative`, `guarantee-language`      |
| `explainer`   | `voice-tts`, `avatar-render`, `living-scene-render`, `memory-read`                           | `neutral`       | (read-only memory; explanatory register)                                |
| `comparative` | includes `cross-lineage-mix`                                                                 | —               | the **only** role permitted to cross lineages                           |

The capability ceiling is what makes this enforceable rather than advisory: a
`teacher` _cannot_ mix lineages because `cross-lineage-mix` is in its hard-bans
and absent from its ceiling. The test suite proves `comparative` is the sole
role holding `cross-lineage-mix` in its ceiling. `roles/types.ts` and
`roles/construct.ts` back the catalog (constructing a concrete persona from a
role plus per-persona deltas).

### The approval lifecycle

`PERSONA_APPROVAL_STATUSES` carries both the canonical states and legacy
aliases. The **canonical** path (`PERSONA_APPROVAL_STATUSES_CANONICAL`) is:

```
drafted → in-review → rehearsal → approved-for-test → approved-for-release → released → deprecated → retired
```

Aliases (`draft`→`drafted`, `approved`→`in-review`, `published`→`released`) are
normalized by `canonicalizePersonaApprovalStatus`. The lifecycle is not just an
enum — `lifecycle/` implements a `state-machine`, a `review-queue`, a
`provenance-bundle`, `champion-challenger` testing, `re-eval-cadence`,
`revocation-cascade`, and `retirement-recovery`. The completeness audit
(2026-06-22) marks the **persona-voice-avatar-approval-workflow** as
**PARTIAL**: registry lifecycle and signoff are well covered, but full
deprecate/retire/recovery and the consumer-picker round-trip are thinner than
the create→approve→release path. The honest read is "approval and signoff are
real; the back-half retirement/recovery and end-to-end consumer picker are in
progress."

## Contracts — VoiceProfile, AvatarPack, ConsentLedger

The registry's multimodal contracts are real interfaces with validators, not
placeholders.

**`AvatarPack`** (`avatar-pack.ts`) binds a persona to its visual assets — a
`baseMesh`, `blendshapesAsset`, and `textureSet` (glTF / USDZ / FBX references),
a `riggingVersion`, and the full `exposedBlendshapes` set and `visemeMap` for
lip-sync. It also carries `renderingConstraints`, `license`, and `watermark`
metadata. License validity is checked at runtime by `checkAvatarPackLicense`,
which returns an `AvatarPackLicenseCheckResult` (`active` and `reason`) —
refusing to render when a license is not yet in effect or has expired.

**`VoiceProfile`** (`voice-profile.ts`) binds a persona to a voice: a `kind`,
internal `engineId` and opaque `engineModelRef`, BCP-47 `supportedLocales` /
`defaultLocale`, an `acousticBaseline`, `provenance` requirements, and the
`requiredConsent` artifact kinds. The §14.3 fields make the consent story
explicit: a commercial `vendor` (ElevenLabs, Microsoft, …) distinct from the
internal engine, and a `sourceIdentityRef` (a stable reference to the
talent/source person for _cloned_ voices, `null` for synthetic). It also carries
a `consentReference` into the ledger, a `prosodyProfile`, and a
`naturalnessBaseline`.

**`ConsentLedger`** (`consent-ledger.ts`) is the real consent primitive. Note
that there is **no type named `ConsentRecord` anywhere in the registry**; the
docs' older "every persona binds a `ConsentRecord`" phrasing names a type that
does not exist. The actual primitive is the ledger: a `byKind` map indexing
`ConsentArtifact`s by kind, each artifact carrying an expiry, a
`ConsentArtifactScope`, and an optional `ConsentRevocation`. Runtime
authorization flows through `authorisePersonaConsent`, which takes a
`ConsentAuthorisationRequest` (persona id, required kinds, surface, modality,
commercial-use flag, current time, the ledger) and returns a
`ConsentAuthorisationResult` with per-kind findings of
`ok | missing | expired | revoked | scope-mismatch`. An authorized result
requires **every** required kind to have at least one unrevoked, non-expired
artifact whose scope covers the requested surface, modality, and commercial use.
Revocations propagate through `lifecycle/revocation-cascade` (a takedown
cascade), so pulling consent retracts the avatar and voice that depended on it.

## Realism, impersonation, and provenance thresholds

`realism-impersonation-thresholds.ts` ships the canonical realism ceilings.
Avatars are ranked across an ordered `AVATAR_PHOTOREALISM_TIERS` ladder
(`illustrative` → `stylised-3d` → `semirealistic-3d` → `photoreal-generic` →
`photoreal-specific`), voices across `VOICE_NATURALNESS_TIERS` (`flat-tts` →
`warm-synthetic` → `naturalistic` → `cloned-consented` → `cloned-unconsented`).
There are **universal prohibitions**: `VOICE_PROHIBITED_TIERS` bars
`cloned-unconsented` outright — a voice cloned from a specific real person
without consent is _always_ impermissible, enforced regardless of context. Each
policy class also declares a `requireC2paManifest` flag.

`requireC2paManifest` is a **real, recurring policy input**, referenced across
`voice-profile.ts`, `voice-provider-abstraction.ts`,
`realism-impersonation-thresholds.ts`, and `watermark-provenance.ts`. The last
of these defines `PROVENANCE_RULES` per `ProvenanceScenario`, most of which set
`requireC2paManifest: true`; `checkProvenanceCompliance` then verifies a
`ProvenanceClaim` actually carries a C2PA manifest (and issuance time) before
the content is allowed out. So "C2PA provenance" is not aspiration — it is a
boolean the release gate reads.

Voice-quality and voice-abuse policy is enforced by
`@oshun/persona-policy-lilith` (the package the registry references via the
opaque `policyPackReference`) together with these registry thresholds, including
`requireC2paManifest` as one input at release-gate time.

## The evaluation suites — real, tested modules

Where [`V1/features.md` § Evaluation Suites](../features.md) describes the eval
strategy in prose, the registry implements each suite as an **individual,
independently tested module** (each `eval-*.ts` ships with a sibling
`eval-*.test.ts`):

| Module                               | What it scores                                                                                                       |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `eval-lipsync-alignment`             | Lip-sync vs. audio alignment across `LIPSYNC_ALIGNMENT_AXES` (default threshold `0.85`, hard-fail axes at min `0.9`) |
| `eval-coherence`                     | Persona conversational coherence                                                                                     |
| `eval-deceptive-realism-risk`        | Risk that realism is deceptive (avatar/voice too real for the context)                                               |
| `eval-impersonation-risk`            | Risk the persona impersonates a real, specific person                                                                |
| `eval-cloned-voice-red-team`         | Red-team battery against cloned-voice abuse                                                                          |
| `eval-disclosure-visibility`         | Whether the "AI persona" disclosure is actually visible enough                                                       |
| `eval-expression-quality`            | Quality of facial/vocal expression                                                                                   |
| `eval-multimodal-identity-coherence` | Whether voice + avatar + text present one coherent identity                                                          |
| `eval-style-consistency-drift`       | Drift from the persona's established brand/style                                                                     |

These are backed by drift detectors and disclosure measurement: the "disclosure
visibility ≥ N time-on-screen" trust gate is implemented concretely by
`disclosure-axis-drift-detector.ts`, `disclosure-visibility-measurement.ts`
(per-surface `DISCLOSURE_VISIBILITY_FLOORS_PER_SURFACE` floors), and
`token-time-disclosure-filter.ts`. The "prompt-time impersonation-attempt
detection" trust gate is implemented by `prompt-impersonation-detection.ts`
(`detectPromptImpersonationAttempt` over `PROMPT_IMPERSONATION_PATTERNS`).
End-to-end signoff is exercised by tests like
`admin-approval-rollback.e2e.test.ts` and `launch-family-signoff.e2e.test.ts`.

## Launch cast assembly

The actual launch cast is assembled — not merely "signed off" — by four modules:
`launch-roster.ts` (the `LAUNCH_ROSTER` of `LaunchRosterEntry` records across
`LAUNCH_WAVES` wave-1/2/3, with per-entry and whole-roster validators),
`launch-family-signoff.ts` (`evaluateLaunchPersonaSignoff`,
`evaluateLaunchFamilySignoff`, `evaluateAllLaunchFamiliesSignoff`, with rollback
evidence and a `summarizeLaunchSignoff`), `launch-persona-config.ts`
(per-persona prompt / policy / grounding / memory / disclosure config), and
`launch-multimodal-assignments.ts`. The last binds each launch persona to its
concrete modalities:

```ts
// libs/oshun/persona-registry/src/launch-multimodal-assignments.ts
export interface LaunchMultimodalAssignment {
  readonly personaId: string;
  readonly familyId: PersonaFamilyId;
  readonly enabledModalities: PersonaModalitySet;
  readonly voiceProfile: VoiceProfile | null; // null for text-only families
  readonly avatarPack: AvatarPack | null; // null for text-only / voice-only
  readonly rationale: string;
}
```

`listLaunchVoiceProfiles()` and `listLaunchAvatarPacks()` flatten the launch
roster into the concrete voice/avatar assets the runtime must provision, with a
launch-wide commercial-use license window and canonical watermark algorithm ids
(`oshun-phase-watermark-v1`, `oshun-avatar-watermark-v1`) the runtime must
recognize. This is the cast-assembly machinery — richer than a generic "release
signoff" line.

## Voice provider abstraction — swappable, ElevenLabs-backed

`voice-provider-abstraction.ts` keeps providers swappable. The canonical
provider set (`CANONICAL_VOICE_PROVIDERS`) is `oshun-native`, `elevenlabs`,
`xtts-local`, `tortoise-local`, `openvoice-local`, `custom`, each with declared
capabilities (`synthetic-tts`, `cloned-tts`, `multilingual`, `streaming`,
`emotion-conditioning`, `long-form`) and a fallback order
(`CANONICAL_VOICE_PROVIDER_FALLBACK_ORDER`, e.g. synthetic →
`oshun-native, xtts-local, elevenlabs, custom`). Cloned-voice safeguards
(`CANONICAL_CLONED_VOICE_SAFEGUARDS`) and per-output-class watermark
requirements are declared alongside.

The "ElevenLabs (and future providers) swappable" claim is backed by **real
provider integration files**:
`libs/isis/ai-providers/src/providers/tts/elevenlabs-provider.ts` and
`libs/isis/audio-generation/src/voice/elevenlabs-client.ts`. Honest caveat: the
registry layer defines the abstraction, fallback order, and quality
requirements; the provider modules exist but live cloned-voice TTS output is
**credential-gated** — the code path is complete and no key has been supplied
here, so it was not exercised end-to-end in this architecture pass. Actual
avatar **rendering and lip-sync against ground-truth video** is a different
state again: see the vocabulary below, and
[Phase 182](#phase-182--where-the-rendering-half-actually-stands) for where that
work now lives. The registry ships the `eval-lipsync-alignment` scorer and
`lipsync-ground-truth-pairs` fixtures; the ground-truth render comparison itself
has no route that has ever produced a comparable render.

## Phase 182 — where the rendering half actually stands

The registry above is the contract and policy layer. The thing it governs — a
video of a real person saying approved words — is built in **Phase 182**
(`TODOS/phase-182.md`), and its architecture is worth stating here because this
page is where people come looking for it.

### The path

```
authorized references -> approved script -> controlled speech audio -> shot plan
  -> native audio-conditioned video candidates
  -> audiovisual / identity / safety evaluation -> select or retry
  -> encode / provenance -> release
```

### The delivery guarantee, stated honestly

Phase 182 treats **perfect lip sync as a delivery requirement, not a claim that
every model's first sample will be perfect.** Oshun may use a native
audio-conditioned model so that generation and synchronisation happen in one
render, and it still inspects the actual output frames and audio. A take that
misses words, assigns speech to the wrong person, drifts out of sync, hides the
mouth for an unjustified duration, damages identity, or crosses a release
threshold is rejected, retried, rerouted, or sent for **explicitly disclosed**
corrective processing.

So the promise is narrow and checkable: **no known bad-sync take is released**,
while measured pass rates and limitations stay visible. It is not "the lip sync
is perfect".

### Exact dialogue is not a post-processing step

The approved audio is **supplied to the video model**, not synchronised onto its
output afterwards. Prompt-only generated dialogue remains an optional creative
mode and is _not_ the exact-dialogue mode, because a video model may paraphrase,
omit words, mispronounce names, or change timing — and nothing downstream can
tell you it did unless the words were fixed first.

Today, of the five model families in the adapter registry, **only Wan 2.7/2.6
carries supplied audio on the OpenRouter route**; every other family refuses
exact-dialogue mode for want of an audio input. That is measured from the
registry rather than asserted, and it means exact dialogue and multi-speaker
currently rest on a single family.

### The active-scene lane is not the avatar lane

This is the distinction this page's `avatar-render` capability does not express.
An avatar lane renders **a head speaking a script against a chosen background**.
An active-scene lane renders **a person performing in a place**. The two can
match on every capability a request names — duration, resolution, references,
seed — and produce products a creator would never confuse.

So routing across that boundary is **the wrong product, not a degraded one**,
and it is a refusal rather than a warning. Five capability classes exist —
active-scene, avatar, video-edit, dubbing, corrective-sync — and the last two
are declared with no mode reaching them yet, because a class with no mode is a
visible gap while an undeclared class is an invisible one.

### Status, so nobody reads this as shipped

The Phase 182 modules are implemented and unit-tested; **none of them is
mounted** — the human-video routes have no caller outside their own specs. No
route has a continuity benchmark, no evaluator has scored a real generated clip,
and nineteen threshold sets are exported as `PROVISIONAL` pending calibration.

The detail lives in these, all of which state their own gaps:

- [Creator workflow](../../docs/domains/isis/human-video/creator-workflow.md)
- [Honest limitations and troubleshooting](../../docs/domains/isis/human-video/limitations.md)
- [API guide](../../docs/domains/isis/human-video/api-guide.md)
- [Provider operations](../../docs/domains/isis/human-video/provider-operations.md)
- [Evaluator operations](../../docs/domains/isis/human-video/evaluator-operations.md)
- [Review and escalation](../../docs/domains/isis/human-video/review-and-escalation.md)
- [Media lifecycle and provenance](../../docs/domains/isis/human-video/media-lifecycle.md)
- [V1 migration guide](../../docs/domains/isis/human-video/v1-migration.md)

## What's real vs. spec-only — the honest split

- **Real and tested in-repo:** the `PersonaRecord` registry with deterministic
  fingerprinting; the frozen seven-role catalog with capability ceilings;
  `AvatarPack`, `VoiceProfile`, and `ConsentLedger` contracts with validators
  and `authorisePersonaConsent`; the realism/impersonation/provenance thresholds
  with `requireC2paManifest` wired into the gate; ~14 eval modules plus drift /
  disclosure detectors; and the launch roster / family-signoff /
  multimodal-assignment cast-assembly machinery — all with sibling test suites.
- **Not yet real**, in three distinct states — see below.

### "Provider-gated" is three states, and they need three different actions

The phrase reads as "a key away" to every reader, and at least two of the things
it covered here are not. `182.C.10.04` already drew this line for provider
probes — `probe_inconclusive` is not `probe_failed`, `region_unrecorded` is not
`region_unavailable` — and the same distinction belongs in the prose:

| State                | What it means                                                                                 | What closes it                                              |
| -------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| **credential-gated** | the code path is complete and nobody has supplied a key here                                  | supply a key and run it                                     |
| **unverified**       | the code exists and has never run against the provider at all, so whether it works is unknown | a live probe (`182.C.10.04`), bound to the dated model slug |
| **unreachable**      | no route we have can do this, whatever keys exist                                             | build it or drop the claim                                  |

Applied to this page's own edges:

| Claim                                       | State            | Evidence / gap                                                                                                                             |
| ------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Live cloned-voice TTS output                | credential-gated | provider modules exist (`elevenlabs-provider.ts`, `elevenlabs-client.ts`); no key supplied in this pass                                    |
| Avatar rendering + lip-sync vs ground truth | unreachable      | no route has produced a comparable render; the scorer and fixtures exist, the render does not. Phase 182 §17–§20 is the work               |
| Exact-dialogue video (approved words)       | unverified       | one model family carries supplied audio on the OpenRouter route; the path is implemented and **not mounted** — no caller outside its specs |
| Back half of the approval workflow          | partial          | deprecate/retire/recovery and consumer-picker round-trip, audited **PARTIAL** on 2026-06-22                                                |

Matching the V1 docs' candor: honest "not yet, and here is which not-yet" beats
both fake "shipped" and a phrase that quietly implies somebody just needs to
find the API key. The policy and contract layer is genuinely shipped. The
backlog is §14, and the rendering half's own tracker is
`docs/domains/isis/human-video/implementation-tracker.md`, whose status
vocabulary
(`not-started → implemented → wired → live-probed → benchmark-passed → release-enabled`)
is the finer-grained version of the three states above.

## Related

- [Cross-Domain Support](./cross-domain-support.md)
- [Trust, Safety, and Privacy](./trust-safety-and-privacy.md)
- [Lilith — Contemplative Policy Substrate](./substrate-lilith.md)
- [Isis — Generation Control Substrate](./substrate-isis.md)
- [Oshun Studio — Authoring, Editorial, Curation](./oshun-studio.md)
- [Subsystem Glossary](./glossary.md)
- [`V1/features.md` § Persona, Avatar, and Voice Packs](../features.md#persona-avatar-and-voice-packs)
- [Hub: V1 Architecture](../ARCHITECTURE.md)
