---
phase: 17-tts-streaming-subtitle-chunking
plan: 02
type: execute
wave: 2
depends_on: [17-01]
files_modified:
  - plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
autonomous: false
requirements: [STREAM-01, STREAM-02, STREAM-03]

must_haves:
  truths:
    - "dispatchTTS() chunks synthesized text into pages via SubtitleChunker before displaying"
    - "Audio starts playing immediately after synthesis (single WAV, no splitting)"
    - "Subtitle pages flip as karaoke advances through each page"
    - "First audio plays within 5 seconds of TTS dispatch (existing RTF < 1.0 ensures this)"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift"
      provides: "Paged TTS dispatch flow"
      contains: "SubtitleChunker.chunkIntoPages"
  key_links:
    - from: "TelegramBot.swift"
      to: "SubtitleChunker.swift"
      via: "SubtitleChunker.chunkIntoPages() called in dispatchTTS()"
      pattern: "SubtitleChunker\\.chunkIntoPages"
    - from: "TelegramBot.swift"
      to: "SubtitlePanel.swift"
      via: "subtitlePanel.showPages() called instead of showUtterance()"
      pattern: "showPages"
---

<objective>
Wire SubtitleChunker into TelegramBot.dispatchTTS() so TTS playback uses paged subtitle display, then verify visually.

Purpose: Connect the chunking and paged display from Plan 01 into the actual TTS dispatch flow. This completes STREAM-01 (audio within 5s), STREAM-02 (one sentence/page at a time), and STREAM-03 (karaoke per page).
Output: Modified TelegramBot.swift with paged dispatchTTS(), visual verification checkpoint.
</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
@.planning/phases/17-tts-streaming-subtitle-chunking/17-CONTEXT.md
@.planning/phases/17-tts-streaming-subtitle-chunking/17-01-SUMMARY.md

@plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitleChunker.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift
@plugins/claude-tts-companion/Sources/claude-tts-companion/TTSEngine.swift

<interfaces>
<!-- Key contracts from Plan 01 output that this plan consumes -->

From SubtitleChunker.swift (created in Plan 01):

```swift
struct SubtitlePage {
    let words: [String]
    let startWordIndex: Int
    var wordCount: Int { words.count }
}

@MainActor
enum SubtitleChunker {
    static func chunkIntoPages(text: String) -> [SubtitlePage]
}
```

From SubtitlePanel.swift (refactored in Plan 01):

```swift
@MainActor
final class SubtitlePanel: NSPanel {
    func showPages(_ pages: [SubtitlePage], wordTimings: [TimeInterval])
    func showUtterance(_ text: String, wordTimings: [TimeInterval])  // backward compat wrapper
}
```

From TTSEngine.swift:

```swift
struct TTSResult {
    let wavPath: String
    let text: String
    let wordTimings: [TimeInterval]
    let audioDuration: TimeInterval
}
```

Current dispatchTTS() in TelegramBot.swift (lines 185-218):

