Skip to content

Kid Mode: content filter on the live voice path (#157) + plumb DOTTY_ADMIN_TOKEN end-to-end#158

Merged
BrettKinny merged 2 commits into
mainfrom
feat/157-kid-mode-voice-content-filter
Jun 11, 2026
Merged

Kid Mode: content filter on the live voice path (#157) + plumb DOTTY_ADMIN_TOKEN end-to-end#158
BrettKinny merged 2 commits into
mainfrom
feat/157-kid-mode-voice-content-filter

Conversation

@BrettKinny

Copy link
Copy Markdown
Owner

What

Two commits, both pre-release items from the 2026-06-11 architecture review:

1. feat(auth) — plumb DOTTY_ADMIN_TOKEN end-to-end

The #149#152 X-Admin-Token chain shipped code-complete but operationally dead: five services read DOTTY_ADMIN_TOKEN, but no config surface provided it (not .env.example, not any compose file, not make setup, not the docs — and dotty-pi's compose had no env mechanism at all). Every deploy silently stayed permissive, i.e. the admin routes remained unauthenticated.

2. feat(kid-mode) — implement #157 per its spec

  • Pure matcher (content_filter_match, tier regexes, replacement) lifted from bridge/text.py into the shared custom-providers/textUtils.py; the bridge keeps its metrics/ring/logging wrapper on top, behaviour unchanged — no regex duplication (test-enforced)
  • New sentence-buffered filter_tts_stream(): nothing reaches TTS before its sentence is checked (word-level patterns can't straddle a sentence boundary; chunk-straddling terms are caught); a hit replaces the rest of the turn with the cheerful redirect, minus its leading emoji mid-turn to keep the one-emoji firmware contract; kid_mode off is a transparent passthrough
  • Both live providers (PiVoiceLLM, OpenAICompat) wrap their TTS-bound streams; OpenAICompat's emoji enforcement runs first so the leading-glyph contract survives
  • docs/faq.md updated: the filter is described honestly as a weak, bypassable backstop — prompt steering stays the primary defence

Acceptance (#157)

  • Pure matcher in textUtils.py; bridge/text.py imports it; no regex duplication
  • pi_voice blocks on kid_mode + tier hit; clean text passes untouched
  • openai_compat same
  • kid_mode off → zero behaviour change
  • Unit tests for both providers (hit / clean / kid-off), mirroring tests/test_dashboard_say_filter.py
  • Red-team verify on-device — bench-pending, must happen before release sign-off (per Kid Mode: blocked-words content-output filter (planned) #138)

Test plan

  • ruff check . clean
  • pytest tests/ custom-providers/pi_voice/tests/ (119) + pytest dotty-behaviour/tests/ --cov-append (213) — all pass, combined coverage 69.4% (gate 56%)
  • 16 new cases in tests/test_voice_content_filter.py (core matcher, stream wrapper, bridge parity, both providers)
  • Token generation block sandbox-tested for idempotency

Refs #157 (leaves the on-device red-team box open — close the issue after bench verification, not on merge). Also advances #138.

AI assistance: drafted by Claude Fable 5 (analysis, implementation, tests, and this PR body); human review pending per AI_TRANSPARENCY.md.

🤖 Generated with Claude Code

BrettKinny and others added 2 commits June 11, 2026 21:00
…docs)

The X-Admin-Token chain (#149-#152) shipped code-complete but
operationally dead: five services read DOTTY_ADMIN_TOKEN, zero config
surfaces provided it, so every deploy silently stayed permissive and
the admin routes remained unauthenticated.

- make setup: create .env from .env.example if missing; generate a
  32-byte hex token when no DOTTY_ADMIN_TOKEN line exists (idempotent;
  an existing line — even deliberately empty — is respected)
- docker-compose.yml.template + compose.all-in-one.yml: pass
  ${DOTTY_ADMIN_TOKEN:-} to xiaozhi-server
- dotty-pi/docker-compose.yml: add optional env_file (the container had
  no env mechanism at all, so adminFetch could never receive the token)
- bridge/ + dotty-behaviour/ compose: document the token among the
  deploy-dir .env secrets
- .env.example + SETUP.md §10: enable-everywhere-or-nowhere semantics
- CHANGELOG: record the #149-#152 chain (previously absent) + this fix

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…157)

Lift the pure blocked-content core (three tier regexes + the kid-safe
replacement) out of bridge/text.py into the shared
custom-providers/textUtils.py — already the canonical safety-constants
home both containers import — and add content_filter_match() plus a
sentence-buffered filter_tts_stream() wrapper. The bridge keeps its
metrics/safety-ring/logging wrapper on top, behaviour unchanged.

Both live providers now run TTS-bound output through the filter, gated
on kid_mode:

- streaming decision (per the issue's open question): buffer to
  _SENTENCE_BOUNDARY — the tier patterns are single-word regexes, so a
  blocked term can never straddle a sentence boundary; nothing reaches
  TTS before its sentence is checked, and spoken latency is unchanged
  (the TTS layer buffers to sentences anyway)
- on a hit the rest of the turn is replaced with the cheerful redirect;
  mid-turn replacements drop the redirect's leading emoji to preserve
  the one-emoji-per-reply firmware contract
- kid_mode off is a transparent passthrough (zero behaviour change)
- OpenAICompat's emoji-prefix enforcement runs before the filter, so
  the leading-glyph contract survives filtering

Tests: tests/test_voice_content_filter.py (16 cases — core matcher,
stream wrapper incl. chunk-straddling terms, bridge wrapper parity, and
both providers hit/clean/kid-off, mirroring the #146 pattern); plus a
guard that the tier regexes exist in only one file. docs/faq.md updated
to describe the filter honestly as a weak, bypassable backstop.

Still open on #157 before release sign-off: on-device red-team
verification (bench-pending).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BrettKinny BrettKinny force-pushed the feat/157-kid-mode-voice-content-filter branch from fd2fe41 to d083f59 Compare June 11, 2026 11:02
@BrettKinny BrettKinny merged commit 2ab8263 into main Jun 11, 2026
7 checks passed
@BrettKinny BrettKinny deleted the feat/157-kid-mode-voice-content-filter branch June 11, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant