Metadata-Version: 2.4
Name: cjm-transcript-review
Version: 0.0.22
Summary: FastHTML Reader View component for verifying transcript segmentation with audio playback and context graph commit.
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
License: Apache-2.0
Project-URL: Repository, https://github.com/cj-mills/cjm-transcript-review
Project-URL: Documentation, https://cj-mills.github.io/cjm-transcript-review
Keywords: nbdev,jupyter,notebook,python
Classifier: Natural Language :: English
Classifier: Intended Audience :: Developers
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cjm-plugin-system
Requires-Dist: cjm-graph-plugin-system
Requires-Dist: cjm-graph-domains
Requires-Dist: python-fasthtml
Requires-Dist: cjm-fasthtml-app-core
Requires-Dist: cjm-fasthtml-daisyui
Requires-Dist: cjm-fasthtml-tailwind
Requires-Dist: cjm-fasthtml-interactions
Requires-Dist: cjm-fasthtml-settings
Requires-Dist: cjm_fasthtml_lucide_icons
Requires-Dist: cjm_fasthtml_keyboard_navigation
Requires-Dist: cjm_fasthtml_card_stack>=0.0.42
Requires-Dist: cjm_workflow_state
Requires-Dist: cjm_source_provider
Requires-Dist: cjm_transcript_source_select
Requires-Dist: cjm_transcript_segmentation
Requires-Dist: cjm_transcript_vad_align
Requires-Dist: cjm_fasthtml_web_audio>=0.0.11
Requires-Dist: cjm_fasthtml_design_system>=0.0.4
Dynamic: license-file

# cjm-transcript-review


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Install

``` bash
pip install cjm_transcript_review
```

## Project Structure

    nbs/
    ├── components/ (7)
    │   ├── audio_controls.ipynb     # Audio playback controls: speed selector and auto-navigate toggle
    │   ├── callbacks.ipynb          # Focus change callback and audio playback JavaScript for the review card stack
    │   ├── card_stack_config.ipynb  # Card stack configuration, HTML IDs, and button IDs for the review card stack
    │   ├── helpers.ipynb            # State getters for the review step from InteractionContext
    │   ├── keyboard_config.ipynb    # Review-specific keyboard building blocks for assembly into a KeyboardManager
    │   ├── review_card.ipynb        # Review card component showing assembled segment with timing and source info
    │   └── step_renderer.ipynb      # Composable render functions for the review card stack step
    ├── routes/ (5)
    │   ├── audio.ipynb       # Route handlers for audio playback controls
    │   ├── card_stack.ipynb  # Card stack UI operations — navigation, viewport, and response builders
    │   ├── commit.ipynb      # Route handler for committing the document to the context graph
    │   ├── core.ipynb        # Review step state management helpers
    │   └── init.ipynb        # Router assembly for Phase 3 review routes
    ├── services/ (1)
    │   └── graph.ipynb  # Graph service for committing documents and segments to the context graph
    ├── html_ids.ipynb  # HTML ID constants for Phase 3: Review & Commit
    ├── models.ipynb    # Review step state and working document model for Phase 3: Review & Commit
    └── utils.ipynb     # Time formatting and source info display utilities for review cards

Total: 16 notebooks across 3 directories

## Module Dependencies

