---
phase: 14-tts-dispatch--feature-gates
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/LanguageDetector.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/FeatureGates.swift
  - plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
autonomous: true
requirements: [TTS-12, TTS-13]

must_haves:
  truths:
    - "CJK detection returns Chinese voice when text has >20% CJK characters"
    - "CJK detection returns English voice when text has <=20% CJK characters"
    - "Feature gates read env vars matching legacy names (TTS_ENABLED, SUMMARIZER_TG_ENABLED, etc.)"
    - "Feature gates default to enabled when env vars are not set"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/LanguageDetector.swift"
      provides: "CJK ratio calculation and voice/speakerId selection"
      exports: ["LanguageDetector", "LanguageResult"]
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/FeatureGates.swift"
      provides: "Per-outlet feature gate checks from environment"
      exports: ["FeatureGates"]
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift"
      provides: "Chinese speaker ID constant and CJK threshold"
      contains: "chineseSpeakerId"
  key_links:
    - from: "LanguageDetector.swift"
      to: "Config.swift"
      via: "Config.defaultSpeakerId, Config.chineseSpeakerId"
      pattern: "Config\\..*SpeakerId"
    - from: "FeatureGates.swift"
      to: "ProcessInfo.processInfo.environment"
      via: "env var lookup"
      pattern: "ProcessInfo\\.processInfo\\.environment"
---

<objective>
Create CJK language detection and per-outlet feature gate utilities for TTS dispatch.

Purpose: These utilities are consumed by Plan 02 to wire language-aware TTS and outlet gating into the notification pipeline.
Output: LanguageDetector.swift, FeatureGates.swift, updated Config.swift
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift

<interfaces>
<!-- Legacy TypeScript CJK detection to port (from kokoro-client.ts) -->
```typescript
function detectLanguage(text: string): { lang: string; voice: string; sayVoice: string } {
  let cjkCount = 0;
  for (const char of text) {
    const code = char.codePointAt(0)!;
    if (
      (code >= 0x4e00 && code <= 0x9fff) ||
      (code >= 0x3400 && code <= 0x4dbf) ||
      (code >= 0x20000 && code <= 0x2a6df)
    ) {
      cjkCount++;
    }
  }
  const ratio = text.length > 0 ? (cjkCount / text.length) * 100 : 0;
  if (ratio >= 20) {
    return { lang: "cmn", voice: ZH_VOICE, sayVoice: SAY_ZH_VOICE };
  }
  return { lang: "en-us", voice: EN_VOICE, sayVoice: SAY_EN_VOICE };
}
```

<!-- Legacy TypeScript feature gates to port (from notification-watcher.ts) -->

```typescript
const summarizerTgEnabled = Bun.env.SUMMARIZER_TG_ENABLED !== "false";
const ttsEnabled = Bun.env.TTS_ENABLED !== "false";
const summaryTtsEnabled = ttsEnabled && Bun.env.SUMMARY_TTS_ENABLED !== "false";
const tbrTgEnabled = Bun.env.TBR_TG_ENABLED !== "false";
const tbrTtsEnabled = ttsEnabled && Bun.env.TBR_TTS_ENABLED !== "false";
```

<!-- Kokoro multi-lang v1.0 speaker IDs (from sherpa-onnx source) -->
<!-- af_heart = 3 (English, already in Config.defaultSpeakerId) -->
<!-- zf_xiaobei = 45 (Chinese) -->

<!-- Existing Config.swift exports used by this plan -->

