---
phase: 19-ttsengine-decomposition-actor-migration
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/claude-tts-companion/Sources/CompanionCore/WordTimingAligner.swift
  - plugins/claude-tts-companion/Sources/CompanionCore/PronunciationProcessor.swift
  - plugins/claude-tts-companion/Sources/CompanionCore/SentenceSplitter.swift
  - plugins/claude-tts-companion/Sources/CompanionCore/PlaybackDelegate.swift
  - plugins/claude-tts-companion/Sources/CompanionCore/TTSError.swift
  - plugins/claude-tts-companion/Sources/CompanionCore/TTSEngine.swift
autonomous: true
requirements: [ARCH-03, ARCH-04]

must_haves:
  truths:
    - "WordTimingAligner is a standalone Sendable struct with all 6 static methods from TTSEngine lines 702-925"
    - "PronunciationProcessor is a standalone Sendable struct with preprocessText() and all pronunciation overrides"
    - "SentenceSplitter is a standalone Sendable struct with splitIntoSentences()"
    - "PlaybackDelegate is in its own file as @MainActor (not @unchecked Sendable)"
    - "TTSError is in its own file"
    - "TTSEngine.swift delegates to extracted types -- no duplicate logic"
    - "swift build succeeds with zero errors"
  artifacts:
    - path: "plugins/claude-tts-companion/Sources/CompanionCore/WordTimingAligner.swift"
      provides: "Pure struct with NativeTimings, ResolvedTimings, and all word timing methods"
      contains: "struct WordTimingAligner"
    - path: "plugins/claude-tts-companion/Sources/CompanionCore/PronunciationProcessor.swift"
      provides: "Pure struct with pronunciation overrides and preprocessText()"
      contains: "struct PronunciationProcessor"
    - path: "plugins/claude-tts-companion/Sources/CompanionCore/SentenceSplitter.swift"
      provides: "Pure struct with splitIntoSentences()"
      contains: "struct SentenceSplitter"
    - path: "plugins/claude-tts-companion/Sources/CompanionCore/PlaybackDelegate.swift"
      provides: "AVAudioPlayerDelegate as @MainActor"
      contains: "class PlaybackDelegate"
    - path: "plugins/claude-tts-companion/Sources/CompanionCore/TTSError.swift"
      provides: "TTSError enum"
      contains: "enum TTSError"
  key_links:
    - from: "TTSEngine.swift"
      to: "WordTimingAligner.swift"
      via: "WordTimingAligner.resolveWordTimings() calls"
      pattern: "WordTimingAligner\\.resolveWordTimings"
    - from: "TTSEngine.swift"
      to: "PronunciationProcessor.swift"
      via: "PronunciationProcessor.preprocessText() calls"
      pattern: "PronunciationProcessor\\.preprocessText"
    - from: "TTSEngine.swift"
      to: "SentenceSplitter.swift"
      via: "SentenceSplitter.splitIntoSentences() calls"
      pattern: "SentenceSplitter\\.splitIntoSentences"
---

<objective>
Extract pure structs and supporting types from TTSEngine.swift into their own files.

Purpose: Reduce TTSEngine from 1138 lines to ~500 lines by extracting WordTimingAligner (per D-02), PronunciationProcessor (per D-03), SentenceSplitter (per D-05), PlaybackDelegate (per D-06), and TTSError into separate files. These are zero-risk moves -- pure static methods with no mutable state. This sets up Plan 02 (actor migration) by reducing TTSEngine's surface area.

Output: 5 new files + slimmed TTSEngine.swift, all compiling successfully.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/19-ttsengine-decomposition-actor-migration/19-CONTEXT.md
@.planning/phases/19-ttsengine-decomposition-actor-migration/19-RESEARCH.md

@plugins/claude-tts-companion/Sources/CompanionCore/TTSEngine.swift
</context>

<tasks>

