本文へ移動
cccskills
無料GitHub で公開

tts-audio-system

The TTS/STT/audio-playback system behind the one `speak()` entry point. Use when working on read-aloud, the Listen panel, voice/speed settings, transcription, mic capture, SpeakerButton, iOS playing silence, audio API routes, or files in features/audio/, features/tts/, hooks/tts/, app/api/audio/.

インストール方法を見る

含まれるファイル(2)

  • SKILL.md16.6 KB
  • reference.md6.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

TTS / Audio System

🚨 Start here — the four things that are easy to get wrong

  1. speak() is the ONE entry point (features/audio/service/speak.ts). Never hand-roll a TTS call, never construct a player, never read a voice preference yourself. React surfaces use useSpeech() on top of it. To let someone HEAR a catalog model's voice (e.g. a speech-script speaker), call speak({ text, sample: { model, voice } }) — the catalog lane plays the vendor's own sample (ElevenLabs) or a line the server renders once per (model, voice) via POST /audio/voice-preview.
  2. Voice / speed / language come from the tiered listening config (features/audio/service/listeningConfig.ts) — system → org → user, user wins. Never read userPreferences.voice.* for playback; those fields survive only as the pre-fetch boot fallback. See the tiered-config section below.
  3. Audio output must be unlocked inside a user gesture or iOS plays silence. Call primeAudioOutput() (features/audio/unlock.ts) synchronously in any handler that will later start audio. See the iOS section below.
  4. One voice at a time, app-wide. playbackLock arbitrates; the playbackQueue + audioSessionRegistry own what is playing and its history. Producing audio without a session trips the runtime bypass guard.

Architecture Overview

Two independent TTS engines, one STT engine, and a voice assistant pipeline.

TTS Engines

EngineProviderTransportPrimary HookAPI Route
CartesiaCartesia APIWebSocket (PCM F32LE, 44.1kHz)useCartesiaSpeakerGET /api/cartesia
Groq/PlayAIGroq SDKREST → WAV blobuseTextToSpeechPOST /api/audio/text-to-speech

STT Engine

RouteProviderAuthMax Size
POST /api/audio/transcribeGroq WhisperSupabase cookie/Bearer4.5 MB
POST /api/audio/transcribe-urlGroq WhisperSupabase cookie/Bearer100 MB (URL)
POST /api/audio/log-errorSupabaseSupabase cookie/BearerN/A

Auth

All audio API routes use resolveUser from utils/supabase/resolveUser.ts — dual-mode: Supabase session cookie OR Bearer token. Both TTS routes (/api/cartesia, /api/audio/text-to-speech) require authentication.


Directory Map

features/tts/              ← Production TTS feature (Cartesia + Groq)
  hooks/
    useCartesiaSpeaker.ts   ← PRIMARY Cartesia hook (lazy, self-contained)
    useTextToSpeech.ts      ← Groq/PlayAI hook (REST, WAV blob)
  components/
    SpeakerButton.tsx       ← Single play/pause toggle (lazy-loads core)
    SpeakerButtonCore.tsx   ← Dynamically imported Cartesia logic
    SpeakerGroup.tsx        ← 3-button group shell (Play, Pause, Stop)
    SpeakerGroupCore.tsx    ← Dynamically imported 3-button core
    SpeakerCompactGroup.tsx ← 2-button group shell (Play/Pause, Stop)
    SpeakerCompactGroupCore.tsx
    AudioPlayerButton.tsx   ← Groq/PlayAI button (uses useTextToSpeech)
  types.ts                  ← Shared types (EnglishVoice, SpeakerVariant, etc.)

hooks/tts/                  ← Legacy/specialized hooks
  simple/
    useCartesiaControls.ts  ← Full controls (connect-on-mount, script state)
    useCartesiaWithPreferences.ts ← Redux voice prefs + connect-on-mount
    useSimpleCartesia.ts    ← Minimal Cartesia (connect-on-mount)
  useCartesia.ts            ← Legacy full-featured (direct API key, eager)
  usePlayer.ts              ← Raw PCM stream player (Web Audio API, 24kHz)
  usePlayerSafe.ts          ← Enhanced usePlayer with explicit init
  useVoiceChat.ts           ← Voice assistant pipeline (VAD + STT + AI + TTS)
  useVoiceChatCdn.ts        ← CDN variant of voice chat
  useVoiceChatWithAutoSleep.ts ← Auto-sleep extension

