""" Video Annotation Layout Generates a form interface for video annotation with: - Temporal segment marking (like audio annotation) - Frame-level classification - Keyframe annotation - Object tracking across frames (basic support) Uses Peaks.js for timeline visualization with synchronized video playback. Features: - Waveform/timeline visualization from video's audio track - Video preview panel with frame counter - Segment creation and labeling - Frame-by-frame stepping (,/. keys) - Multiple playback speeds (0.1x to 2.0x) - Keyframe marking (K key) """ import logging import json from typing import List, Dict, Tuple, Any from .identifier_utils import ( safe_generate_layout, escape_html_content ) logger = logging.getLogger(__name__) # Default colors for segment/frame labels DEFAULT_COLORS = [ "#4ECDC4", # Teal "#FF6B6B", # Red "#45B7D1", # Blue "#96CEB4", # Green "#FFEAA7", # Yellow "#DDA0DD", # Plum "#95A5A6", # Gray "#F39C12", # Orange "#9B59B6", # Purple "#3498DB", # Light Blue ] # Valid annotation modes VALID_MODES = ["segment", "frame", "keyframe", "tracking", "combined"] def generate_video_annotation_layout(annotation_scheme: Dict[str, Any]) -> Tuple[str, List[Tuple[str, str]]]: """ Generate HTML for a video annotation interface. Args: annotation_scheme (dict): Configuration including: - name: Schema identifier - description: Display description - mode: "segment" | "frame" | "keyframe" | "tracking" | "combined" - labels: List of labels (for segment/frame/keyframe modes) - segment_schemes: List of annotation schemes per segment (optional) - min_segments: Minimum required segments (default: 0) - max_segments: Maximum allowed segments (default: null/unlimited) - timeline_height: Height of timeline in pixels (default: 70) - overview_height: Height of overview bar (default: 40) - zoom_enabled: Whether to enable zoom (default: True) - playback_rate_control: Show playback speed controls (default: True) - frame_stepping: Enable frame-by-frame navigation (default: True) - show_timecode: Show timecode display (default: True) - video_fps: Frames per second for frame calculations (default: 30) Returns: tuple: (html_string, key_bindings) html_string: Complete HTML for the video annotation interface key_bindings: List of keyboard shortcuts Raises: ValueError: If required fields are missing or invalid """ return safe_generate_layout(annotation_scheme, _generate_video_annotation_layout_internal) def _generate_video_annotation_layout_internal(annotation_scheme: Dict[str, Any]) -> Tuple[str, List[Tuple[str, str]]]: """ Internal function to generate video annotation layout after validation. """ schema_name = annotation_scheme.get('name', 'video_annotation') logger.debug(f"Generating video annotation layout for schema: {schema_name}") # Get mode (default to "segment") mode = annotation_scheme.get('mode', 'segment') if mode not in VALID_MODES: error_msg = f"Invalid mode '{mode}' in schema: {schema_name}. Must be one of: {VALID_MODES}" logger.error(error_msg) raise ValueError(error_msg) # Validate labels for segment/frame/keyframe/tracking/combined modes labels = [] if mode in ['segment', 'frame', 'keyframe', 'tracking', 'combined']: if 'labels' not in annotation_scheme: error_msg = f"Missing labels in schema: {schema_name} (required for mode '{mode}')" logger.error(error_msg) raise ValueError(error_msg) labels = _process_labels(annotation_scheme['labels']) # Validate segment_schemes for combined mode segment_schemes = [] if mode in ['combined'] and 'segment_schemes' in annotation_scheme: segment_schemes = annotation_scheme['segment_schemes'] if not isinstance(segment_schemes, list): error_msg = f"segment_schemes must be a list in schema: {schema_name}" logger.error(error_msg) raise ValueError(error_msg) # Get configuration options min_segments = annotation_scheme.get('min_segments', 0) max_segments = annotation_scheme.get('max_segments', None) timeline_height = annotation_scheme.get('timeline_height', 70) overview_height = annotation_scheme.get('overview_height', 40) zoom_enabled = annotation_scheme.get('zoom_enabled', True) playback_rate_control = annotation_scheme.get('playback_rate_control', True) frame_stepping = annotation_scheme.get('frame_stepping', True) show_timecode = annotation_scheme.get('show_timecode', True) video_fps = annotation_scheme.get('video_fps', 30) # AI support configuration ai_support = annotation_scheme.get("ai_support", {}) ai_enabled = ai_support.get("enabled", False) # source_field: Links this annotation schema to a display field from instance_display source_field = annotation_scheme.get("source_field", "") # Build config object for JavaScript js_config = { "schemaName": schema_name, "mode": mode, "labels": labels, "segmentSchemes": segment_schemes, "minSegments": min_segments, "maxSegments": max_segments, "timelineHeight": timeline_height, "overviewHeight": overview_height, "zoomEnabled": zoom_enabled, "playbackRateControl": playback_rate_control, "frameStepping": frame_stepping, "showTimecode": show_timecode, "videoFps": video_fps, "aiSupport": ai_enabled, "aiFeatures": ai_support.get("features", {}) if ai_enabled else {}, "sourceField": source_field, } # Generate HTML html = _generate_html(annotation_scheme, js_config, schema_name, labels, mode, ai_enabled, ai_support) # Generate keybindings keybindings = _generate_keybindings(labels, mode, frame_stepping) logger.info(f"Successfully generated video annotation layout for {schema_name}") return html, keybindings def _process_labels(labels_config: List) -> List[Dict[str, Any]]: """ Process label configuration and assign colors. Args: labels_config: List of label configs (strings or dicts) Returns: List of processed label dicts with name, color, and optional key_value """ processed = [] for i, label in enumerate(labels_config): if isinstance(label, str): processed.append({ "name": label, "color": DEFAULT_COLORS[i % len(DEFAULT_COLORS)], }) elif isinstance(label, dict): processed.append({ "name": label.get("name", f"label_{i}"), "color": label.get("color", DEFAULT_COLORS[i % len(DEFAULT_COLORS)]), "key_value": label.get("key_value"), }) else: processed.append({ "name": str(label), "color": DEFAULT_COLORS[i % len(DEFAULT_COLORS)], }) return processed def _generate_html( annotation_scheme: Dict[str, Any], js_config: Dict[str, Any], schema_name: str, labels: List[Dict[str, Any]], mode: str, ai_enabled: bool = False, ai_support: Dict[str, Any] = None ) -> str: """ Generate the HTML for the video annotation interface. """ escaped_name = escape_html_content(schema_name) description = escape_html_content(annotation_scheme.get('description', '')) config_json = json.dumps(js_config) timeline_height = js_config.get('timelineHeight', 70) overview_height = js_config.get('overviewHeight', 40) # Generate label buttons label_selector = "" if labels: label_selector = _generate_label_selector(labels) # Generate AI toolbar if enabled ai_toolbar_html = "" ai_init_script = "" if ai_enabled: ai_features = ai_support.get("features", {}) if ai_support else {} ai_toolbar_html = _generate_video_ai_toolbar(ai_features, mode) ai_init_script = _generate_video_ai_init_script(escaped_name) # Generate playback rate control playback_rate_html = "" if js_config.get('playbackRateControl'): playback_rate_html = '''