``` mermaid
graph LR
    components_audio_controls[components.audio_controls<br/>audio_controls]
    components_callbacks[components.callbacks<br/>callbacks]
    components_card_stack_config[components.card_stack_config<br/>card_stack_config]
    components_helpers[components.helpers<br/>helpers]
    components_keyboard_config[components.keyboard_config<br/>keyboard_config]
    components_review_card[components.review_card<br/>review_card]
    components_step_renderer[components.step_renderer<br/>step_renderer]
    html_ids[html_ids<br/>html_ids]
    models[models<br/>models]
    routes_audio[routes.audio<br/>audio]
    routes_card_stack[routes.card_stack<br/>card_stack]
    routes_commit[routes.commit<br/>commit]
    routes_core[routes.core<br/>core]
    routes_init[routes.init<br/>init]
    services_graph[services.graph<br/>graph]
    utils[utils<br/>utils]

    components_callbacks --> components_audio_controls
    components_helpers --> models
    components_review_card --> utils
    components_review_card --> html_ids
    components_step_renderer --> components_review_card
    components_step_renderer --> components_audio_controls
    components_step_renderer --> components_callbacks
    components_step_renderer --> components_card_stack_config
    components_step_renderer --> models
    components_step_renderer --> html_ids
    components_step_renderer --> components_keyboard_config
    routes_audio --> routes_core
    routes_audio --> models
    routes_audio --> components_callbacks
    routes_card_stack --> routes_core
    routes_card_stack --> components_review_card
    routes_card_stack --> components_card_stack_config
    routes_card_stack --> components_step_renderer
    routes_card_stack --> models
    routes_commit --> routes_core
    routes_commit --> services_graph
    routes_commit --> utils
    routes_commit --> models
    routes_core --> models
    routes_core --> components_review_card
    routes_init --> routes_core
    routes_init --> routes_card_stack
    routes_init --> routes_commit
    routes_init --> services_graph
    routes_init --> models
    routes_init --> routes_audio
```

*31 cross-module dependencies detected*

## CLI Reference

No CLI commands found in this project.

## Module Overview

Detailed documentation for each module in the project:

### audio (`audio.ipynb`)

> Route handlers for audio playback controls

#### Import

``` python
from cjm_transcript_review.routes.audio import (
    DEBUG_AUDIO_ROUTES,
    init_audio_router
)
```

#### Functions

``` python
def _generate_auto_nav_js(
    enabled:bool  # Whether auto-navigate is enabled
) -> str:  # JavaScript to update auto-navigate flag
    "Generate JS to update auto-navigate flag via shared library."
```

``` python
def _generate_replay_js() -> str:  # JavaScript to replay current segment
    "Generate JS to replay the current segment's audio."
```

``` python
def init_audio_router(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    prefix:str,  # Base prefix for audio routes
    urls:ReviewUrls,  # URL bundle to populate
) -> Tuple[APIRouter, Dict[str, Callable]]:  # (router, routes dict)
    "Initialize audio control routes."
```

#### Variables

``` python
DEBUG_AUDIO_ROUTES = False
```

### audio_controls (`audio_controls.ipynb`)

> Audio playback controls: speed selector and auto-navigate toggle

#### Import

``` python
from cjm_transcript_review.components.audio_controls import (
    AudioControlIds,
    render_auto_navigate_toggle,
    render_audio_controls
)
```

#### Functions

``` python
def _toggle_color_js(toggle_id:str) -> str:  # JS snippet to sync toggle color classes
    """Generate JS to swap bg-error/bg-success on the toggle based on checked state."""
    return (
        f"var _t=document.getElementById('{toggle_id}');"
        f"if(_t){{_t.classList.remove('{_TOGGLE_BG_OFF}','{_TOGGLE_BG_ON}');"
        f"_t.classList.add(_t.checked?'{_TOGGLE_BG_ON}':'{_TOGGLE_BG_OFF}');}}"
    )

def render_auto_navigate_toggle(
    enabled:bool=False,  # Whether auto-navigate is enabled
) -> Any:  # Auto-navigate toggle component
    "Generate JS to swap bg-error/bg-success on the toggle based on checked state."
```

``` python
def render_auto_navigate_toggle(
    enabled:bool=False,  # Whether auto-navigate is enabled
) -> Any:  # Auto-navigate toggle component
    "Render auto-navigate toggle switch (client-side only, no server persistence)."
```

``` python
def render_audio_controls(
    current_speed:float=1.0,  # Current playback speed
    auto_navigate:bool=False,  # Whether auto-navigate is enabled
    speed_url:str="",  # URL for speed changes
    oob:bool=False,  # Whether to render as OOB swap
) -> Any:  # Combined audio controls component
    "Render combined audio controls (speed selector + auto-navigate toggle)."
```

#### Classes

``` python
class AudioControlIds:
    "HTML ID constants for audio control elements."
```

#### Variables

``` python
_REVIEW_SPEED_CONFIG
_TOGGLE_BG_OFF  # Red when auto-play disabled
_TOGGLE_BG_ON  # Green when auto-play enabled
```

### callbacks (`callbacks.ipynb`)

> Focus change callback and audio playback JavaScript for the review
> card stack

#### Import

``` python
from cjm_transcript_review.components.callbacks import (
    REVIEW_AUDIO_CONFIG,
    generate_review_callbacks_script
)
```

#### Functions

``` python
def _generate_toggle_auto_play_js() -> str:  # JS defining window.toggleReviewAutoPlay
    "Generate JS for the A key auto-play toggle function."
```

``` python
def generate_review_callbacks_script(
    ids:CardStackHtmlIds,  # Card stack HTML IDs
    button_ids:CardStackButtonIds,  # Card stack button IDs
    config:CardStackConfig,  # Card stack configuration
    urls:CardStackUrls,  # Card stack URL bundle
    container_id:str,  # ID of the review container (parent of card stack)
    focus_input_id:str,  # ID of hidden input for focused segment index
) -> any:  # Script element with all JavaScript callbacks
    "Generate JavaScript for review card stack with Web Audio API audition."
```

#### Variables

``` python
REVIEW_AUDIO_CONFIG
```

### card_stack (`card_stack.ipynb`)

> Card stack UI operations — navigation, viewport, and response builders

#### Import

``` python
from cjm_transcript_review.routes.card_stack import (
    init_card_stack_router
)
```

#### Functions

``` python
def _build_slots_oob(
    assembled:List[AssembledSegment],  # Assembled segments with VAD chunks
    state:CardStackState,  # Card stack viewport state
    urls:ReviewUrls,  # URL bundle
) -> List[Any]:  # OOB slot elements
    "Build OOB slot updates for the viewport sections."
```

``` python
def _build_nav_response(
    assembled:List[AssembledSegment],  # Assembled segments with VAD chunks
    state:CardStackState,  # Card stack viewport state
    urls:ReviewUrls,  # URL bundle
) -> Tuple:  # OOB elements (slots + progress + focus)
    "Build OOB response for navigation."
```

``` python
def _handle_review_navigate(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    sess,  # FastHTML session object
    direction:str,  # Navigation direction: "up", "down", "first", "last", "page_up", "page_down"
    urls:ReviewUrls,  # URL bundle for review routes
):  # OOB slot updates with progress, focus, and source position
    "Navigate to a different segment in the viewport using OOB slot swaps."
```

``` python
def _handle_review_navigate_to_index(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    sess,  # FastHTML session object
    target_index:int,  # Target item index to navigate to
    urls:ReviewUrls,  # URL bundle for review routes
):  # OOB slot updates with progress, focus, and source position
    "Navigate to a specific segment index using OOB slot swaps."
```

``` python
async def _handle_review_update_viewport(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    request,  # FastHTML request object
    sess,  # FastHTML session object
    visible_count:int,  # New number of visible cards
    urls:ReviewUrls,  # URL bundle for review routes
):  # Full viewport component (outerHTML swap)
    "Update the viewport with a new card count."
```

``` python
def _handle_review_save_width(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    sess,  # FastHTML session object
    card_width:int,  # Card stack width in rem
) -> None:  # No response body (swap=none on client)
    "Save the card stack width to server state."
```

``` python
def init_card_stack_router(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    prefix:str,  # Route prefix (e.g., "/workflow/review/card_stack")
    urls:ReviewUrls,  # URL bundle (populated after routes defined)
) -> Tuple[APIRouter, Dict[str, Callable]]:  # (router, route_dict)
    "Initialize card stack routes for review."
```

### card_stack_config (`card_stack_config.ipynb`)