features/audio/             ← THE modern audio system (start here, not features/tts/)
  service/
    speak.ts                ← 🚨 THE app-wide "turn text into audio" entry point
    useSpeech.ts            ← React face of speak() (per-surface status)
    listeningConfig.ts      ← 🚨 tiered voice/speed/language (system→org→user)
    useListeningSettings.ts ← settings-pane face; update() writes MY tier
    engines.ts              ← AV engine registry (voice lists live here ONCE)
  unlock.ts                 ← 🚨 iOS/WebKit gesture unlock (see iOS section)
  activation.ts             ← one-way latch that mounts the lazy audio system
  playback/
    playbackQueue.ts        ← the single app-wide queue (one at a time)
    playbackLock.ts         ← app-wide single-output arbiter (start-always-wins)
    AudioPlaybackHost.tsx   ← queue → Redux mirror; installs unlock listeners
    adapters/cartesiaAdapter.ts  ← streaming PCM lane (SinkAwarePlayer)
    adapters/catalogAdapter.ts   ← server-catalog lane (HTMLAudioElement)
  session/
    audioSessionRegistry.ts ← ALL audio activity (in + out, live + history)
    usePlaybackSessionController.ts / useMediaElementPlaybackSession.ts
    bypassGuard.ts          ← screams when audio is produced with no session
  sinkAwarePlayer.ts        ← our WebPlayer fork; output-device routing
  hooks/ components/ services/ voice/   ← recording, mic UI, voice selection UI

features/window-panels/windows/listen/
  ListenSummaryWindow.tsx   ← the Listen panel (summarize-for-listening player)

providers/AudioSystemHost.tsx        ← the ONE ssr:false boundary (lazy mount)
providers/AudioOutputHostImpl.tsx    ← app-root streaming speaker owner

lib/cartesia/               ← Low-level Cartesia client & types
  client.ts                 ← LEGACY: direct NEXT_PUBLIC_CARTESIA_API_KEY
  cartesia.types.ts         ← Full Cartesia type definitions
  voices.ts / voices.json   ← Voice catalog

app/api/cartesia/route.ts   ← Token endpoint (authenticated)
app/api/audio/              ← TTS + STT API routes

utils/supabase/resolveUser.ts  ← Shared auth resolution
@ai-matrx/media/voices parseMarkdownToText ← MD → plain text

Hook Selection Guide

Adding TTS to a new component? Use useCartesiaSpeaker or a Speaker* component.

NeedSolution
Single play/pause button<SpeakerButton text={content} />
3-button group (Play/Pause/Stop)<SpeakerGroup text={content} />
2-button group (toggle + Stop)<SpeakerCompactGroup text={content} />
Programmatic TTS with Redux prefsuseCartesiaSpeaker({ processMarkdown: true })
Groq/PlayAI TTS (non-Cartesia)useTextToSpeech() or <AudioPlayerButton text={content} />
Full voice config UI (demo/playground)useCartesiaControls() or useSimpleCartesia()
Voice assistant with VADuseVoiceChat() or useVoiceChatWithAutoSleep()

Why multiple hooks exist

The legacy hooks (useCartesiaControls, useSimpleCartesia, useCartesia) connect eagerly on mount and manage their own voice/emotion/speed state. They exist for playground and demo pages where the user needs full control over Cartesia parameters.

useCartesiaSpeaker is the production hook: lazy (does nothing until speak() is called), reads voice preferences from Redux, and has proper error handling. Always prefer it for new features.


Speaker Component Contracts

All Speaker* components follow the same pattern:

  1. Thin shell renders static disabled buttons (zero JS loaded)
  2. First click triggers React.lazy() → dynamically imports Core
  3. Core initializes useCartesiaSpeaker and begins playback
  4. Shape never changes — buttons are always rendered, unavailable actions are disabled

Props (all variants)

interface SpeakerProps {
  text: string;           // Content to speak
  processMarkdown?: boolean; // Strip markdown (default: true)
  className?: string;     // Applied to outer container
  disabled?: boolean;     // Disable all buttons
}

// SpeakerButton also accepts:
variant?: 'glass' | 'transparent' | 'solid' | 'group';

Icon System

Speaker buttons use raw SVG icons from @ai-matrx/tap-target/buttons, NOT Lucide. Available: PlayTapButton, PauseTapButton, StopTapButton, Volume2TapButton. (Formerly components/icons/tap-buttons.tsx, deleted in the 2026-08-30 C9 package adoption.)

Styling

Uses TapTargetButton / TapTargetButtonGroup from the @ai-matrx/tap-target package (C9 adoption, 2026-08-30 — the old in-repo copy is gone). Glass styling via matrx-glass CSS classes (globally available in app/globals.css).


API Contracts