```swift
enum Config {
    static let defaultSpeakerId: Int32 = 3  // af_heart
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: LanguageDetector + FeatureGates + Config constants</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/LanguageDetector.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/FeatureGates.swift,
    plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/claude-tts-companion/Config.swift,
    ~/.claude/automation/claude-telegram-sync/src/tts/kokoro-client.ts,
    ~/.claude/automation/claude-telegram-sync/src/claude-sync/notification-watcher.ts
  </read_first>
  <action>
    **1. Config.swift additions** (append after existing speaker ID constants):
    - Add `static let chineseSpeakerId: Int32 = 45` (zf_xiaobei in Kokoro multi-lang v1.0, confirmed from sherpa-onnx source `scripts/kokoro/v1.0/generate_voices_bin.py`)
    - Add `static let cjkDetectionThreshold: Double = 20.0` (percentage, matches legacy `TTS_CJK_DETECTION_RATIO` default of 20)

    **2. LanguageDetector.swift** (new file):
    - Create `struct LanguageResult` with fields: `lang: String` ("en-us" or "cmn"), `speakerId: Int32`
    - Create `enum LanguageDetector` with static function `detect(text: String) -> LanguageResult`
    - Port CJK detection from legacy TypeScript `detectLanguage()`:
      - Iterate over `text.unicodeScalars` (not `text.characters` — Unicode scalar is the correct abstraction for code point ranges)
      - Count CJK code points matching these ranges (exact port from legacy):
        - U+4E00..U+9FFF (CJK Unified Ideographs)
        - U+3400..U+4DBF (CJK Extension A)
        - U+20000..U+2A6DF (CJK Extension B)
      - Calculate ratio: `(cjkCount / totalScalarCount) * 100`
      - If ratio >= `Config.cjkDetectionThreshold`: return `LanguageResult(lang: "cmn", speakerId: Config.chineseSpeakerId)`
      - Otherwise: return `LanguageResult(lang: "en-us", speakerId: Config.defaultSpeakerId)`
    - Handle empty string: return English defaults (same as legacy behavior)

    **3. FeatureGates.swift** (new file):
    - Create `enum FeatureGates` with static computed properties matching legacy env var names exactly:
      - `static var summarizerTgEnabled: Bool` — reads `SUMMARIZER_TG_ENABLED`, defaults to `true` (enabled unless explicitly "false")
      - `static var ttsEnabled: Bool` — reads `TTS_ENABLED`, defaults to `true`
      - `static var summaryTtsEnabled: Bool` — `ttsEnabled && env("SUMMARY_TTS_ENABLED") != "false"`
      - `static var tbrTgEnabled: Bool` — reads `TBR_TG_ENABLED`, defaults to `true`
      - `static var tbrTtsEnabled: Bool` — `ttsEnabled && env("TBR_TTS_ENABLED") != "false"`
    - Private helper: `static func env(_ key: String) -> String?` wrapping `ProcessInfo.processInfo.environment[key]`
    - The "defaults to true" pattern: gate is enabled unless the env var is exactly the string "false" (matching legacy `!== "false"` semantics)
    - Add `static var allOutletsDisabled: Bool` — true when all five gates are false (matches legacy early-exit check)

  </action>
  <acceptance_criteria>
    - LanguageDetector.detect("Hello world") returns lang="en-us", speakerId=3
    - LanguageDetector.detect("你好世界测试文本这是中文内容") returns lang="cmn", speakerId=45
    - FeatureGates properties return true by default (no env vars set)
    - Config.chineseSpeakerId == 45
    - CJK regex covers all 3 Unicode ranges from legacy
  </acceptance_criteria>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <done>LanguageDetector detects CJK with >20% threshold matching legacy regex ranges. FeatureGates reads all 5 legacy env var names with correct default-enabled semantics. Config has chineseSpeakerId=45.</done>
</task>

</tasks>

<verification>
- `swift build` compiles without errors
- LanguageDetector covers exact same Unicode ranges as legacy TypeScript
- FeatureGates env var names match legacy exactly: SUMMARIZER_TG_ENABLED, TTS_ENABLED, SUMMARY_TTS_ENABLED, TBR_TG_ENABLED, TBR_TTS_ENABLED
</verification>

<success_criteria>
All three files exist with correct types exported. Build succeeds. CJK detection and feature gates match legacy behavior.
</success_criteria>

<output>
After completion, create `.planning/phases/14-tts-dispatch--feature-gates/14-01-SUMMARY.md`
</output>