<task type="auto">
  <name>Task 1: Extract WordTimingAligner, PronunciationProcessor, SentenceSplitter, PlaybackDelegate, TTSError into separate files</name>
  <files>
    plugins/claude-tts-companion/Sources/CompanionCore/WordTimingAligner.swift
    plugins/claude-tts-companion/Sources/CompanionCore/PronunciationProcessor.swift
    plugins/claude-tts-companion/Sources/CompanionCore/SentenceSplitter.swift
    plugins/claude-tts-companion/Sources/CompanionCore/PlaybackDelegate.swift
    plugins/claude-tts-companion/Sources/CompanionCore/TTSError.swift
  </files>
  <read_first>
    plugins/claude-tts-companion/Sources/CompanionCore/TTSEngine.swift
  </read_first>
  <action>
Create 5 new Swift files by extracting code from TTSEngine.swift:

**1. WordTimingAligner.swift** (per D-02 -- pure Sendable struct, all static methods):

- Extract `struct NativeTimings` (lines 704-713) as a nested type inside WordTimingAligner
- Extract `struct ResolvedTimings` (lines 893-896) as a nested type inside WordTimingAligner
- Extract `static func extractTimingsFromTokens(_:)` (lines 723-747)
- Extract `static func alignOnsetsToWords(native:subtitleWords:audioDuration:)` (lines 760-841)
- Extract `private static func stripPunctuation(_:)` (lines 850-865) -- make it `static func` (internal, not private)
- Extract `static func extractWordTimings(text:audioDuration:)` (lines 875-890)
- Extract `static func resolveWordTimings(tokenArray:text:audioDuration:logger:)` (lines 903-925)
- Declare as `public struct WordTimingAligner: Sendable { ... }` with all methods `static`
- Import Foundation, Logging, KokoroSwift (for MToken type)

**2. PronunciationProcessor.swift** (per D-03 -- pure Sendable struct):

- Extract `private static let pronunciationOverrides` (lines 97-102) -- make it `static let`
- Extract `private static let compiledOverrides` (lines 105-113) -- make it `static let`
- Extract `static func preprocessText(_:)` (lines 120-130) -- keep `public`
- Declare as `public struct PronunciationProcessor: Sendable { ... }` with all properties/methods `static`
- Import Foundation

**3. SentenceSplitter.swift** (per D-05 -- pure Sendable struct):

- Extract `static func splitIntoSentences(_:)` (lines 613-667)
- Declare as `public struct SentenceSplitter: Sendable { ... }` with the method `static`
- Import Foundation

**4. PlaybackDelegate.swift** (per D-06):

- Extract `PlaybackDelegate` class (lines 1097-1119) from bottom of TTSEngine.swift
- Change from `@unchecked Sendable` to `@MainActor` (per Research pitfall 5 -- AVAudioPlayerDelegate callbacks fire on main thread)
- Mark completion closure as `@Sendable` if needed for compilation
- Import AVFoundation, Foundation, Logging

**5. TTSError.swift:**

- Extract `enum TTSError` (lines 1123-1138) from bottom of TTSEngine.swift
- Add `case circuitBreakerOpen` variant (will be needed in Plan 02)
- Keep `public enum TTSError: Error, CustomStringConvertible`
- Import Foundation

**After creating all 5 files, update TTSEngine.swift:**

- Remove all extracted code (NativeTimings, ResolvedTimings, extractTimingsFromTokens, alignOnsetsToWords, stripPunctuation, extractWordTimings, resolveWordTimings, pronunciationOverrides, compiledOverrides, preprocessText, splitIntoSentences, PlaybackDelegate, TTSError)
- Update internal references:
  - `TTSEngine.preprocessText(text)` becomes `PronunciationProcessor.preprocessText(text)`
  - `TTSEngine.resolveWordTimings(...)` becomes `WordTimingAligner.resolveWordTimings(...)`
  - `TTSEngine.splitIntoSentences(text)` becomes `SentenceSplitter.splitIntoSentences(text)`
  - `TTSEngine.extractWordTimings(...)` becomes `WordTimingAligner.extractWordTimings(...)`
  - `TTSEngine.trailingSilenceSamples` stays in TTSEngine (it's a synthesis concern)
  - References to `PlaybackDelegate` and `TTSError` continue to work because they're now top-level types in the same module
- The `ChunkResult` struct stays in TTSEngine.swift (it's the streaming synthesis API type per discretion)
  </action>
  <verify>
  <automated>cd plugins/claude-tts-companion && swift build 2>&1 | tail -5</automated>
  </verify>
  <acceptance_criteria>
  - `swift build` succeeds with zero errors
  - `grep -c "struct WordTimingAligner" plugins/claude-tts-companion/Sources/CompanionCore/WordTimingAligner.swift` returns 1
  - `grep -c "struct PronunciationProcessor" plugins/claude-tts-companion/Sources/CompanionCore/PronunciationProcessor.swift` returns 1
  - `grep -c "struct SentenceSplitter" plugins/claude-tts-companion/Sources/CompanionCore/SentenceSplitter.swift` returns 1
  - `grep -c "class PlaybackDelegate" plugins/claude-tts-companion/Sources/CompanionCore/PlaybackDelegate.swift` returns 1
  - `grep -c "enum TTSError" plugins/claude-tts-companion/Sources/CompanionCore/TTSError.swift` returns 1
  - `grep -c "@unchecked Sendable" plugins/claude-tts-companion/Sources/CompanionCore/PlaybackDelegate.swift` returns 0
  - `grep "PronunciationProcessor.preprocessText" plugins/claude-tts-companion/Sources/CompanionCore/TTSEngine.swift` shows updated references
  - `grep "WordTimingAligner.resolveWordTimings" plugins/claude-tts-companion/Sources/CompanionCore/TTSEngine.swift` shows updated references
  - `grep "SentenceSplitter.splitIntoSentences" plugins/claude-tts-companion/Sources/CompanionCore/TTSEngine.swift` shows updated references
  - TTSEngine.swift line count is under 650 lines (was 1138)
    </acceptance_criteria>
    <done>
    5 new files created with extracted types. TTSEngine.swift slimmed to ~500-600 lines. All internal references updated. `swift build` passes. No behavior changes -- same public API surface.
    </done>
    </task>

<task type="auto">
  <name>Task 2: Run existing tests to verify no regressions from extraction</name>
  <files>
    (no files modified -- verification only)
  </files>
  <read_first>
    plugins/claude-tts-companion/Tests/CompanionCoreTests/
  </read_first>
  <action>
Run the full test suite to verify the extraction did not break any existing tests.

1. Run `cd plugins/claude-tts-companion && swift test`
2. If any tests fail, investigate and fix -- the extraction should be purely mechanical
3. Verify test count matches pre-extraction (check test output for total count)
   </action>

  <verify>
    <automated>cd plugins/claude-tts-companion && swift test 2>&1 | tail -10</automated>
  </verify>
  <acceptance_criteria>
    - `swift test` shows "Test Suite ... passed" with zero failures
    - All existing tests (LanguageDetector, SubtitleChunker, TelegramFormatter, TranscriptParser, CircuitBreaker) pass
  </acceptance_criteria>
  <done>
    All existing unit tests pass after extraction. Zero regressions confirmed.
  </done>
</task>

</tasks>

<verification>
1. `cd plugins/claude-tts-companion && swift build` -- zero errors
2. `cd plugins/claude-tts-companion && swift test` -- all tests pass
3. `wc -l plugins/claude-tts-companion/Sources/CompanionCore/TTSEngine.swift` -- under 650 lines
4. `grep -r "@unchecked Sendable" plugins/claude-tts-companion/Sources/CompanionCore/PlaybackDelegate.swift` -- no matches
5. No `TTSEngine.preprocessText`, `TTSEngine.resolveWordTimings`, or `TTSEngine.splitIntoSentences` references remain in TTSEngine.swift (replaced with extracted type names)
</verification>

<success_criteria>

- 5 new source files exist with correct Sendable structs/types
- TTSEngine.swift reduced from 1138 to ~500-600 lines
- All internal cross-references use extracted type names
- `swift build` and `swift test` both pass
- PlaybackDelegate uses @MainActor instead of @unchecked Sendable
  </success_criteria>

<output>
After completion, create `.planning/phases/19-ttsengine-decomposition-actor-migration/19-01-SUMMARY.md`
</output>
