diff --git a/src/cad_documenter/gui_capture.py b/src/cad_documenter/gui_capture.py index 7224b7c..fbb94e4 100644 --- a/src/cad_documenter/gui_capture.py +++ b/src/cad_documenter/gui_capture.py @@ -1,12 +1,14 @@ """ -KB Capture GUI +KB Capture GUI (OBS Mode) -Simple recording interface modeled after Voice Recorder. -Record → Pause → Resume → Stop → Transcribe → Done +Load OBS recordings → Transcribe → Process for KB + +Simple flow: Load Video → Transcribe → Done """ import sys import json +import shutil import threading from pathlib import Path from typing import Optional @@ -16,17 +18,16 @@ from datetime import datetime try: import customtkinter as ctk from customtkinter import CTk, CTkFrame, CTkLabel, CTkButton, CTkEntry - from customtkinter import CTkOptionMenu, CTkScrollableFrame, CTkToplevel + from customtkinter import CTkOptionMenu, CTkToplevel, CTkProgressBar HAS_CTK = True except ImportError: HAS_CTK = False -from .kb_capture import KBCaptureApp, AppState, AppStatus -from .session import SessionType +from .session import SessionManager, Session, SessionType, SessionStatus # ============================================================================ -# THEME (matching Voice Recorder) +# THEME # ============================================================================ COLORS = { @@ -115,7 +116,7 @@ Created: {datetime.now().strftime("%Y-%m-%d")} # ============================================================================ class KBCaptureGUI: - """Main application window.""" + """Main application window - OBS video loading only.""" def __init__(self): if not HAS_CTK: @@ -131,37 +132,33 @@ class KBCaptureGUI: if saved.exists(): self.projects_root = saved - # App - self.app = None - self.recording_indicator_visible = True + # Session manager + self.session_manager = None + + # Processing state + self.is_processing = False # Window ctk.set_appearance_mode("dark") self.window = CTk() self.window.title("KB Capture") - self.window.geometry("400x600") - self.window.minsize(380, 550) + self.window.geometry("420x520") + self.window.minsize(400, 480) self.window.configure(fg_color=COLORS["bg"]) self._build_ui() - # Initialize app if we have a projects root + # Initialize if we have a projects root if self.projects_root: - self._init_app() + self._init_session_manager() self._refresh_projects() - # Start indicator animation - self._update_indicator() - self.window.protocol("WM_DELETE_WINDOW", self._on_close) - def _init_app(self): - """Initialize the capture app.""" - self.app = KBCaptureApp( - projects_root=self.projects_root, - on_status_change=self._on_status_change, - ) + def _init_session_manager(self): + """Initialize session manager.""" + self.session_manager = SessionManager(self.projects_root) def _build_ui(self): """Build the interface.""" @@ -170,12 +167,12 @@ class KBCaptureGUI: # Header header = CTkFrame(main, fg_color="transparent") - header.pack(fill="x", pady=(0, 12)) + header.pack(fill="x", pady=(0, 16)) CTkLabel( header, text="KB Capture", - font=("Segoe UI Semibold", 18), + font=("Segoe UI Semibold", 20), text_color=COLORS["text"], ).pack(side="left") @@ -190,93 +187,48 @@ class KBCaptureGUI: command=self._browse_folder, ).pack(side="right") - # Load video button - CTkButton( - header, - text="📂 Load", - width=60, - height=32, - font=("", 10), - fg_color=COLORS["bg_card"], - hover_color=COLORS["bg_elevated"], - command=self._load_video, - ).pack(side="right", padx=(0, 8)) + # Info card + info_frame = CTkFrame(main, fg_color=COLORS["bg_card"], corner_radius=10) + info_frame.pack(fill="x", pady=(0, 16)) - # Timer card - timer_frame = CTkFrame(main, fg_color=COLORS["bg_card"], corner_radius=10) - timer_frame.pack(fill="x", pady=(0, 12)) + info_inner = CTkFrame(info_frame, fg_color="transparent") + info_inner.pack(pady=16, padx=16, fill="x") - timer_inner = CTkFrame(timer_frame, fg_color="transparent") - timer_inner.pack(pady=20) + CTkLabel( + info_inner, + text="📹 Record with OBS, then load here", + font=("Segoe UI Semibold", 13), + text_color=COLORS["text"], + ).pack(anchor="w") - # Recording indicator + timer - timer_row = CTkFrame(timer_inner, fg_color="transparent") - timer_row.pack() - - self.indicator = CTkLabel( - timer_row, - text="", - font=("", 10), - text_color=COLORS["red"], - ) - self.indicator.pack(side="left", padx=(0, 4)) - - self.timer_label = CTkLabel( - timer_row, - text="00:00:00", - font=("Consolas", 36, "bold"), - text_color=COLORS["text_muted"], - ) - self.timer_label.pack(side="left") - - self.status_label = CTkLabel( - timer_inner, - text="Select a project to start", + CTkLabel( + info_inner, + text="Video → Transcribe → Ready for Clawdbot", font=("", 11), text_color=COLORS["text_secondary"], - ) - self.status_label.pack(pady=(8, 0)) + ).pack(anchor="w", pady=(4, 0)) - # Recording info (what's being captured) - self.capture_info_label = CTkLabel( - timer_inner, + # Status area + self.status_label = CTkLabel( + info_inner, text="", - font=("", 9), + font=("", 11), text_color=COLORS["text_muted"], ) - self.capture_info_label.pack(pady=(4, 0)) + self.status_label.pack(anchor="w", pady=(8, 0)) - # Recording controls - controls = CTkFrame(main, fg_color="transparent") - controls.pack(fill="x", pady=12) - controls.grid_columnconfigure((0, 1), weight=1) - - self.record_btn = CTkButton( - controls, - text="Record", - height=45, - font=("Segoe UI Semibold", 12), - fg_color=COLORS["red"], - hover_color="#dc2626", - state="disabled", - command=self._toggle_recording, - ) - self.record_btn.grid(row=0, column=0, padx=(0, 6), sticky="ew") - - self.pause_btn = CTkButton( - controls, - text="Pause", - height=45, - font=("", 12), + # Progress bar (hidden by default) + self.progress = CTkProgressBar( + info_inner, + width=300, + height=6, fg_color=COLORS["border"], - hover_color=COLORS["bg_elevated"], - state="disabled", - command=self._toggle_pause, + progress_color=COLORS["blue"], ) - self.pause_btn.grid(row=0, column=1, padx=(6, 0), sticky="ew") + # Don't pack yet - will show during processing # Separator - CTkFrame(main, fg_color=COLORS["border"], height=1).pack(fill="x", pady=12) + CTkFrame(main, fg_color=COLORS["border"], height=1).pack(fill="x", pady=8) # Project selector proj_frame = CTkFrame(main, fg_color="transparent") @@ -306,7 +258,7 @@ class KBCaptureGUI: self.project_menu = CTkOptionMenu( proj_frame, values=["(Select folder first)"], - width=340, + width=360, height=35, fg_color=COLORS["bg_card"], button_color=COLORS["bg_elevated"], @@ -324,7 +276,7 @@ class KBCaptureGUI: self.name_entry = CTkEntry( main, - placeholder_text="What are you working on?", + placeholder_text="e.g., Vertical support walkthrough", height=35, fg_color=COLORS["bg_card"], border_color=COLORS["border"], @@ -332,15 +284,22 @@ class KBCaptureGUI: self.name_entry.pack(fill="x") # Session type + CTkLabel( + main, + text="Session Type", + font=("Segoe UI Semibold", 11), + text_color=COLORS["text_secondary"], + ).pack(anchor="w", pady=(16, 8)) + type_frame = CTkFrame(main, fg_color="transparent") - type_frame.pack(fill="x", pady=(12, 0)) + type_frame.pack(fill="x") type_frame.grid_columnconfigure((0, 1), weight=1) self.type_var = ctk.StringVar(value="design") self.design_btn = CTkButton( type_frame, - text="🎨 Design", + text="🎨 Design → KB/Design/", height=40, font=("", 11), fg_color=COLORS["blue"], @@ -351,7 +310,7 @@ class KBCaptureGUI: self.analysis_btn = CTkButton( type_frame, - text="📊 Analysis", + text="📊 Analysis → KB/Analysis/", height=40, font=("", 11), fg_color=COLORS["border"], @@ -360,71 +319,42 @@ class KBCaptureGUI: ) self.analysis_btn.grid(row=0, column=1, padx=(4, 0), sticky="ew") - # Screen selector (for multi-monitor setups) - screen_frame = CTkFrame(main, fg_color="transparent") - screen_frame.pack(fill="x", pady=(12, 0)) - - CTkLabel( - screen_frame, - text="Capture", - font=("Segoe UI Semibold", 11), - text_color=COLORS["text_secondary"], - ).pack(side="left") - - # Get available screens - self.screens = self._get_screens() - screen_values = ["All Screens"] + [f"Screen {i+1}" for i in range(len(self.screens))] - - self.screen_menu = CTkOptionMenu( - screen_frame, - values=screen_values, - width=120, - height=28, - font=("", 10), - fg_color=COLORS["bg_card"], - button_color=COLORS["bg_elevated"], - button_hover_color=COLORS["border"], - ) - self.screen_menu.pack(side="right") - self.screen_menu.set("All Screens") - # Spacer CTkFrame(main, fg_color="transparent").pack(fill="both", expand=True) + # Load button (main action) + self.load_btn = CTkButton( + main, + text="📂 Load OBS Recording", + height=50, + font=("Segoe UI Semibold", 14), + fg_color=COLORS["blue"], + hover_color="#2563eb", + state="disabled", + command=self._load_video, + ) + self.load_btn.pack(fill="x", pady=(16, 0)) + + # Recent sessions link + self.recent_label = CTkLabel( + main, + text="", + font=("", 10), + text_color=COLORS["text_muted"], + cursor="hand2", + ) + self.recent_label.pack(pady=(12, 0)) + # Folder path self.folder_label = CTkLabel( main, - text=str(self.projects_root) if self.projects_root else "No folder selected", + text=str(self.projects_root) if self.projects_root else "Click 📁 to select projects folder", font=("", 9), text_color=COLORS["text_muted"], cursor="hand2", ) - self.folder_label.pack(pady=(12, 0)) + self.folder_label.pack(pady=(8, 0)) self.folder_label.bind("", lambda e: self._browse_folder()) - - # Bottom row: hints + reset - bottom_row = CTkFrame(main, fg_color="transparent") - bottom_row.pack(fill="x", pady=(8, 0)) - - CTkLabel( - bottom_row, - text="Ctrl+Shift+R: Record/Stop", - font=("", 9), - text_color=COLORS["text_muted"], - ).pack(side="left") - - # Reset button (hidden until needed) - self.reset_btn = CTkButton( - bottom_row, - text="🔄 Reset", - width=60, - height=24, - font=("", 9), - fg_color=COLORS["red"], - hover_color="#dc2626", - command=self._force_reset, - ) - # Don't pack initially - will show when stuck def _set_type(self, type_id: str): """Set session type.""" @@ -432,72 +362,16 @@ class KBCaptureGUI: if type_id == "design": self.design_btn.configure(fg_color=COLORS["blue"]) self.analysis_btn.configure(fg_color=COLORS["border"]) - self.status_label.configure(text="Design session → KB/Design/", text_color=COLORS["blue"]) else: self.design_btn.configure(fg_color=COLORS["border"]) self.analysis_btn.configure(fg_color=COLORS["orange"]) - self.status_label.configure(text="Analysis session → KB/Analysis/", text_color=COLORS["orange"]) - - def _get_screens(self) -> list: - """Get list of available screens.""" - screens = [] - try: - if sys.platform == "win32": - import ctypes - user32 = ctypes.windll.user32 - - # EnumDisplayMonitors callback - def callback(hMonitor, hdcMonitor, lprcMonitor, dwData): - screens.append({ - "handle": hMonitor, - "left": lprcMonitor.contents.left, - "top": lprcMonitor.contents.top, - "right": lprcMonitor.contents.right, - "bottom": lprcMonitor.contents.bottom, - }) - return True - - # Define RECT structure - class RECT(ctypes.Structure): - _fields_ = [ - ("left", ctypes.c_long), - ("top", ctypes.c_long), - ("right", ctypes.c_long), - ("bottom", ctypes.c_long), - ] - - MONITORENUMPROC = ctypes.WINFUNCTYPE( - ctypes.c_bool, ctypes.c_ulong, ctypes.c_ulong, - ctypes.POINTER(RECT), ctypes.c_double - ) - - user32.EnumDisplayMonitors(None, None, MONITORENUMPROC(callback), 0) - except Exception as e: - print(f"Error getting screens: {e}") - - return screens if screens else [{"name": "Primary"}] - - def _update_indicator(self): - """Animate recording indicator.""" - if self.app and self.app.state == AppState.RECORDING: - self.recording_indicator_visible = not self.recording_indicator_visible - self.indicator.configure( - text="●" if self.recording_indicator_visible else "", - text_color=COLORS["red"], - ) - elif self.app and self.app.state == AppState.PAUSED: - self.indicator.configure(text="●", text_color=COLORS["orange"]) - else: - self.indicator.configure(text="") - - self.window.after(500, self._update_indicator) def _browse_folder(self): """Browse for projects folder.""" initial = str(self.projects_root) if self.projects_root else str(Path.home()) folder = filedialog.askdirectory( - title="Select Projects Folder", + title="Select Projects Folder (e.g., ATODrive/Projects)", initialdir=initial, ) @@ -508,25 +382,42 @@ class KBCaptureGUI: self.config["projects_root"] = str(self.projects_root) save_config(self.config) - self._init_app() + self._init_session_manager() self._refresh_projects() def _refresh_projects(self): """Refresh project list.""" - if not self.app: + if not self.session_manager: self.project_menu.configure(values=["(Select folder first)"]) return - projects = self.app.session_manager.list_projects() + projects = self.session_manager.list_projects() if projects: self.project_menu.configure(values=projects) - self.project_menu.set(projects[0]) - self.record_btn.configure(state="normal") - self.status_label.configure(text="Ready to record") + # Try to restore last used project + last_project = self.config.get("last_project") + if last_project in projects: + self.project_menu.set(last_project) + else: + self.project_menu.set(projects[0]) + self.load_btn.configure(state="normal") + self._update_recent() else: self.project_menu.configure(values=["(No projects - click + New)"]) - self.record_btn.configure(state="disabled") - self.status_label.configure(text="Create a project first") + self.load_btn.configure(state="disabled") + + def _update_recent(self): + """Update recent sessions count.""" + project = self.project_menu.get() + if project.startswith("("): + self.recent_label.configure(text="") + return + + sessions = self.session_manager.list_sessions(project) + if sessions: + self.recent_label.configure(text=f"📋 {len(sessions)} previous sessions") + else: + self.recent_label.configure(text="No sessions yet") def _new_project(self): """Create new project.""" @@ -605,13 +496,8 @@ class KBCaptureGUI: name_entry.bind("", lambda e: create()) def _load_video(self): - """Load an existing video file for processing.""" - if not self.app: - messagebox.showwarning("No Folder", "Select a projects folder first") - return - - if self.app.state != AppState.IDLE: - messagebox.showwarning("Busy", "Stop current recording first") + """Load an OBS recording for processing.""" + if self.is_processing: return project = self.project_menu.get() @@ -619,11 +505,18 @@ class KBCaptureGUI: messagebox.showwarning("No Project", "Select a project first") return + # Save last project + self.config["last_project"] = project + save_config(self.config) + # Ask for video file + initial_dir = self.config.get("last_video_dir", str(Path.home() / "Videos")) + video_path = filedialog.askopenfilename( - title="Select Video to Process", + title="Select OBS Recording", + initialdir=initial_dir, filetypes=[ - ("Video files", "*.mp4 *.mkv *.avi *.mov *.webm"), + ("Video files", "*.mp4 *.mkv *.avi *.mov *.webm *.flv"), ("All files", "*.*"), ], ) @@ -636,199 +529,181 @@ class KBCaptureGUI: messagebox.showerror("Error", "File not found") return + # Save last video directory + self.config["last_video_dir"] = str(video_path.parent) + save_config(self.config) + + # Get session name name = self.name_entry.get().strip() or video_path.stem session_type = SessionType.DESIGN if self.type_var.get() == "design" else SessionType.ANALYSIS - # Create session folder and copy/link video - import shutil - session = self.app.session_manager.start_session(name, project, session_type) - session_dir = self.app.session_manager.get_session_dir() - - # Copy video to session folder - dest_video = session_dir / "recording.mp4" - self.status_label.configure(text="Copying video...", text_color=COLORS["orange"]) - self.window.update() + # Start processing + self.is_processing = True + self._set_processing_ui(True, "Copying video...") + # Run in background + threading.Thread( + target=self._process_video, + args=(video_path, name, project, session_type), + daemon=True, + ).start() + + def _process_video(self, video_path: Path, name: str, project: str, session_type: SessionType): + """Process video in background thread.""" try: + # Create session + session = self.session_manager.start_session(name, project, session_type) + session_dir = self.session_manager.get_session_dir() + + # Copy video + dest_video = session_dir / "recording.mp4" + self._update_status("Copying video...") shutil.copy2(video_path, dest_video) + + # Transcribe + self._update_status("Loading Whisper model...") + self._transcribe(dest_video, session) + except Exception as e: - messagebox.showerror("Error", f"Failed to copy video: {e}") - self.app.session_manager.cancel_session() - return - - # Now transcribe - self.status_label.configure(text="Transcribing...", text_color=COLORS["orange"]) - self.record_btn.configure(state="disabled") - self.window.update() - - # Run transcription in background - def do_transcribe(): - self.app._transcribe(dest_video) - - self.app.state = AppState.TRANSCRIBING - threading.Thread(target=do_transcribe, daemon=True).start() + self._update_status(f"Error: {e}", error=True) + if self.session_manager.current_session: + self.session_manager.cancel_session() + finally: + self.is_processing = False + self.window.after(0, lambda: self._set_processing_ui(False)) - def _toggle_recording(self): - """Start or stop recording.""" - if not self.app: - return - - if self.app.state == AppState.IDLE: - self._start_recording() - elif self.app.state in (AppState.RECORDING, AppState.PAUSED): - self._stop_recording() - - def _start_recording(self): - """Start recording session.""" - # Prevent double-click - if self.app and self.app.state != AppState.IDLE: - return - - project = self.project_menu.get() - if project.startswith("("): - messagebox.showwarning("No Project", "Select a project first") - return - - name = self.name_entry.get().strip() or f"Session {datetime.now().strftime('%H:%M')}" - session_type = SessionType.DESIGN if self.type_var.get() == "design" else SessionType.ANALYSIS - - # Disable Record button immediately to prevent double-click - self.record_btn.configure(state="disabled", text="Starting...") - self.window.update() - + def _transcribe(self, video_path: Path, session: Session): + """Transcribe video with Whisper.""" try: - # Get selected screen - screen_selection = self.screen_menu.get() - screen_index = None - if screen_selection != "All Screens": - try: - screen_index = int(screen_selection.split()[-1]) - 1 - except: - pass + import whisper - self.app.start_session(name, project, session_type, screen_index=screen_index) + self._update_status("Transcribing (this takes a while)...") + model = whisper.load_model("base") - # Update UI for recording state - self.record_btn.configure(text="Stop", fg_color=COLORS["border"], state="normal") - self.pause_btn.configure(state="normal", fg_color=COLORS["orange"]) + # Whisper can handle video files directly + result = model.transcribe(str(video_path), language="en", verbose=False) + + # Save transcript + transcript_path = video_path.parent / "transcript.json" + with open(transcript_path, "w") as f: + json.dump(result, f, indent=2) + + # Find screenshot triggers + triggers = [] + for segment in result.get("segments", []): + text = segment.get("text", "").lower() + if "screenshot" in text: + triggers.append({ + "timestamp": segment.get("start", 0), + "text": segment.get("text", ""), + }) + + # Get video duration + duration = self._get_video_duration(video_path) + + # Save metadata + metadata = { + "session_id": session.id, + "name": session.name, + "project": session.project, + "session_type": session.session_type.value, + "created_at": session.created_at.isoformat(), + "duration": duration, + "status": "ready", + "screenshot_triggers": triggers, + "source_file": video_path.name, + "files": { + "video": "recording.mp4", + "transcript": "transcript.json", + }, + } + + metadata_path = video_path.parent / "metadata.json" + with open(metadata_path, "w") as f: + json.dump(metadata, f, indent=2) + + # Update session + self.session_manager.set_duration(duration) + self.session_manager.set_transcript("transcript.json") + self.session_manager.end_session() + + self._update_status(f"✅ Done! {len(triggers)} screenshot triggers found", success=True) + self.window.after(0, self._update_recent) + + except ImportError: + self._update_status("⚠️ Saved (Whisper not installed)", warning=True) + self.session_manager.end_session() + except Exception as e: + error_msg = str(e) + if "audio" in error_msg.lower(): + self._update_status("⚠️ Saved (video has no audio track)", warning=True) + # Still save the session without transcript + self.session_manager.end_session() + else: + raise + + def _get_video_duration(self, video_path: Path) -> float: + """Get video duration using ffprobe.""" + try: + import subprocess + result = subprocess.run( + [ + "ffprobe", "-v", "error", + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", + str(video_path), + ], + capture_output=True, + text=True, + timeout=30, + ) + return float(result.stdout.strip()) + except: + return 0.0 + + def _update_status(self, text: str, error: bool = False, success: bool = False, warning: bool = False): + """Update status label from any thread.""" + def update(): + if error: + color = COLORS["red"] + elif success: + color = COLORS["green"] + elif warning: + color = COLORS["orange"] + else: + color = COLORS["text_secondary"] + self.status_label.configure(text=text, text_color=color) + + self.window.after(0, update) + + def _set_processing_ui(self, processing: bool, status: str = ""): + """Update UI for processing state.""" + if processing: + self.load_btn.configure(state="disabled", text="Processing...") self.project_menu.configure(state="disabled") self.name_entry.configure(state="disabled") self.design_btn.configure(state="disabled") self.analysis_btn.configure(state="disabled") - self.screen_menu.configure(state="disabled") - self.timer_label.configure(text_color=COLORS["text"]) - self.status_label.configure(text="Recording...", text_color=COLORS["red"]) - - # Show what's being captured - capture_info = f"🖥️ {screen_selection}" - audio_devices = self.app.recorder.list_audio_devices() if hasattr(self.app, 'recorder') else [] - if audio_devices: - capture_info += f" + 🎤 {audio_devices[0][:20]}" - else: - capture_info += " (no mic)" - self.capture_info_label.configure(text=capture_info) - - except Exception as e: - # Reset UI on failure - self._reset_ui_to_idle() - messagebox.showerror("Recording Failed", str(e)) - - def _reset_ui_to_idle(self): - """Reset UI to idle state (for error recovery).""" - self.record_btn.configure(text="Record", fg_color=COLORS["red"], state="normal") - self.pause_btn.configure(state="disabled", text="Pause", fg_color=COLORS["border"]) - self.project_menu.configure(state="normal") - self.name_entry.configure(state="normal") - self.design_btn.configure(state="normal") - self.analysis_btn.configure(state="normal") - self.screen_menu.configure(state="normal") - self.timer_label.configure(text="00:00:00", text_color=COLORS["text_muted"]) - self.capture_info_label.configure(text="") - self.status_label.configure(text="Ready to record", text_color=COLORS["text_secondary"]) - # Hide reset button - self.reset_btn.pack_forget() - - def _show_reset_button(self): - """Show reset button when stuck.""" - self.reset_btn.pack(side="right") - - def _force_reset(self): - """Force reset the app to idle state.""" - if self.app: - self.app.force_reset() - self._reset_ui_to_idle() - self.status_label.configure(text="Reset complete - ready to record", text_color=COLORS["green"]) - - def _stop_recording(self): - """Stop recording and transcribe.""" - if not self.app: - return - - self.app.stop() - - self.record_btn.configure(text="Record", fg_color=COLORS["red"], state="disabled") - self.pause_btn.configure(state="disabled", text="Pause", fg_color=COLORS["border"]) - self.timer_label.configure(text_color=COLORS["green"]) - self.status_label.configure(text="Transcribing...", text_color=COLORS["orange"]) - - def _toggle_pause(self): - """Toggle pause/resume.""" - if not self.app: - return - - self.app.toggle_pause() - - if self.app.state == AppState.PAUSED: - self.pause_btn.configure(text="Resume", fg_color=COLORS["green"]) - self.timer_label.configure(text_color=COLORS["orange"]) - self.status_label.configure(text="Paused", text_color=COLORS["orange"]) + self.progress.pack(fill="x", pady=(8, 0)) + self.progress.configure(mode="indeterminate") + self.progress.start() + if status: + self.status_label.configure(text=status, text_color=COLORS["text_secondary"]) else: - self.pause_btn.configure(text="Pause", fg_color=COLORS["orange"]) - self.timer_label.configure(text_color=COLORS["text"]) - self.status_label.configure(text="Recording...", text_color=COLORS["red"]) - - def _on_status_change(self, status: AppStatus): - """Handle status updates.""" - self.window.after(0, lambda: self._update_ui(status)) - - def _update_ui(self, status: AppStatus): - """Update UI from status.""" - # Timer - if status.state in (AppState.RECORDING, AppState.PAUSED): - secs = int(status.duration) - hours = secs // 3600 - mins = (secs % 3600) // 60 - secs = secs % 60 - self.timer_label.configure(text=f"{hours:02d}:{mins:02d}:{secs:02d}") - - # Back to idle (including after errors) - if status.state == AppState.IDLE: - self._reset_ui_to_idle() + self.load_btn.configure(state="normal", text="📂 Load OBS Recording") + self.project_menu.configure(state="normal") + self.name_entry.configure(state="normal") + self.design_btn.configure(state="normal") + self.analysis_btn.configure(state="normal") + self.progress.stop() + self.progress.pack_forget() self.name_entry.delete(0, "end") - # Keep the timer showing final time if we just finished - if "Stopped" in status.message or "Done" in status.message: - self.timer_label.configure(text_color=COLORS["green"]) - - # Status message - if status.message: - # Color code the message - if "failed" in status.message.lower() or "error" in status.message.lower(): - self.status_label.configure(text=status.message, text_color=COLORS["red"]) - # Show reset button on errors - self._show_reset_button() - elif "done" in status.message.lower() or "saved" in status.message.lower(): - self.status_label.configure(text=status.message, text_color=COLORS["green"]) - else: - self.status_label.configure(text=status.message, text_color=COLORS["text_secondary"]) def _on_close(self): """Handle window close.""" - if self.app and self.app.state != AppState.IDLE: - if messagebox.askyesno("Recording Active", "Cancel current recording?"): - self.app.cancel() - else: + if self.is_processing: + if not messagebox.askyesno("Processing", "Video is being processed. Close anyway?"): return - self.window.destroy() def run(self):