```swift
private func dispatchTTS(text: String, greeting: String?) async {
    let fullText: String
    if let greeting = greeting, !greeting.isEmpty {
        fullText = "\(greeting) \(text)"
    } else {
        fullText = text
    }
    let langResult = LanguageDetector.detect(text: fullText)
    ttsEngine.synthesizeWithTimestamps(text: fullText, speakerId: langResult.speakerId) { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .success(let ttsResult):
            DispatchQueue.main.async {
                self.subtitlePanel.showUtterance(ttsResult.text, wordTimings: ttsResult.wordTimings)
            }
            self.ttsEngine.play(wavPath: ttsResult.wavPath) { ... }
        case .failure(let error): ...
        }
    }
}
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Update dispatchTTS() to use SubtitleChunker and showPages()</name>
  <files>
    plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift
    plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitleChunker.swift
    plugins/claude-tts-companion/Sources/claude-tts-companion/SubtitlePanel.swift
  </read_first>
  <action>
Modify the `dispatchTTS(text:greeting:)` method in TelegramBot.swift. Replace the success handler's subtitle display code.

Current code in the success case:

```swift
DispatchQueue.main.async {
    self.subtitlePanel.showUtterance(ttsResult.text, wordTimings: ttsResult.wordTimings)
}
```

Replace with:

```swift
DispatchQueue.main.async {
    let pages = SubtitleChunker.chunkIntoPages(text: ttsResult.text)
    self.subtitlePanel.showPages(pages, wordTimings: ttsResult.wordTimings)
}
```

This is a 2-line change. Everything else in dispatchTTS() stays identical:

- fullText construction with greeting prefix: unchanged
- LanguageDetector.detect(): unchanged
- ttsEngine.synthesizeWithTimestamps(): unchanged (single WAV, full paragraph)
- ttsEngine.play(wavPath:): unchanged (plays the single WAV)
- The audio plays continuously; only the subtitle display is now paginated

The flow becomes:

1. Synthesize full text as single WAV (existing, unchanged) -- meets STREAM-01 (RTF < 1.0 means audio ready in ~2s)
2. Chunk synthesized text into 2-line pages via SubtitleChunker (new)
3. Display pages with showPages() which schedules page flips + per-word karaoke (new) -- meets STREAM-02, STREAM-03
4. Play single WAV concurrently (existing, unchanged)
   </action>

  <verify>
    <automated>cd plugins/claude-tts-companion && swift build -c debug 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
    - TelegramBot.swift contains `SubtitleChunker.chunkIntoPages(text: ttsResult.text)`
    - TelegramBot.swift contains `self.subtitlePanel.showPages(pages, wordTimings: ttsResult.wordTimings)`
    - TelegramBot.swift does NOT contain `self.subtitlePanel.showUtterance(ttsResult.text` in dispatchTTS (replaced)
    - TelegramBot.swift still contains `ttsEngine.play(wavPath: ttsResult.wavPath)` (audio playback unchanged)
    - TelegramBot.swift still contains `LanguageDetector.detect(text: fullText)` (language detection unchanged)
    - `swift build -c debug` completes with exit code 0
  </acceptance_criteria>
  <done>dispatchTTS() uses SubtitleChunker to chunk text into pages and SubtitlePanel.showPages() for paged karaoke display. Audio plays as single continuous WAV. Project compiles.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 2: Visual verification of paged subtitle display with karaoke</name>
  <files>plugins/claude-tts-companion/Sources/claude-tts-companion/TelegramBot.swift</files>
  <action>
Build and run the binary, then trigger a TTS dispatch to visually verify paged karaoke:
1. Build: `cd plugins/claude-tts-companion && swift build -c debug && .build/debug/claude-tts-companion`
2. Trigger TTS via test notification or HTTP API
3. Observe subtitle panel at bottom of screen:
   - First page: all words white (future), karaoke highlights gold word-by-word
   - Page flip: when last word of page highlighted, next page appears instantly (no fade)
   - New page: all words white, karaoke continues from where it left off
   - Last page: lingers 2 seconds then hides
4. Verify audio plays continuously without gaps during page transitions
5. Verify short text (single page) matches old showUtterance() behavior exactly
  </action>
  <verify>
    <automated>cd plugins/claude-tts-companion && swift build -c debug 2>&1 | tail -3</automated>
  </verify>
  <acceptance_criteria>
    - Binary builds and runs without crash
    - Subtitle pages flip as karaoke advances (visual confirmation by user)
    - Audio plays continuously without gaps during page transitions (auditory confirmation by user)
  </acceptance_criteria>
  <done>Human confirms paged karaoke subtitles display correctly with page flips synchronized to word highlighting and continuous audio playback.</done>
</task>

</tasks>

<verification>
- `swift build -c debug` succeeds (zero errors)
- TelegramBot.dispatchTTS() uses SubtitleChunker.chunkIntoPages() + SubtitlePanel.showPages()
- Visual: subtitle panel shows 2-line pages that flip as karaoke advances
- Visual: audio plays continuously without interruption during page transitions
- Visual: short text (single page) still works correctly
</verification>

<success_criteria>

- Project compiles with `swift build -c debug`
- TTS dispatch flow chunks text into pages and uses paged subtitle display
- Human verifies: pages flip correctly as karaoke advances, audio is continuous
- STREAM-01 met: first audio within 5 seconds of dispatch (synthesis RTF < 1.0)
- STREAM-02 met: subtitle shows one page at a time, advancing as speech progresses
- STREAM-03 met: karaoke gold highlighting works within each page
  </success_criteria>

<output>
After completion, create `.planning/phases/17-tts-streaming-subtitle-chunking/17-02-SUMMARY.md`
</output>
