---
id: TASK-TRS-002
title: Create TranscriptClient service
task_type: feature
parent_review: TASK-REV-9AD6
feature_id: FEAT-SKEL-003
status: in_review
priority: high
wave: 2
implementation_mode: task-work
complexity: 5
dependencies:
- TASK-TRS-001
tags:
- service
- transcript
- async
- feature
estimated_minutes: 90
autobuild_state:
  current_turn: 1
  max_turns: 25
  worktree_path: /Users/richardwoollcott/Projects/appmilla_github/youtube-transcript-mcp/.guardkit/worktrees/FEAT-6F80
  base_branch: main
  started_at: '2026-03-09T21:43:39.708407'
  last_updated: '2026-03-09T21:48:18.272733'
  turns:
  - turn: 1
    decision: approve
    feedback: null
    timestamp: '2026-03-09T21:43:39.708407'
    player_summary: Implementation via task-work delegation
    player_success: true
    coach_success: true
---

# Task: Create TranscriptClient Service

## Objective

Create `src/services/transcript_client.py` implementing the `TranscriptClient` class with async transcript fetching, intelligent language fallback, and structured error handling. This follows the established service layer pattern from FEAT-SKEL-002 (YouTubeClient).

## Acceptance Criteria

- [ ] `TranscriptClient` class created in `src/services/transcript_client.py`
- [ ] `get_transcript()` async method fetches transcript with language fallback
- [ ] `list_transcripts()` async method lists available transcripts
- [ ] Language fallback strategy: requested -> auto-generated -> English -> first available
- [ ] Custom exceptions: `TranscriptClientError`, `TranscriptsDisabledError`, `NoTranscriptFoundError`, `VideoUnavailableError`
- [ ] `TranscriptResult` and `TranscriptSegment` dataclasses for structured responses
- [ ] Async wrappers use `asyncio.to_thread()` for sync youtube-transcript-api calls
- [ ] CancelledError caught, logged, and re-raised (never swallowed)
- [ ] All logging to stderr via `logging.getLogger(__name__)`

## Implementation Details

### File: `src/services/transcript_client.py`

Follow the code in the feature spec at `docs/features/FEAT-SKEL-003-transcript-tool.md` lines 29-262.

Key patterns to apply:
1. **Async wrapper**: `asyncio.to_thread(self._sync_get_transcript, video_id, language)`
2. **CancelledError**: Catch, log, re-raise in both async methods
3. **Language fallback**: 4-step strategy in `_fetch_with_fallback()`
4. **Structured results**: `TranscriptResult` dataclass with segments and full_text
5. **Exception hierarchy**: Base `TranscriptClientError` with specific subclasses

### Critical MCP Patterns

| Pattern | Application |
|---------|------------|
| stderr logging | `logger = logging.getLogger(__name__)` |
| Async wrappers | `asyncio.to_thread()` for all sync API calls |
| CancelledError | Catch, log, re-raise in `get_transcript()` and `list_transcripts()` |
| Error boundaries | Custom exception hierarchy maps to structured error responses |

### youtube-transcript-api v1.2+ API

```python
api = YouTubeTranscriptApi()
transcript = api.fetch(video_id, languages=['en'])  # FetchedTranscript
transcript_list = api.list(video_id)                 # TranscriptList (iterable)
```

## Verification

```bash
python -c "from src.services.transcript_client import TranscriptClient, TranscriptResult; print('OK')"
ruff check src/services/transcript_client.py
mypy src/services/transcript_client.py
```

## Implementation Notes

- `NoTranscriptFoundError` includes `available_languages` list for caller context
- `_build_result()` calculates `total_segments`, `total_duration_seconds`, and `full_text` from transcript snippets
- `_sync_list_transcripts()` returns empty list (not exception) when transcripts unavailable