GET /api/cartesia

Auth: Required (cookie or Bearer) Response: { token: string } — short-lived Cartesia access token Errors: 401 (no auth), 500 (token generation failed)

POST /api/audio/text-to-speech

Auth: Required (cookie or Bearer) Body: { text: string; voice?: EnglishVoice; model?: string } Response: audio/wav binary (Cache-Control: 1 year) Limits: text max 10,000 chars, voice must be valid PlayAI voice Errors: 400 (validation), 401 (no auth), 429 (rate limit), 500

POST /api/audio/transcribe

Auth: Required Body: multipart/form-data with file (audio), optional language, prompt Response: { success, text, language, duration, segments, _meta: { attempts } } Limits: 4.5 MB file size, allowed types: flac/mp3/mp4/mpeg/mpga/m4a/ogg/wav/webm Retries: 3 with exponential backoff, logged to audio_transcription_errors

POST /api/audio/transcribe-url

Auth: Required Body: { url: string; language?; prompt? } — URL must be on the route's allowlist (Python backend tiers + AWS S3 hostnames; signed cld_files URLs qualify) Response: Same as /transcribe Limits: 100 MB via Groq URL parameter


Voice / speed / language — the tiered listening config (2026-08-28)

🚨 Cartesia voice, speed, and language NO LONGER live in userPreferences.voice for playback. They resolve through the tiered surface-config listening namespace (system → org → user, user wins) via features/audio/service/listeningConfig.ts:

  • React reads: selectListeningVoice/Speed/Language (+ selectListeningVoiceId(state, purpose)).
  • Imperative reads (speak(), playback adapters): getListeningSettings() — resolves at START time so replays honor current settings.
  • Writes: useListeningSettings().update(patch) — ONE row at the user's tier (never a whole-preferences write).
  • System default: platform-global row, admin-edited at /administration/ui/surfaces/matrx-user/assistant-message (Config namespaces). Org rows override; user rows win.

userPreferences.voice.{voice,speed,language} remain ONLY as the pre-fetch boot fallback; emotion/wakeWord/microphone/speaker still live there.

Redux Voice Preferences (legacy fields — see above for voice/speed/language)

// state.userPreferences.voice (Cartesia)
interface VoicePreferences {
  voice: string;    // LEGACY for playback — boot fallback only
  language: string; // LEGACY for playback — boot fallback only
  speed: number;    // LEGACY for playback — boot fallback only
  emotion: string;
  microphone: boolean;
  speaker: boolean;
  wakeWord: string;
}

// state.userPreferences.textToSpeech (Groq/PlayAI)
interface TextToSpeechPreferences {
  preferredVoice: GroqTtsVoice; // e.g. "Cheyenne-PlayAI"
  autoPlay: boolean;
  processMarkdown: boolean;
}

🚨 iOS / WebKit — the output-unlock law (2026-08-30)

Every browser on iPhone is WebKit — Chrome and Firefox included — so these rules are not "a Safari edge case", they are the whole mobile platform.

Two WebKit behaviors made all mobile audio silent, with no error:

  1. An AudioContext created (or resumed) outside a user gesture starts suspended and stays silent. Our TTS starts audio from websocket callbacks seconds after the tap, so per-utterance contexts were always suspended.
  2. Web Audio is muted by the ringer/silent switch unless the page declares navigator.audioSession.type = "playback" (iOS 16.4+).

features/audio/unlock.ts is the fix and the only place this logic lives:

  • installAudioUnlockListeners() — capture-phase pointerdown/keydown/ touchend listeners; mounted ONCE by AudioPlaybackHost. Unlocks on the user's first interaction anywhere, so the page is usually already unlocked before any audio is requested.
  • primeAudioOutput() — call this synchronously in every handler that will later start audio. Idempotent and cheap. Already called by enqueuePlayback / resumePlayback / playPlaybackItem, both Listen actions, and the Listen panel transport.
  • getUnlockedAudioContext() / getPrimedMediaElement() — the shared, gesture-unlocked handles. SinkAwarePlayer schedules into the shared context when it exists (per-utterance GainNode; never closes it); catalogAdapter plays through the shared element.

Rules when you touch audio:

  • Adding a new "start audio" button? Call primeAudioOutput() in the handler.
  • Never mint a bare new AudioContext() for playback — go through the queue.
  • Never close the shared context (stop() in shared mode tears down the utterance only).
  • A context that will not reach running throws a user-facing error ("The browser blocked audio output — tap the play button to start sound"). Keep it that way: silence with no error is the bug this replaced.

The Listen panel — summarize-for-listening

features/window-panels/windows/listen/ListenSummaryWindow.tsx (overlay listenSummaryWindow). Two entry points, both in one "Listen" context-menu submenu and in the assistant action bar's ⋯ menu:

  • Summarize without playing — summary streams in as text; user presses Play.
  • Summarize & listen — stream-to-stream: the summary is spoken as it is written, via the app-root speaker (voicePlaybackBus request with includeActive: true).

The summarizing agent is resolved from the spoken_summary surface role, falling back to the platform home surface (LISTENING_HOME_SURFACE) whose ambient.spoken_summary mandate holds the default agent — so it works on every surface for every user with no personal binding. Never put an agent UUID in code here. Menu wiring: @ai-matrx/context-menu (listen submenu role) + messageActionRegistry.listeningItems.


Known Deferred Issues

These are documented design decisions, not bugs:

  1. Five Cartesia hook variants — Consolidation into one hook requires updating ~15 consumer files. Deferred as separate effort. Use useCartesiaSpeaker for all new code.
  2. lib/cartesia/client.ts leaks API key — Uses NEXT_PUBLIC_CARTESIA_API_KEY. Legacy hook useCartesia depends on it. Will be removed when legacy hook is consolidated.
  3. No connection health monitoring — No heartbeat/ping. Silent WebSocket drops discovered only on next speak().
  4. No playback progress for Cartesia — the catalog (element) lane tracks duration/currentTime; the streaming PCM lane does not.
  5. Token expiry — Cartesia token fetched once, no refresh. If it expires mid-session, connectCartesiaTts refreshes and retries once automatically.

Two entries were REMOVED on 2026-09-08 because they became false — do not reinstate them from an old copy of this file: "no global TTS instance management" (the playbackQueue + playbackLock + audioSessionRegistry trio has been the single owner since the unified-audio work) and "no abort for in-flight TTS" (skipPlayback / clearPlayback / a session's stop control all abort; playbackLock preempts across paths).


Adding a New Speaker Variant

  1. Create SpeakerNewVariant.tsx (thin shell) in features/tts/components/
  2. Create SpeakerNewVariantCore.tsx (default export, uses useCartesiaSpeaker)
  3. Use React.lazy() in shell to import core
  4. Use TapTargetButton* from @ai-matrx/tap-target (icons: @ai-matrx/tap-target/buttons)
  5. Export from features/tts/components/index.ts
  6. Never hide buttons — disable unavailable actions
  7. Never change component shape during state transitions

Modifying API Routes

All audio API routes use resolveUser from utils/supabase/resolveUser.ts. When adding new routes:

  1. Import and call resolveUser(request) first
  2. Return 401 if !user
  3. Use structured error responses with code field
  4. For Groq routes: implement retry with exponential backoff
  5. Log errors to audio_transcription_errors via logTranscriptionError()

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Admin debug system wiring for the floating AdminIndicator's live debug data and Copy Full Context. Use when adding debug visibility to a route or feature, wiring useDebugContext or the debug panel, capturing console errors, or enabling the copy-context workflow for a page.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

Compact two-icon Copy / Copy-for-AI controls (components/agent-copy). Use when adding copy buttons to a row, card, list, or record; merging duplicate Copy/JSON/AI controls; continuing the copy rollout; or writing a Copy-for-AI payload. NOT for markdown content actions (use rich-document-actions).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

Disclosing a surface's existing fixed AI jobs in the shell's top Agents menu. Use when a page, panel, overlay, or window already runs a mandate behind a button, assist, automatic action, or mode; when the agent-disclosure guard names a file; or during a surface check.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

The watch-fix-rerun method for making an existing platform agent or automated process efficient and correct: baseline its ledger, run one unit yourself, fix the class behind every wasted call, rerun, record. Use when asked to improve, optimize, watch, or 'make efficient' an agent, a sandbox session, a sync job, or any recurring automated process.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

Redux state for agent execution and firing agent shortcuts. Use when editing features/agents/redux/, building agent UI, creating a conversation, touching assembleRequest or NDJSON stream state, wiring agent-state selectors, adding a per-conversation capability, or triggering a shortcut from a button, menu, or code (useShortcutTrigger, launchAgentExecution).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

The provision for a call site: the exhaustive menu of values that place in the code can realistically produce. Use before creating or fixing a mandate or agent that reasons about more than its own input, when deciding whether an agent can answer at all with what it is sent, or when its output 'looks right' but nobody checked it could be. NOT for building the agent (use create-agent).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月13日 更新

armanisadeghi のスキルをすべて見る

このスキルの問題を報告する