> Card stack configuration, HTML IDs, and button IDs for the review card
> stack

#### Import

``` python
from cjm_transcript_review.components.card_stack_config import (
    REVIEW_CS_CONFIG,
    REVIEW_CS_IDS,
    REVIEW_CS_BTN_IDS
)
```

#### Variables

``` python
REVIEW_CS_CONFIG
REVIEW_CS_IDS
REVIEW_CS_BTN_IDS
```

### commit (`commit.ipynb`)

> Route handler for committing the document to the context graph

#### Import

``` python
from cjm_transcript_review.routes.commit import (
    DEBUG_COMMIT_ROUTES,
    COMMIT_ALERT_ID,
    CommitResult,
    init_commit_router
)
```

#### Functions

``` python
def _render_commit_alert(
    result:CommitResult,  # Commit operation result
    auto_dismiss_ms:int=5000,  # Auto-dismiss after milliseconds (0 = no auto-dismiss)
) -> Div:  # Alert element with optional auto-dismiss script
    "Render a success or error alert for commit result."
```

``` python
async def _handle_commit(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str,  # Session identifier string
    graph_service:GraphService,  # Graph service for committing
    document_title:Optional[str]=None,  # Override document title (None = auto-generate)
) -> CommitResult:  # Result of the commit operation
    "Handle committing the document to the context graph."
```

``` python
def init_commit_router(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    prefix:str,  # Base prefix for commit route
    graph_service:GraphService,  # Graph service for committing
    urls:ReviewUrls,  # URL bundle to populate
    alert_container_id:str="commit-alert-container",  # ID of container for alert OOB swap
) -> Tuple[APIRouter, Dict[str, Callable]]:  # (router, routes dict)
    "Initialize commit route."
```

#### Classes

``` python
@dataclass
class CommitResult:
    "Result of a commit operation."
    
    success: bool  # Whether the commit succeeded
    document_id: Optional[str]  # Created document node ID
    segment_count: int = 0  # Number of segments committed
    edge_count: int = 0  # Number of edges created
    error: Optional[str]  # Error message if failed
```

#### Variables

``` python
DEBUG_COMMIT_ROUTES = False
COMMIT_ALERT_ID = 'commit-alert'
```

### core (`core.ipynb`)

> Review step state management helpers

#### Import

``` python
from cjm_transcript_review.routes.core import (
    WorkflowStateStore,
    DEBUG_REVIEW_STATE,
    ReviewContext
)
```

#### Functions

``` python
def _get_review_state(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str  # Session identifier string
) -> ReviewStepState:  # Review step state dictionary
    "Get the review step state from the workflow state store."
```

``` python
def _get_segmentation_state(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str  # Session identifier string
) -> Dict[str, Any]:  # Segmentation step state dictionary
    "Get the segmentation step state from the workflow state store."
```

``` python
def _get_alignment_state(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str  # Session identifier string
) -> Dict[str, Any]:  # Alignment step state dictionary
    "Get the alignment step state from the workflow state store."
```

``` python
def _load_review_context(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str  # Session identifier string
) -> ReviewContext:  # Common review state values
    "Load commonly-needed review state values including cross-step data."
```

``` python
def _get_assembled_segments(
    ctx:ReviewContext  # Loaded review context
) -> List[AssembledSegment]:  # Paired segments with VAD chunks
    "Pair segments with VAD chunks for review display."
```

``` python
def _update_review_state(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str,  # Session identifier string
    focused_index:int=None,  # Updated focused index (None = don't change)
    visible_count:int=None,  # Visible card count (None = don't change)
    is_auto_mode:bool=None,  # Auto-adjust mode flag (None = don't change)
    card_width:int=None,  # Card stack width in rem (None = don't change)
    document_title:str=None,  # Document title (None = don't change)
    is_validated:bool=None,  # Validation flag (None = don't change)
    playback_speed:float=None,  # Playback speed (None = don't change)
    auto_navigate:bool=None,  # Auto-navigate flag (None = don't change)
) -> None
    "Update the review step state in the workflow state store."
```

``` python
def _handle_update_title(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str,  # Session identifier string
    document_title:str,  # New document title from form input
) -> None
    "Update the document title in review state."
```

``` python
def _build_card_stack_state(
    ctx:ReviewContext,  # Loaded review context
) -> CardStackState:  # Card stack state for library functions
    "Build a CardStackState from review context for library calls."
```

#### Classes

``` python
class ReviewContext(NamedTuple):
    "Common review state values loaded by handlers."
```

#### Variables

``` python
DEBUG_REVIEW_STATE = False
```

### graph (`graph.ipynb`)

> Graph service for committing documents and segments to the context
> graph

#### Import

``` python
from cjm_transcript_review.services.graph import (
    GraphService
)
```

#### Classes

``` python
class GraphService:
    def __init__(
        self,
        plugin_manager:PluginManager,  # Plugin manager for accessing graph plugin
        plugin_name:str="cjm-graph-plugin-sqlite",  # Name of the graph plugin
    )
    "Service for committing structure to context graph."
    
    def __init__(
            self,
            plugin_manager:PluginManager,  # Plugin manager for accessing graph plugin
            plugin_name:str="cjm-graph-plugin-sqlite",  # Name of the graph plugin
        )
        "Initialize the graph service."
    
    def is_available(self) -> bool:  # True if plugin is loaded and ready
            """Check if the graph plugin is available."""
            return self._manager.get_plugin(self._plugin_name) is not None
        
        def ensure_loaded(
            self,
            config:Optional[Dict[str, Any]]=None,  # Optional plugin configuration
        ) -> bool:  # True if successfully loaded
        "Check if the graph plugin is available."
    
    def ensure_loaded(
            self,
            config:Optional[Dict[str, Any]]=None,  # Optional plugin configuration
        ) -> bool:  # True if successfully loaded
        "Ensure the graph plugin is loaded."
    
    async def commit_document_async(
            self,
            title:str,  # Document title
            text_segments:List[TextSegment],  # Text segments from decomposition
            vad_chunks:List[VADChunk],  # VAD chunks for timing (1:1 with segments)
            media_type:str="audio",  # Source media type
        ) -> Dict[str, Any]:  # Result with document_id and segment_ids
        "Commit a document to the context graph.

Assembles text segments with VAD timing at commit time.
Requires 1:1 alignment: len(text_segments) == len(vad_chunks)."
    
    def commit_document(
            self,
            title:str,  # Document title
            text_segments:List[TextSegment],  # Text segments from decomposition
            vad_chunks:List[VADChunk],  # VAD chunks for timing
            media_type:str="audio",  # Source media type
        ) -> Dict[str, Any]:  # Result with document_id and segment_ids
        "Commit a document to the context graph synchronously."
```

### helpers (`helpers.ipynb`)

> State getters for the review step from InteractionContext

#### Import

``` python
from cjm_transcript_review.components.helpers import *
```

#### Functions

``` python
def _get_review_state(
    ctx:InteractionContext  # Interaction context with state
) -> ReviewStepState:  # Typed review step state
    "Get the full review step state from context."
```

``` python
def _get_focused_index(
    ctx:InteractionContext,  # Interaction context with state
    default:int=0,  # Default focused index
) -> int:  # Currently focused segment index
    "Get the currently focused segment index."
```

``` python
def _get_visible_count(
    ctx:InteractionContext,  # Interaction context with state
    default:int=5,  # Default visible card count
) -> int:  # Number of visible cards in viewport
    "Get the stored visible card count."
```

``` python
def _get_is_auto_mode(
    ctx:InteractionContext,  # Interaction context with state
) -> bool:  # Whether card count is in auto-adjust mode
    "Get whether the card count is in auto-adjust mode."
```

``` python
def _get_card_width(
    ctx:InteractionContext,  # Interaction context with state
    default:int=50,  # Default card width in rem
) -> int:  # Card stack width in rem
    "Get the stored card stack width."
```

``` python
def _get_segments(
    ctx:InteractionContext  # Interaction context with state
) -> List[TextSegment]:  # List of TextSegment objects from segmentation step
    "Get text segments from segmentation step state."
```

``` python
def _get_vad_chunks(
    ctx:InteractionContext  # Interaction context with state
) -> List[VADChunk]:  # List of VADChunk objects from alignment step
    "Get VAD chunks from alignment step state."
```

``` python
def _get_media_path(
    ctx:InteractionContext  # Interaction context with state
) -> Optional[str]:  # Path to original audio file or None
    "Get the original audio file path from alignment step."
```

``` python
def _is_aligned(
    ctx:InteractionContext  # Interaction context with state
) -> bool:  # True if segment count matches VAD chunk count
    "Check if segments are aligned with VAD chunks (1:1 match)."
```

### html_ids (`html_ids.ipynb`)

> HTML ID constants for Phase 3: Review & Commit

#### Import

``` python
from cjm_transcript_review.html_ids import (
    ReviewHtmlIds
)
```

#### Classes

``` python
class ReviewHtmlIds:
    "HTML ID constants for Phase 3: Review & Commit."
    
    def as_selector(
            id_str:str  # The HTML ID to convert
        ) -> str:  # CSS selector with # prefix
        "Convert an ID to a CSS selector format."
    
    def review_card(
            index:int  # Segment index
        ) -> str:  # HTML ID for review card
        "Generate HTML ID for a review card."
```

### init (`init.ipynb`)

> Router assembly for Phase 3 review routes

#### Import

``` python
from cjm_transcript_review.routes.init import (
    init_review_routers
)
```

#### Functions

``` python
def init_review_routers(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    prefix:str,  # Base prefix for review routes (e.g., "/workflow/review")
    audio_src_url:str="",  # Audio source route (from core routes)
    graph_service:Optional[GraphService]=None,  # Graph service for commit (None = no commit route)
    alert_container_id:str="commit-alert-container",  # ID of container for commit alerts
) -> Tuple[List[APIRouter], ReviewUrls, Dict[str, Callable]]:  # (routers, urls, routes)
    "Initialize and return all review routers with URL bundle."
```

### keyboard_config (`keyboard_config.ipynb`)

> Review-specific keyboard building blocks for assembly into a
> KeyboardManager

#### Import

``` python
from cjm_transcript_review.components.keyboard_config import (
    create_review_kb_parts,
    create_review_keyboard_manager
)
```

#### Functions

``` python
def create_review_kb_parts(
    ids:CardStackHtmlIds,  # Card stack HTML IDs
    button_ids:CardStackButtonIds,  # Card stack button IDs for navigation
    config:CardStackConfig,  # Card stack configuration
) -> Tuple[FocusZone, tuple, tuple]:  # (zone, actions, modes)
    "Create review-specific keyboard building blocks."
```

``` python
def create_review_keyboard_manager(
    ids:CardStackHtmlIds,  # Card stack HTML IDs
    button_ids:CardStackButtonIds,  # Card stack button IDs for navigation
    config:CardStackConfig,  # Card stack configuration
) -> ZoneManager:  # Configured keyboard zone manager
    "Create the keyboard zone manager for the review step."
```

### models (`models.ipynb`)

> Review step state and working document model for Phase 3: Review &
> Commit

#### Import

``` python
from cjm_transcript_review.models import (
    ReviewStepState,
    ReviewUrls,
    WorkingDocument
)
```

#### Classes

``` python
class ReviewStepState(TypedDict):
    "State for Phase 3: Review & Commit."
```

``` python
@dataclass
class ReviewUrls:
    "URL bundle for Phase 3 review route handlers and renderers."
    
    card_stack: CardStackUrls = field(...)
    audio_src: str = ''  # Audio source route
    speed_change: str = ''  # Playback speed change handler
    toggle_auto_nav: str = ''  # Auto-navigate toggle handler
    replay_current: str = ''  # Replay current segment handler
    update_title: str = ''  # Document title update handler
    commit: str = ''  # Commit handler route
```

``` python
@dataclass
class WorkingDocument:
    "Container for workflow state during structure decomposition."
    
    title: str = ''  # Document title
    media_type: str = 'audio'  # Source media type ('audio', 'video', 'text')
    media_path: Optional[str]  # Path to primary source media
    source_blocks: List[SourceBlock] = field(...)  # Ordered source blocks
    combined_text: str = ''  # Concatenated text from all sources
    segments: List[TextSegment] = field(...)  # Decomposed segments
    vad_chunks: List[VADChunk] = field(...)  # VAD time ranges
    audio_duration: Optional[float]  # Total audio duration in seconds
    
    def to_dict(self) -> Dict[str, Any]:  # Dictionary representation
            """Convert to dictionary for JSON serialization."""
            return {
                'title': self.title,
        "Convert to dictionary for JSON serialization."
    
    def from_dict(
            cls,
            data: Dict[str, Any]  # Dictionary representation
        ) -> "WorkingDocument":  # Reconstructed WorkingDocument
        "Create from dictionary."
```

### review_card (`review_card.ipynb`)

> Review card component showing assembled segment with timing and source
> info

#### Import

``` python
from cjm_transcript_review.components.review_card import (
    AssembledSegment,
    render_review_card,
    create_review_card_renderer
)
```

#### Functions

``` python
def render_review_card(
    assembled:AssembledSegment,  # Assembled segment with text and timing
    card_role:CardRole,  # Role of this card in viewport ("focused" or "context")
    has_boundary_above:bool=False,  # Audio file boundary exists above this card
    has_boundary_below:bool=False,  # Audio file boundary exists below this card
) -> Any:  # Review card component
    "Render a single review card with text, timing, source info, playing indicator, and play button."
```

``` python
def create_review_card_renderer(
    audio_file_boundaries:Set[int]=None,  # Indices where audio_file_index changes
) -> Callable:  # Card renderer callback: (item, CardRenderContext) -> FT
    "Create a card renderer callback for review cards."
```

#### Classes

``` python
@dataclass
class AssembledSegment:
    "A segment paired with its corresponding VAD chunk for review."
    
    segment: TextSegment  # Text segment with content and source info
    vad_chunk: VADChunk  # VAD chunk with timing
    
    def index(self) -> int:  # Segment index for card stack
            """Get the index from the segment."""
            return self.segment.index
        
        @property
        def text(self) -> str:  # Segment text content
        "Get the index from the segment."
    
    def text(self) -> str:  # Segment text content
            """Get the text content from the segment."""
            return self.segment.text
        
        @property
        def start_time(self) -> float:  # Start time from VAD chunk
        "Get the text content from the segment."
    
    def start_time(self) -> float:  # Start time from VAD chunk
            """Get the start time from the VAD chunk."""
            return self.vad_chunk.start_time
        
        @property
        def end_time(self) -> float:  # End time from VAD chunk
        "Get the start time from the VAD chunk."
    
    def end_time(self) -> float:  # End time from VAD chunk
        "Get the end time from the VAD chunk."
```

### step_renderer (`step_renderer.ipynb`)

> Composable render functions for the review card stack step

#### Import

``` python
from cjm_transcript_review.components.step_renderer import (
    DEBUG_REVIEW_RENDER,
    render_review_toolbar,
    render_review_stats,
    render_review_source_position,
    render_review_content,
    render_review_footer,
    render_review_step
)
```

#### Functions

``` python
def render_review_toolbar(
    playback_speed:float=1.0,  # Current playback speed
    auto_navigate:bool=False,  # Whether auto-navigate is enabled
    document_title:str="",  # Current document title
    urls:ReviewUrls=None,  # URL bundle for audio control routes
    oob:bool=False,  # Whether to render as OOB swap
) -> Any:  # Toolbar component
    "Render the review toolbar with title input and audio controls."
```

``` python
def render_review_stats(
    assembled:List[AssembledSegment],  # Assembled segments
    oob:bool=False,  # Whether to render as OOB swap
) -> Any:  # Statistics component
    "Render review statistics."
```

``` python
def render_review_source_position(
    assembled:List[AssembledSegment],  # Assembled segments
    focused_index:int=0,  # Currently focused segment index
    oob:bool=False,  # Whether to render as OOB swap
) -> Any:  # Audio file position indicator (empty if single file)
    "Render audio file position indicator for the focused segment."
```

``` python
def render_review_content(
    assembled:List[AssembledSegment],  # Assembled segments to display
    focused_index:int,  # Currently focused segment index
    visible_count:int,  # Number of visible cards in viewport
    card_width:int,  # Card stack width in rem
    urls:ReviewUrls,  # URL bundle for review routes
    audio_urls:Optional[List[str]]=None,  # Audio file URLs for Web Audio API
) -> Any:  # Main content area
    "Render the review content area with card stack viewport and keyboard system."
```

``` python
def render_review_footer(
    assembled:List[AssembledSegment],  # Assembled segments
    focused_index:int,  # Currently focused segment index
) -> Any:  # Footer content with progress indicator, source position, and stats
    "Render footer content with progress indicator, source position, and statistics."
```

``` python
def render_review_step(
    assembled:List[AssembledSegment],  # Assembled segments to display
    focused_index:int=0,  # Currently focused segment index
    visible_count:int=DEFAULT_VISIBLE_COUNT,  # Number of visible cards
    is_auto_mode:bool=False,  # Whether card count is in auto-adjust mode
    card_width:int=DEFAULT_CARD_WIDTH,  # Card stack width in rem
    playback_speed:float=1.0,  # Current playback speed
    auto_navigate:bool=False,  # Whether auto-navigate is enabled
    document_title:str="",  # Current document title
    urls:ReviewUrls=None,  # URL bundle for review routes
    audio_urls:Optional[List[str]]=None,  # Audio file URLs for Web Audio API
) -> Any:  # Complete review step component
    "Render the complete review step with toolbar, content, and footer."
```

#### Variables

``` python
DEBUG_REVIEW_RENDER = False
```

### utils (`utils.ipynb`)

> Time formatting and source info display utilities for review cards

#### Import

``` python
from cjm_transcript_review.utils import (
    format_time,
    format_duration,
    truncate_id,
    format_char_range,
    format_source_info,
    generate_document_title
)
```

#### Functions

``` python
def format_time(
    seconds:Optional[float]  # Time in seconds
) -> str:  # Formatted time string (m:ss.s)
    "Format seconds as m:ss.s for sub-second display."
```

``` python
def format_duration(
    start:Optional[float],  # Start time in seconds
    end:Optional[float],  # End time in seconds
) -> str:  # Formatted duration string (e.g., "3.2s")
    "Format duration from start/end times."
```

``` python
def truncate_id(
    id_str:Optional[str],  # Full ID string
    length:int=8,  # Number of characters to keep
) -> str:  # Truncated ID with ellipsis if needed
    "Truncate an ID string for display, adding ellipsis if truncated."
```

``` python
def format_char_range(
    start_char:Optional[int],  # Start character index
    end_char:Optional[int],  # End character index
) -> str:  # Formatted range string (e.g., "char:25-68")
    "Format character range for source reference display."
```

``` python
def format_source_info(
    provider_id:Optional[str],  # Source provider identifier
    source_id:Optional[str],  # Source record ID
    start_char:Optional[int]=None,  # Start character index
    end_char:Optional[int]=None,  # End character index
) -> str:  # Formatted source info string
    "Format source info for display in review cards."
```

``` python
def generate_document_title(
    media_path:Optional[str],  # Path to media file
    default:str="Untitled Document",  # Fallback title if path is None
) -> str:  # Clean document title
    "Generate a document title from a media file path."
```
