Skip to content

feat(realtime): speaker-aware conversations - surface identity to client and LLM - #10424

Merged
mudler merged 11 commits into
masterfrom
feat/realtime-speaker-aware
Jun 21, 2026
Merged

feat(realtime): speaker-aware conversations - surface identity to client and LLM#10424
mudler merged 11 commits into
masterfrom
feat/realtime-speaker-aware

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Speaker-aware realtime conversations

Extends the realtime voice-recognition gate so the recognized speaker is surfaced to the client and fed to the LLM for personalized replies (e.g. "Hey Jeremy, ..."), decoupled from the authorization gate and fully backward compatible.

What it does

  • Stream identity to the client via a new server event conversation.item.speaker (a LocalAI extension; OpenAI clients ignore unknown event types, so it is non-breaking):
    { "type": "conversation.item.speaker", "item_id": "...",
      "speaker": { "name": "Jeremy", "id": "spk_1", "confidence": 92.0, "distance": 0.1, "matched": true } }
  • Inform the LLM (hybrid, configurable): sets the per-message OpenAI name field on each user turn and/or appends a The current speaker is <Name>. note to the system message. Each user item carries its own speaker, so multi-speaker histories stay correctly attributed.
  • Decoupled, configurable new keys under pipeline.voice_recognition:
    • enforce (default true): the authorization gate. false resolves/surfaces the speaker without ever dropping a turn.
    • identity.{announce, announce_unknown, personalize, inject_name, inject_system_note, note_unknown}: surfacing + personalization, independent of enforce. When set, identity is resolved on every turn even under when: first.

Design

  • voiceGate is split into Resolve (embed once → types.Speaker identity) and a pure authorize policy step; the legacy Authorize wrapper is preserved so existing behavior and tests are unchanged.
  • One embed per turn feeds the gate, the client event, and personalization.
  • The gate still fails closed: when enforcing, a denied or resolve-errored utterance never reaches the LLM/tools/TTS. enforce: false never drops a turn.

Backward compatibility

A voice_recognition config with neither enforce nor identity behaves exactly as before. The new event and item field are additive.

Tests

TDD throughout (Ginkgo/Gomega). New unit + integration specs cover: the Resolve/authorize split and confidence math, event marshaling, per-turn surfacing, enforce:false never dropping, when:first×identity per-turn re-resolution, and multi-speaker history attribution. core/config and core/http/endpoints/openai both green.

Docs

docs/content/features/openai-realtime.md extended with the new config keys and the conversation.item.speaker event.

Assisted-by: Claude:claude-opus-4-8

🤖 Generated with Claude Code

mudler and others added 11 commits June 21, 2026 06:58
Add Enforce *bool and Identity *VoiceIdentityConfig to
PipelineVoiceRecognition, plus EnforceGate/IdentityEnabled/
AnnounceEnabled/PersonalizeEnabled helpers. Enforce nil defaults to
gating (backward compatible); identity surfacing is independent of the
gate.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Split the speaker authorization into a Resolve step (embed once, produce a
types.Speaker identity) and a pure authorize policy step, with a 0..100
confidence score mirroring /v1/voice/identify. The legacy Authorize wrapper is
kept so existing specs stay green.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
…peaker

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Set the per-message name field on each recognized user turn and append a
current-speaker note to the system message, both gated by the voice
recognition identity config.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Document the new voice_recognition keys (enforce, identity.*) and the
LocalAI-extension conversation.item.speaker server event in the realtime
feature docs.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
…aker history

Add two integration specs to harden the speaker-aware realtime path:

- when:first with an Identity block re-resolves the speaker every turn even
  though re-authorization is skipped after the first match: a later resolve
  error now fails closed, while a clean later resolve still surfaces and names
  the speaker.
- multi-speaker history attribution: each user turn carries its own per-message
  name and the injected system note reflects the latest speaker.

Test-only change; no production behavior was modified.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Carry the registered speaker's labels (identify mode) on types.Speaker so
they flow into the conversation.item.speaker event and the stored item.
Verify mode has no labels, so the field is omitted there.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Add a realtime-pipeline-identity config (verify mode, enforce:false, identity
announce+announce_unknown+personalize) and two e2e specs driving the real
server over a real WebSocket with the mock VoiceEmbed backend: an authorized
speaker yields a conversation.item.speaker event naming e2e-speaker (matched
true) and reaches response.done; an unauthorized speaker yields an unknown
(matched false, no name) event and still responds, proving enforce:false
never drops a turn.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The meta registry coverage test (TestAllFieldsHaveRegistryEntries) requires
every config field to have an entry in core/config/meta/registry.go. The new
voice_recognition.enforce and voice_recognition.identity.* fields were missing,
failing tests-linux and tests-apple. Add registry entries (toggles) so the
fields are surfaced in the model-config editor and the coverage test passes.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
@mudler
mudler merged commit 32c4770 into master Jun 21, 2026
60 checks passed
@mudler
mudler deleted the feat/realtime-speaker-aware branch June 21, 2026 19:07
@localai-bot localai-bot added the enhancement New feature or request label Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants