Metadata-Version: 2.4
Name: cjm-transcript-vad-align
Version: 0.0.15
Summary: FastHTML VAD alignment component for transcript decomposition with Silero VAD audio chunking, card stack navigation with audio playback, and Web Audio API integration.
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-vad-align
Project-URL: Documentation, https://cj-mills.github.io/cjm-transcript-vad-align
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_media_plugin_system
Requires-Dist: cjm-fasthtml-app-core
Requires-Dist: cjm-fasthtml-daisyui
Requires-Dist: cjm_fasthtml_lucide_icons
Requires-Dist: cjm_fasthtml_file_browser
Requires-Dist: cjm_fasthtml_keyboard_navigation
Requires-Dist: cjm_fasthtml_card_stack
Requires-Dist: cjm_fasthtml_token_selector
Requires-Dist: duckdb
Requires-Dist: pandas
Requires-Dist: cjm_workflow_state
Requires-Dist: cjm_source_provider
Requires-Dist: cjm_fasthtml_interactions
Requires-Dist: cjm_transcript_source_select
Requires-Dist: cjm_fasthtml_web_audio>=0.0.8
Dynamic: license-file

# cjm-transcript-vad-align


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

## Install

``` bash
pip install cjm_transcript_vad_align
```

## Project Structure

    nbs/
    ├── components/ (7)
    │   ├── audio_controls.ipynb     # Audio playback controls for the alignment card stack: auto-navigate toggle
    │   ├── callbacks.ipynb          # Focus change callback and audio playback JavaScript for the alignment card stack
    │   ├── card_stack_config.ipynb  # Card stack configuration, HTML IDs, and button IDs for the VAD alignment card stack
    │   ├── helpers.ipynb            # State getters for the alignment step from InteractionContext
    │   ├── keyboard_config.ipynb    # Alignment-specific keyboard building blocks for assembly into a shared ZoneManager
    │   ├── step_renderer.ipynb      # Composable render functions for the alignment card stack column
    │   └── vad_card.ipynb           # VAD chunk card renderer for the alignment card stack
    ├── routes/ (5)
    │   ├── audio.ipynb       # Route handlers for alignment audio playback controls
    │   ├── card_stack.ipynb  # Card stack operations for the alignment column: navigation, viewport update, width save
    │   ├── core.ipynb        # Alignment state context, getters, and updaters for route handlers
    │   ├── handlers.ipynb    # Workflow-specific alignment handlers: init
    │   └── init.ipynb        # Router assembly for Phase 2 alignment routes
    ├── services/ (1)
    │   └── alignment.ipynb  # Alignment service for temporal coordination via Silero VAD plugin
    ├── html_ids.ipynb  # HTML ID constants for Phase 2 Right Column: VAD Alignment
    ├── models.ipynb    # Data models and URL bundles for the alignment package
    └── utils.ipynb     # Time formatting utilities for VAD alignment display

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_step_renderer[components.step_renderer<br/>step_renderer]
    components_vad_card[components.vad_card<br/>vad_card]
    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_core[routes.core<br/>core]
    routes_handlers[routes.handlers<br/>handlers]
    routes_init[routes.init<br/>init]
    services_alignment[services.alignment<br/>alignment]
    utils[utils<br/>utils]

    components_callbacks --> components_audio_controls
    components_helpers --> models
    components_step_renderer --> components_callbacks
    components_step_renderer --> components_vad_card
    components_step_renderer --> models
    components_step_renderer --> utils
    components_step_renderer --> components_card_stack_config
    components_step_renderer --> components_audio_controls
    components_step_renderer --> html_ids
    components_vad_card --> utils
    components_vad_card --> html_ids
    components_vad_card --> models
    routes_audio --> models
    routes_audio --> routes_core
    routes_card_stack --> components_vad_card
    routes_card_stack --> routes_core
    routes_card_stack --> models
    routes_card_stack --> utils
    routes_card_stack --> components_step_renderer
    routes_card_stack --> components_card_stack_config
    routes_core --> models
    routes_handlers --> services_alignment
    routes_handlers --> models
    routes_handlers --> components_step_renderer
    routes_handlers --> routes_core
    routes_handlers --> html_ids
    routes_init --> routes_handlers
    routes_init --> routes_card_stack
    routes_init --> routes_audio
    routes_init --> services_alignment
    routes_init --> models
    routes_init --> routes_core
    services_alignment --> models
    utils --> models
```

*34 cross-module dependencies detected*

## CLI Reference

No CLI commands found in this project.

## Module Overview

Detailed documentation for each module in the project:

### alignment (`alignment.ipynb`)

> Alignment service for temporal coordination via Silero VAD plugin

#### Import

``` python
from cjm_transcript_vad_align.services.alignment import (
    AlignmentService,
    check_alignment_ready
)
```

#### Functions

``` python
def check_alignment_ready(
    segment_count: int,  # Number of text segments
    chunk_count: int,  # Number of VAD chunks
) -> bool:  # True if counts match and alignment can proceed
    "Check if segment and VAD chunk counts match for 1:1 alignment."
```

#### Classes

``` python
class AlignmentService:
    def __init__(
        self,
        plugin_manager: PluginManager,  # Plugin manager for accessing VAD plugin
        plugin_name: str = "cjm-media-plugin-silero-vad"  # Name of the VAD plugin
    )
    "Service for temporal alignment via Silero VAD plugin."
    
    def __init__(
            self,
            plugin_manager: PluginManager,  # Plugin manager for accessing VAD plugin
            plugin_name: str = "cjm-media-plugin-silero-vad"  # Name of the VAD plugin
        )
        "Initialize the alignment service."
    
    def is_available(self) -> bool:  # True if plugin is loaded and ready
            """Check if the VAD 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 VAD plugin is available."
    
    def ensure_loaded(
            self,
            config: Optional[Dict[str, Any]] = None  # Optional plugin configuration
        ) -> bool:  # True if successfully loaded
        "Ensure the VAD plugin is loaded."
    
    async def analyze_audio_async(
            self,
            media_path: str  # Path to audio/video file
        ) -> tuple[List[VADChunk], float]:  # (VAD chunks, total duration)
        "Analyze audio file and return VAD chunks."
    
    def analyze_audio(
            self,
            media_path: str  # Path to audio/video file
        ) -> tuple[List[VADChunk], float]:  # (VAD chunks, total duration)
        "Analyze audio file synchronously."
```

### audio (`audio.ipynb`)

> Route handlers for alignment audio playback controls

#### Import

``` python
from cjm_transcript_vad_align.routes.audio import (
    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_speed_change_js(
    speed:float  # New playback speed
) -> str:  # JavaScript to update playback speed
    "Generate JS to update Web Audio API playback speed via shared library."
```

``` 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:AlignmentUrls,  # URL bundle to populate
) -> Tuple[APIRouter, Dict[str, Callable]]:  # (router, routes dict)
    "Initialize alignment audio control routes."
```

### audio_controls (`audio_controls.ipynb`)

> Audio playback controls for the alignment card stack: auto-navigate
> toggle

#### Import

``` python
from cjm_transcript_vad_align.components.audio_controls import (
    PLAYBACK_SPEEDS,
    AlignAudioControlIds,
    render_align_speed_selector,
    render_align_auto_navigate_toggle,
    render_align_audio_controls
)
```

#### Functions

``` python
def render_align_speed_selector(
    current_speed:float=1.0,  # Current playback speed
    change_url:str="",  # URL to POST speed changes to (for server persistence)
) -> Any:  # Speed selector component
    "Render playback speed selector dropdown for alignment audio."
```

``` 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_align_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_align_auto_navigate_toggle(
    enabled:bool=False,  # Whether auto-navigate is enabled
) -> Any:  # Auto-navigate toggle component
    "Render auto-navigate toggle switch for alignment audio (client-side only)."
```

``` python
def render_align_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 (server persistence)
    oob:bool=False,  # Whether to render as OOB swap
) -> Any:  # Combined audio controls component
    "Render alignment audio controls (speed selector + auto-navigate toggle)."
```

#### Classes

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

#### Variables

``` python
PLAYBACK_SPEEDS: List[tuple]
_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 alignment
> card stack

#### Import

``` python
from cjm_transcript_vad_align.components.callbacks import (
    ALIGN_AUDIO_CONFIG,
    generate_align_callbacks_script
)
```

#### Functions

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

``` python
def generate_align_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 alignment container (parent of card stack)
    focus_input_id:str,  # ID of hidden input for focused chunk index
    should_play_fn:str="",  # Consumer-defined play guard function name (overrides default zone guard)
) -> any:  # Script element with all JavaScript callbacks
    "Generate JavaScript for alignment card stack with Web Audio API audition."
```

#### Variables

``` python
ALIGN_AUDIO_CONFIG
```

### card_stack (`card_stack.ipynb`)

> Card stack operations for the alignment column: navigation, viewport
> update, width save

#### Import

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

#### Functions

``` python
def _build_nav_response(
    chunk_dicts:List[Dict[str, Any]],  # Serialized VAD chunks
    state:CardStackState,  # Current card stack state
    urls:AlignmentUrls,  # URL bundle
) -> Tuple:  # OOB response elements (slots + progress + focus)
    "Build OOB response for navigation changes."
```

``` python
def _handle_align_navigate(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    sess:Any,  # FastHTML session object
    direction:str,  # Navigation direction: up/down/first/last/page_up/page_down
    urls:AlignmentUrls,  # URL bundle
) -> Tuple:  # OOB response elements (slots + progress + focus + source position)
    "Navigate the alignment card stack."
```

``` python
def _handle_align_navigate_to_index(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    sess:Any,  # FastHTML session object
    target_index:int,  # Target item index to navigate to
    urls:AlignmentUrls,  # URL bundle
) -> Tuple:  # OOB response elements (slots + progress + focus + source position)
    "Navigate the alignment card stack to a specific index."
```

``` python
async def _handle_align_update_viewport(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    request:Any,  # FastHTML request object
    sess:Any,  # FastHTML session object
    visible_count:int,  # New visible card count
    urls:AlignmentUrls,  # URL bundle
) -> Tuple:  # OOB section elements for viewport update
    "Update viewport with new card count via OOB section swaps."
```

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

``` 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/align/card_stack")
    urls:AlignmentUrls,  # URL bundle (populated after routes defined)
) -> Tuple[APIRouter, Dict[str, Callable]]:  # (router, route_dict)
    "Initialize card stack routes for alignment."
```

### card_stack_config (`card_stack_config.ipynb`)

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

#### Import

``` python
from cjm_transcript_vad_align.components.card_stack_config import (
    ALIGN_CS_CONFIG,
    ALIGN_CS_IDS,
    ALIGN_CS_BTN_IDS
)
```

#### Variables

``` python
ALIGN_CS_CONFIG
ALIGN_CS_IDS
ALIGN_CS_BTN_IDS
```

### core (`core.ipynb`)

> Alignment state context, getters, and updaters for route handlers

#### Import

``` python
from cjm_transcript_vad_align.routes.core import (
    WorkflowStateStore,
    DEBUG_ALIGN_STATE,
    AlignContext
)
```

#### Functions

``` python
def _get_alignment_state(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str,  # Session identifier
) -> AlignmentStepState:  # Typed alignment step state
    "Get the alignment step state from the workflow state store."
```

``` python
def _get_selection_state(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str,  # Session identifier
) -> Dict[str, Any]:  # Selection step state dictionary
    "Get the selection step state (Phase 1) from the workflow state store."
```

``` python
def _load_alignment_context(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str,  # Session identifier
) -> AlignContext:  # Loaded context with all common alignment state
    "Load commonly-needed alignment state values in a single call."
```

``` python
def _update_alignment_state(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    session_id:str,  # Session identifier
    vad_chunks=None,  # Updated VAD chunks (serialized)
    focused_chunk_index=None,  # Updated focused chunk index
    is_initialized=None,  # Initialization flag
    visible_count=None,  # Visible card count
    is_auto_mode=None,  # Auto-adjust mode flag
    card_width=None,  # Card stack width in rem
    media_path=None,  # First audio file path (backward compat)
    media_paths=None,  # Ordered list of all audio file paths
    audio_duration=None,  # Audio duration
    auto_navigate=None,  # Auto-navigate flag
    playback_speed=None,  # Pitch-preserving playback speed
) -> None
    "Update the alignment step state in the workflow state store."
```

``` python
def _to_vad_chunks(
    chunk_dicts:List[Dict[str, Any]]  # Serialized VAD chunk dictionaries
) -> List[VADChunk]:  # List of VADChunk objects
    "Convert chunk dictionaries to VADChunk objects."
```

``` python
def _build_card_stack_state(
    ctx:AlignContext,  # Loaded alignment context
    active_mode:str=None,  # Current interaction mode name (unused for alignment)
) -> CardStackState:  # Card stack state for library functions
    "Build a CardStackState from alignment context for library functions."
```

#### Classes

``` python
class AlignContext(NamedTuple):
    "Common alignment state values loaded by handlers."
```

#### Variables

``` python
DEBUG_ALIGN_STATE = False
```

### handlers (`handlers.ipynb`)

> Workflow-specific alignment handlers: init

#### Import

``` python
from cjm_transcript_vad_align.routes.handlers import (
    DEBUG_ALIGNMENT,
    AlignInitResult,
    init_workflow_router
)
```

#### Functions

``` python
async def _handle_align_init(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    source_service:SourceService,  # Service for fetching source blocks
    alignment_service:AlignmentService,  # Service for VAD analysis
    request,  # FastHTML request object
    sess,  # FastHTML session object
    urls:AlignmentUrls,  # URL bundle
    visible_count:int=DEFAULT_VISIBLE_COUNT,  # Initial visible card count
    card_width:int=DEFAULT_CARD_WIDTH,  # Initial card width in rem
    should_play_fn:str="",  # Consumer-defined play guard function name
) -> AlignInitResult:  # Pure domain result for wrapper to use
    """
    Initialize alignment from audio files via VAD plugin.
    
    Processes all selected sources' audio files, generating VAD chunks
    for each with correct audio_file_index. Returns pure domain data.
    """
```

``` python
def init_workflow_router(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    source_service:SourceService,  # Service for fetching source blocks
    alignment_service:AlignmentService,  # Service for VAD analysis
    prefix:str,  # Route prefix (e.g., "/workflow/align/workflow")
    urls:AlignmentUrls,  # URL bundle (populated after routes defined)
    handler_init:Callable=None,  # Optional wrapped init handler
) -> Tuple[APIRouter, Dict[str, Callable]]:  # (router, route_dict)
    """
    Initialize workflow routes for alignment.
    
    Accepts optional handler override for wrapping with cross-domain
    coordination (e.g., shared chrome, alignment status OOB updates).
    """
```

#### Classes

``` python
class AlignInitResult(NamedTuple):
    """
    Result from pure alignment init handler.
    
    Contains domain-specific data for the combined layer wrapper to use
    when building cross-domain OOB elements (shared chrome, alignment status).
    """
```

#### Variables

``` python
DEBUG_ALIGNMENT = True
```

### helpers (`helpers.ipynb`)

> State getters for the alignment step from InteractionContext

#### Import

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

#### Functions

``` python
def _get_alignment_state(
    ctx:InteractionContext  # Interaction context with state
) -> AlignmentStepState:  # Typed alignment step state
    "Get the full alignment step state from context."
```

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

``` python
def _is_alignment_initialized(
    ctx:InteractionContext  # Interaction context with state
) -> bool:  # True if VAD data has been fetched
    "Check if alignment has been initialized."
```

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

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

``` python
def _get_alignment_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_alignment_card_width(
    ctx:InteractionContext,  # Interaction context with state
    default:int=40,  # Default card width in rem (narrower for alignment)
) -> int:  # Card stack width in rem
    "Get the stored card stack width."
```

``` python
def _get_alignment_history(
    ctx:InteractionContext  # Interaction context with state
) -> list:  # Undo history stack
    "Get the undo history stack."
```

``` 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."
```

``` python
def _get_audio_duration(
    ctx:InteractionContext  # Interaction context with state
) -> Optional[float]:  # Audio duration in seconds or None
    "Get the total audio duration."
```

### html_ids (`html_ids.ipynb`)

> HTML ID constants for Phase 2 Right Column: VAD Alignment

#### Import

``` python
from cjm_transcript_vad_align.html_ids import (
    AlignmentHtmlIds
)
```

#### Classes

``` python
class AlignmentHtmlIds:
    "HTML ID constants for Phase 2 Right Column: VAD Alignment."
    
    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 vad_chunk(
            index:int  # VAD chunk index
        ) -> str:  # HTML ID for the VAD chunk element
        "Generate HTML ID for a VAD chunk element."
```

### init (`init.ipynb`)

> Router assembly for Phase 2 alignment routes

#### Import

``` python
from cjm_transcript_vad_align.routes.init import (
    init_alignment_routers
)
```

#### Functions

``` python
def init_alignment_routers(
    state_store:WorkflowStateStore,  # The workflow state store
    workflow_id:str,  # The workflow identifier
    source_service:SourceService,  # Service for fetching source blocks
    alignment_service:AlignmentService,  # Service for VAD analysis
    prefix:str,  # Base prefix for alignment routes (e.g., "/workflow/align")
    audio_src_url:str,  # URL for audio_src route (from core router)
    wrapped_init:Callable=None,  # Optional wrapped init handler
) -> Tuple[List[APIRouter], AlignmentUrls, Dict[str, Callable]]:  # (routers, urls, merged_routes)
    "Initialize and return all alignment routers with URL bundle."
```

### keyboard_config (`keyboard_config.ipynb`)

> Alignment-specific keyboard building blocks for assembly into a shared
> ZoneManager

#### Import

``` python
from cjm_transcript_vad_align.components.keyboard_config import (
    create_align_kb_parts
)
```

#### Functions

``` python
def create_align_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 alignment-specific keyboard building blocks."
```

### models (`models.ipynb`)

> Data models and URL bundles for the alignment package

#### Import

``` python
from cjm_transcript_vad_align.models import (
    AlignmentStepState,
    VADChunk,
    AlignmentUrls
)
```

#### Classes

``` python
class AlignmentStepState(TypedDict):
    "State for Phase 2 (right column): Temporal Alignment."
```

``` python
@dataclass
class VADChunk:
    "A voice activity detection time range."
    
    index: int  # Chunk index in sequence (global across all audio files)
    start_time: float  # Start time in seconds (relative to its audio file)
    end_time: float  # End time in seconds (relative to its audio file)
    audio_file_index: int = 0  # Which audio file this chunk belongs to
    
    def duration(self) -> float:  # Duration in seconds
            """Calculate chunk duration."""
            return self.end_time - self.start_time
        
        def to_dict(self) -> Dict[str, Any]:  # Dictionary representation
        "Calculate chunk duration."
    
    def to_dict(self) -> Dict[str, Any]:  # Dictionary representation
            """Convert to dictionary for JSON serialization."""
            return asdict(self)
        
        @classmethod
        def from_dict(
            cls,
            data: Dict[str, Any]  # Dictionary representation
        ) -> "VADChunk":  # Reconstructed VADChunk
        "Convert to dictionary for JSON serialization."
    
    def from_dict(
            cls,
            data: Dict[str, Any]  # Dictionary representation
        ) -> "VADChunk":  # Reconstructed VADChunk
        "Create from dictionary."
```

``` python
@dataclass
class AlignmentUrls:
    "URL bundle for Phase 2 alignment route handlers and renderers."
    
    card_stack: CardStackUrls = field(...)
    init: str = ''  # Initialize alignment (fetch VAD data)
    audio_src: str = ''  # Audio file serving URL base
    toggle_auto_nav: str = ''  # Toggle auto-navigate on/off
    speed_change: str = ''  # Persist playback speed changes
```

### step_renderer (`step_renderer.ipynb`)

> Composable render functions for the alignment card stack column

#### Import

``` python
from cjm_transcript_vad_align.components.step_renderer import (
    DEBUG_ALIGN_RENDER,
    render_align_toolbar,
    render_align_stats,
    render_align_source_position,
    render_align_column_body,
    render_align_footer_content,
    render_align_mini_stats_text
)
```

#### Functions

``` python
def render_align_toolbar(
    current_speed:float=1.0,  # Current playback speed
    auto_navigate:bool=False,  # Whether auto-navigate is enabled
    speed_url:str="",  # URL for speed-change POST (server persistence)
    oob:bool=False,  # Whether to render as OOB swap
) -> Any:  # Toolbar component
    "Render the alignment toolbar (speed selector + auto-play toggle)."
```

``` python
def render_align_stats(
    chunks:List[VADChunk],  # Current VAD chunks
    oob:bool=False,  # Whether to render as OOB swap
) -> Any:  # Statistics component
    "Render alignment statistics."
```

``` python
def render_align_source_position(
    chunks:List[VADChunk],  # Current VAD chunks
    focused_index:int=0,  # Currently focused chunk 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 chunk."
```

``` python
def render_align_column_body(
    chunks:List[VADChunk],  # VAD chunks to display
    focused_index:int,  # Currently focused chunk index
    visible_count:int,  # Number of visible cards in viewport
    card_width:int,  # Card stack width in rem
    urls:AlignmentUrls,  # URL bundle for alignment routes
    kb_system:Any=None,  # Rendered keyboard system (None when KB managed externally)
    audio_urls:Optional[List[str]]=None,  # Audio file URLs for Web Audio API
    should_play_fn:str="",  # Consumer-defined play guard function name (overrides default zone guard)
) -> Any:  # Div with id=COLUMN_CONTENT
    "Render the alignment column content area with card stack viewport."
```

``` python
def render_align_footer_content(
    chunks:List[VADChunk],  # Current VAD chunks
    focused_index:int,  # Currently focused chunk index
) -> Any:  # Footer content with progress indicator, source position, and stats
    "Render footer content with progress indicator, source position, and alignment statistics."
```

``` python
def render_align_mini_stats_text(
    chunks:List[VADChunk],  # Current VAD chunks
) -> str:  # Compact stats string for column header badge
    "Generate compact stats string for the alignment column header badge."
```

#### Variables

``` python
DEBUG_ALIGN_RENDER = False
```

### utils (`utils.ipynb`)

> Time formatting utilities for VAD alignment display

#### Import

``` python
from cjm_transcript_vad_align.utils import (
    format_time_precise,
    get_audio_file_boundaries,
    get_audio_file_count,
    get_audio_file_position
)
```

#### Functions

``` python
def format_time_precise(
    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 get_audio_file_boundaries(
    chunks: List["VADChunk"],  # Ordered list of VAD chunks
) -> Set[int]:  # Indices where audio_file_index changes from the previous chunk
    """
    Find indices where audio_file_index changes between adjacent chunks.
    
    A boundary at index N means chunk[N].audio_file_index differs from
    chunk[N-1].audio_file_index.
    """
```

``` python
def get_audio_file_count(
    chunks: List["VADChunk"],  # Ordered list of VAD chunks
) -> int:  # Number of unique audio files
    "Count the number of unique audio files in the chunk list."
```

``` python
def get_audio_file_position(
    chunks: List["VADChunk"],  # Ordered list of VAD chunks
    focused_index: int,  # Index of the focused chunk
) -> Optional[int]:  # 1-based position of the audio file, or None
    """
    Get the audio file position (1-based) of the focused chunk.
    
    Returns which audio file the focused chunk belongs to,
    based on order of first appearance.
    """
```

### vad_card (`vad_card.ipynb`)

> VAD chunk card renderer for the alignment card stack

#### Import

``` python
from cjm_transcript_vad_align.components.vad_card import (
    render_vad_card,
    create_vad_card_renderer
)
```

#### Functions

``` python
def render_vad_card(
    chunk:VADChunk,  # VAD chunk to render
    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:  # VAD chunk card component
    "Render a single VAD chunk card with time range, duration, playing indicator, and play button."
```

``` python
def create_vad_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 VAD chunk cards."
```
