feat(gui): Add topic field and smart video naming

- Added AddVideoDialog for entering session topic when adding videos
- Videos now renamed to: gen-{num}_{topic}_{timestamp}.ext
- Shows next generation number in UI
- Shows project info (total videos, pending, processed)
- Better error handling and user feedback
- Cleaner layout with sections

When adding a video, user can enter a topic like 'lateral-support-detail'
and the video is automatically renamed to a standardized format.
This commit is contained in:
Mario Lavoie
2026-01-28 15:19:14 +00:00
parent e8cca0b9c5
commit ca01c7a944

View File

@@ -5,6 +5,9 @@ from tkinter import ttk, filedialog, messagebox
from pathlib import Path from pathlib import Path
import threading import threading
import json import json
import shutil
import re
from datetime import datetime
try: try:
import customtkinter as ctk import customtkinter as ctk
@@ -20,13 +23,136 @@ from .incremental import IncrementalProcessor
from .config import load_config from .config import load_config
def slugify(text: str) -> str:
"""Convert text to a filename-safe slug."""
text = text.lower().strip()
text = re.sub(r'[^\w\s-]', '', text)
text = re.sub(r'[\s_-]+', '-', text)
return text[:50] # Limit length
class AddVideoDialog:
"""Dialog for adding a video with topic."""
def __init__(self, parent, video_path: Path, gen_number: int):
self.result = None
self.video_path = video_path
self.gen_number = gen_number
self.dialog = ctk.CTkToplevel(parent) if USE_CTK else tk.Toplevel(parent)
self.dialog.title("Add Video")
self.dialog.geometry("500x250")
self.dialog.transient(parent)
self.dialog.grab_set()
# Make modal
self.dialog.focus_set()
# Center on parent
self.dialog.update_idletasks()
x = parent.winfo_x() + (parent.winfo_width() - 500) // 2
y = parent.winfo_y() + (parent.winfo_height() - 250) // 2
self.dialog.geometry(f"+{x}+{y}")
self._create_widgets()
def _create_widgets(self):
frame = ctk.CTkFrame(self.dialog) if USE_CTK else ttk.Frame(self.dialog)
frame.pack(fill="both", expand=True, padx=20, pady=20)
# Video info
info_text = f"Adding: {self.video_path.name}"
if USE_CTK:
ctk.CTkLabel(frame, text=info_text, wraplength=450).pack(anchor="w", pady=(0, 10))
else:
ttk.Label(frame, text=info_text, wraplength=450).pack(anchor="w", pady=(0, 10))
# Generation number
gen_text = f"This will be: Generation {self.gen_number:03d}"
if USE_CTK:
ctk.CTkLabel(frame, text=gen_text, text_color="gray").pack(anchor="w", pady=(0, 15))
else:
ttk.Label(frame, text=gen_text).pack(anchor="w", pady=(0, 15))
# Topic input
if USE_CTK:
ctk.CTkLabel(frame, text="Session Topic (optional):").pack(anchor="w")
else:
ttk.Label(frame, text="Session Topic (optional):").pack(anchor="w")
self.topic_var = tk.StringVar()
if USE_CTK:
self.topic_entry = ctk.CTkEntry(frame, textvariable=self.topic_var, width=400,
placeholder_text="e.g., lateral-support-detail, stage2-joints")
else:
self.topic_entry = ttk.Entry(frame, textvariable=self.topic_var, width=50)
self.topic_entry.pack(fill="x", pady=(5, 10))
self.topic_entry.focus_set()
# Preview
self.preview_var = tk.StringVar()
self._update_preview()
self.topic_var.trace_add("write", lambda *args: self._update_preview())
if USE_CTK:
ctk.CTkLabel(frame, text="Will be saved as:").pack(anchor="w", pady=(10, 0))
self.preview_label = ctk.CTkLabel(frame, textvariable=self.preview_var, text_color="cyan")
else:
ttk.Label(frame, text="Will be saved as:").pack(anchor="w", pady=(10, 0))
self.preview_label = ttk.Label(frame, textvariable=self.preview_var)
self.preview_label.pack(anchor="w")
# Buttons
btn_frame = ctk.CTkFrame(frame) if USE_CTK else ttk.Frame(frame)
btn_frame.pack(fill="x", pady=(20, 0))
if USE_CTK:
ctk.CTkButton(btn_frame, text="Cancel", width=100, command=self._cancel).pack(side="right", padx=5)
ctk.CTkButton(btn_frame, text="Add Video", width=100, command=self._confirm).pack(side="right", padx=5)
else:
ttk.Button(btn_frame, text="Cancel", command=self._cancel).pack(side="right", padx=5)
ttk.Button(btn_frame, text="Add Video", command=self._confirm).pack(side="right", padx=5)
# Bind Enter key
self.dialog.bind("<Return>", lambda e: self._confirm())
self.dialog.bind("<Escape>", lambda e: self._cancel())
def _update_preview(self):
topic = self.topic_var.get().strip()
topic_slug = slugify(topic) if topic else "session"
timestamp = datetime.now().strftime("%Y%m%d-%H%M")
new_name = f"gen-{self.gen_number:03d}_{topic_slug}_{timestamp}{self.video_path.suffix}"
self.preview_var.set(new_name)
def _confirm(self):
topic = self.topic_var.get().strip()
topic_slug = slugify(topic) if topic else "session"
timestamp = datetime.now().strftime("%Y%m%d-%H%M")
new_name = f"gen-{self.gen_number:03d}_{topic_slug}_{timestamp}{self.video_path.suffix}"
self.result = {
"topic": topic,
"new_filename": new_name,
"gen_number": self.gen_number
}
self.dialog.destroy()
def _cancel(self):
self.result = None
self.dialog.destroy()
def show(self):
self.dialog.wait_window()
return self.result
class CADDocumenterGUI: class CADDocumenterGUI:
"""Main GUI application.""" """Main GUI application."""
def __init__(self): def __init__(self):
self.root = ctk.CTk() if USE_CTK else tk.Tk() self.root = ctk.CTk() if USE_CTK else tk.Tk()
self.root.title("CAD Documenter") self.root.title("CAD Documenter")
self.root.geometry("700x600") self.root.geometry("750x650")
self.current_project: Project | None = None self.current_project: Project | None = None
self.config = load_config() self.config = load_config()
@@ -40,45 +166,55 @@ class CADDocumenterGUI:
main_frame = ctk.CTkFrame(self.root) if USE_CTK else ttk.Frame(self.root) main_frame = ctk.CTkFrame(self.root) if USE_CTK else ttk.Frame(self.root)
main_frame.pack(fill="both", expand=True, padx=10, pady=10) main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Project selection # === Project Section ===
project_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Project") project_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Project")
project_frame.pack(fill="x", pady=(0, 10)) project_frame.pack(fill="x", pady=(0, 10))
if USE_CTK: if USE_CTK:
ctk.CTkLabel(project_frame, text="Project:").pack(side="left", padx=5) ctk.CTkLabel(project_frame, text="Project:", font=("", 14, "bold")).pack(anchor="w", padx=10, pady=(10, 5))
else:
ttk.Label(project_frame, text="Project:").pack(side="left", padx=5) proj_row = ctk.CTkFrame(project_frame) if USE_CTK else ttk.Frame(project_frame)
proj_row.pack(fill="x", padx=10, pady=(0, 10))
self.project_var = tk.StringVar() self.project_var = tk.StringVar()
self.project_combo = ctk.CTkComboBox( self.project_combo = ctk.CTkComboBox(
project_frame, proj_row,
variable=self.project_var, variable=self.project_var,
values=[], values=[],
width=400,
command=self._on_project_selected command=self._on_project_selected
) if USE_CTK else ttk.Combobox(project_frame, textvariable=self.project_var) ) if USE_CTK else ttk.Combobox(proj_row, textvariable=self.project_var, width=50)
self.project_combo.pack(side="left", fill="x", expand=True, padx=5) self.project_combo.pack(side="left", padx=(0, 10))
if not USE_CTK: if not USE_CTK:
self.project_combo.bind("<<ComboboxSelected>>", lambda e: self._on_project_selected(None)) self.project_combo.bind("<<ComboboxSelected>>", lambda e: self._on_project_selected(None))
btn_new = ctk.CTkButton(project_frame, text="+ New", width=70, command=self._new_project) if USE_CTK else ttk.Button(project_frame, text="+ New", command=self._new_project) btn_new = ctk.CTkButton(proj_row, text="+ New", width=80, command=self._new_project) if USE_CTK else ttk.Button(proj_row, text="+ New", command=self._new_project)
btn_new.pack(side="left", padx=5) btn_new.pack(side="left", padx=5)
btn_open = ctk.CTkButton(project_frame, text="Open", width=70, command=self._open_project) if USE_CTK else ttk.Button(project_frame, text="Open", command=self._open_project) btn_open = ctk.CTkButton(proj_row, text="Open", width=80, command=self._open_project) if USE_CTK else ttk.Button(proj_row, text="Open", command=self._open_project)
btn_open.pack(side="left", padx=5) btn_open.pack(side="left", padx=5)
# Videos list # Project info
self.project_info_var = tk.StringVar(value="No project loaded")
if USE_CTK:
self.project_info = ctk.CTkLabel(project_frame, textvariable=self.project_info_var, text_color="gray")
else:
self.project_info = ttk.Label(project_frame, textvariable=self.project_info_var)
self.project_info.pack(anchor="w", padx=10, pady=(0, 10))
# === Videos Section ===
videos_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Videos") videos_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Videos")
videos_frame.pack(fill="both", expand=True, pady=(0, 10)) videos_frame.pack(fill="both", expand=True, pady=(0, 10))
if USE_CTK: if USE_CTK:
ctk.CTkLabel(videos_frame, text="Videos:").pack(anchor="w", padx=5, pady=5) ctk.CTkLabel(videos_frame, text="Session Videos:", font=("", 14, "bold")).pack(anchor="w", padx=10, pady=(10, 5))
# Listbox for videos # Listbox for videos
list_frame = ctk.CTkFrame(videos_frame) if USE_CTK else ttk.Frame(videos_frame) list_frame = ctk.CTkFrame(videos_frame) if USE_CTK else ttk.Frame(videos_frame)
list_frame.pack(fill="both", expand=True, padx=5, pady=5) list_frame.pack(fill="both", expand=True, padx=10, pady=5)
self.video_listbox = tk.Listbox(list_frame, height=10, selectmode="extended") self.video_listbox = tk.Listbox(list_frame, height=12, selectmode="extended", font=("Consolas", 10))
self.video_listbox.pack(side="left", fill="both", expand=True) self.video_listbox.pack(side="left", fill="both", expand=True)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.video_listbox.yview) scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.video_listbox.yview)
@@ -87,67 +223,100 @@ class CADDocumenterGUI:
# Video buttons # Video buttons
video_btn_frame = ctk.CTkFrame(videos_frame) if USE_CTK else ttk.Frame(videos_frame) video_btn_frame = ctk.CTkFrame(videos_frame) if USE_CTK else ttk.Frame(videos_frame)
video_btn_frame.pack(fill="x", padx=5, pady=5) video_btn_frame.pack(fill="x", padx=10, pady=10)
btn_add = ctk.CTkButton(video_btn_frame, text="Add Videos", command=self._add_videos) if USE_CTK else ttk.Button(video_btn_frame, text="Add Videos", command=self._add_videos) btn_add = ctk.CTkButton(video_btn_frame, text="+ Add Video", width=120, command=self._add_videos) if USE_CTK else ttk.Button(video_btn_frame, text="+ Add Video", command=self._add_videos)
btn_add.pack(side="left", padx=5) btn_add.pack(side="left", padx=(0, 10))
btn_remove = ctk.CTkButton(video_btn_frame, text="Remove", command=self._remove_video) if USE_CTK else ttk.Button(video_btn_frame, text="Remove", command=self._remove_video) btn_remove = ctk.CTkButton(video_btn_frame, text="Remove", width=100, command=self._remove_video, fg_color="gray") if USE_CTK else ttk.Button(video_btn_frame, text="Remove", command=self._remove_video)
btn_remove.pack(side="left", padx=5) btn_remove.pack(side="left")
# Drop zone hint # Next gen hint
self.next_gen_var = tk.StringVar(value="")
if USE_CTK: if USE_CTK:
ctk.CTkLabel(video_btn_frame, text="(Drag & drop videos here)", text_color="gray").pack(side="right", padx=5) ctk.CTkLabel(video_btn_frame, textvariable=self.next_gen_var, text_color="cyan").pack(side="right", padx=10)
else:
ttk.Label(video_btn_frame, textvariable=self.next_gen_var).pack(side="right", padx=10)
# Options # === Options Section ===
options_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Options") options_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Options")
options_frame.pack(fill="x", pady=(0, 10)) options_frame.pack(fill="x", pady=(0, 10))
if USE_CTK: if USE_CTK:
ctk.CTkLabel(options_frame, text="Options:").pack(anchor="w", padx=5, pady=5) ctk.CTkLabel(options_frame, text="Processing Options:", font=("", 14, "bold")).pack(anchor="w", padx=10, pady=(10, 5))
opts_inner = ctk.CTkFrame(options_frame) if USE_CTK else ttk.Frame(options_frame)
opts_inner.pack(fill="x", padx=10, pady=(0, 10))
self.export_only_var = tk.BooleanVar(value=True) self.export_only_var = tk.BooleanVar(value=True)
cb_export = ctk.CTkCheckBox(options_frame, text="Export for Clawdbot (no API)", variable=self.export_only_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Export for Clawdbot (no API)", variable=self.export_only_var) cb_export = ctk.CTkCheckBox(opts_inner, text="Export for Clawdbot (recommended)", variable=self.export_only_var) if USE_CTK else ttk.Checkbutton(opts_inner, text="Export for Clawdbot (recommended)", variable=self.export_only_var)
cb_export.pack(anchor="w", padx=20, pady=2) cb_export.pack(anchor="w", pady=2)
self.scene_detect_var = tk.BooleanVar(value=True) self.scene_detect_var = tk.BooleanVar(value=True)
cb_scene = ctk.CTkCheckBox(options_frame, text="Use scene detection", variable=self.scene_detect_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Use scene detection", variable=self.scene_detect_var) cb_scene = ctk.CTkCheckBox(opts_inner, text="Smart frame extraction (scene detection)", variable=self.scene_detect_var) if USE_CTK else ttk.Checkbutton(opts_inner, text="Smart frame extraction", variable=self.scene_detect_var)
cb_scene.pack(anchor="w", padx=20, pady=2) cb_scene.pack(anchor="w", pady=2)
self.transcribe_var = tk.BooleanVar(value=True) self.transcribe_var = tk.BooleanVar(value=True)
cb_trans = ctk.CTkCheckBox(options_frame, text="Whisper transcription", variable=self.transcribe_var) if USE_CTK else ttk.Checkbutton(options_frame, text="Whisper transcription", variable=self.transcribe_var) cb_trans = ctk.CTkCheckBox(opts_inner, text="Whisper transcription (local GPU)", variable=self.transcribe_var) if USE_CTK else ttk.Checkbutton(opts_inner, text="Whisper transcription", variable=self.transcribe_var)
cb_trans.pack(anchor="w", padx=20, pady=2) cb_trans.pack(anchor="w", pady=2)
# Process button # === Process Button ===
self.btn_process = ctk.CTkButton( self.btn_process = ctk.CTkButton(
main_frame, main_frame,
text="Process Videos", text="Process Pending Videos",
height=40, height=45,
font=("", 16, "bold"),
command=self._process_videos command=self._process_videos
) if USE_CTK else ttk.Button(main_frame, text="Process Videos", command=self._process_videos) ) if USE_CTK else ttk.Button(main_frame, text="Process Pending Videos", command=self._process_videos)
self.btn_process.pack(fill="x", pady=(0, 10)) self.btn_process.pack(fill="x", pady=(0, 10))
# Status # === Status Section ===
status_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.Frame(main_frame) status_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.Frame(main_frame)
status_frame.pack(fill="x") status_frame.pack(fill="x")
self.status_var = tk.StringVar(value="Ready") self.status_var = tk.StringVar(value="Ready — Open or create a project to start")
status_label = ctk.CTkLabel(status_frame, textvariable=self.status_var) if USE_CTK else ttk.Label(status_frame, textvariable=self.status_var) status_label = ctk.CTkLabel(status_frame, textvariable=self.status_var) if USE_CTK else ttk.Label(status_frame, textvariable=self.status_var)
status_label.pack(side="left", padx=5) status_label.pack(side="left", padx=5)
self.progress = ctk.CTkProgressBar(status_frame) if USE_CTK else ttk.Progressbar(status_frame, mode="indeterminate") self.progress = ctk.CTkProgressBar(status_frame, width=200) if USE_CTK else ttk.Progressbar(status_frame, mode="indeterminate", length=200)
self.progress.pack(side="right", fill="x", expand=True, padx=5) self.progress.pack(side="right", padx=5)
if USE_CTK: if USE_CTK:
self.progress.set(0) self.progress.set(0)
def _get_next_gen_number(self) -> int:
"""Get the next generation number for the current project."""
if not self.current_project:
return 1
# Count existing videos as generations
return len(self.current_project.manifest.videos) + 1
def _update_project_list(self): def _update_project_list(self):
"""Update the project dropdown with recent projects.""" """Update the project dropdown with recent projects."""
# For now, just clear - could load from config/history pass # Could load from config/history
pass
def _update_project_info(self):
"""Update the project info display."""
if not self.current_project:
self.project_info_var.set("No project loaded")
self.next_gen_var.set("")
return
m = self.current_project.manifest
total = len(m.videos)
pending = len(self.current_project.get_pending_videos())
processed = total - pending
info = f"📁 {m.name}{total} videos ({processed} processed, {pending} pending)"
self.project_info_var.set(info)
next_gen = self._get_next_gen_number()
self.next_gen_var.set(f"Next: Gen {next_gen:03d}")
def _on_project_selected(self, value): def _on_project_selected(self, value):
"""Handle project selection.""" """Handle project selection."""
self._refresh_video_list() self._refresh_video_list()
self._update_project_info()
def _new_project(self): def _new_project(self):
"""Create a new project.""" """Create a new project."""
@@ -156,20 +325,21 @@ class CADDocumenterGUI:
return return
name = Path(folder).name name = Path(folder).name
# Simple dialog for project name
dialog = ctk.CTkInputDialog(
text="Enter project name:",
title="New Project"
) if USE_CTK else None
# Dialog for project name
if USE_CTK: if USE_CTK:
dialog = ctk.CTkInputDialog(
text="Enter project name:",
title="New Project"
)
name = dialog.get_input() or name name = dialog.get_input() or name
try: try:
self.current_project = Project.create(Path(folder), name, "") self.current_project = Project.create(Path(folder), name, "")
self.project_var.set(f"{name} ({folder})") self.project_var.set(f"{name}")
self.status_var.set(f"Created project: {name}") self.status_var.set(f"Created project: {name}")
self._refresh_video_list() self._refresh_video_list()
self._update_project_info()
except Exception as e: except Exception as e:
messagebox.showerror("Error", str(e)) messagebox.showerror("Error", str(e))
@@ -181,9 +351,12 @@ class CADDocumenterGUI:
try: try:
self.current_project = Project.load(Path(folder)) self.current_project = Project.load(Path(folder))
self.project_var.set(f"{self.current_project.manifest.name} ({folder})") self.project_var.set(f"{self.current_project.manifest.name}")
self.status_var.set(f"Loaded project: {self.current_project.manifest.name}") self.status_var.set(f"Loaded: {self.current_project.manifest.name}")
self._refresh_video_list() self._refresh_video_list()
self._update_project_info()
except FileNotFoundError:
messagebox.showerror("Error", f"No project.json found in {folder}\n\nUse '+ New' to create a new project.")
except Exception as e: except Exception as e:
messagebox.showerror("Error", f"Failed to load project: {e}") messagebox.showerror("Error", f"Failed to load project: {e}")
@@ -194,7 +367,7 @@ class CADDocumenterGUI:
if not self.current_project: if not self.current_project:
return return
for video in self.current_project.manifest.videos: for i, video in enumerate(self.current_project.manifest.videos, 1):
status_icon = { status_icon = {
"pending": "", "pending": "",
"processed": "", "processed": "",
@@ -202,35 +375,80 @@ class CADDocumenterGUI:
"error": "" "error": ""
}.get(video.status, "?") }.get(video.status, "?")
self.video_listbox.insert(tk.END, f"{status_icon} {video.filename}") self.video_listbox.insert(tk.END, f"{status_icon} Gen {i:03d}: {video.filename}")
self._update_project_info()
def _add_videos(self): def _add_videos(self):
"""Add videos to the project.""" """Add videos to the project with topic dialog."""
if not self.current_project: if not self.current_project:
messagebox.showwarning("Warning", "Please create or open a project first") messagebox.showwarning("Warning", "Please create or open a project first")
return return
files = filedialog.askopenfilenames( files = filedialog.askopenfilenames(
title="Select videos", title="Select video to add",
filetypes=[ filetypes=[
("Video files", "*.mp4 *.mkv *.avi *.mov *.webm"), ("Video files", "*.mp4 *.mkv *.avi *.mov *.webm"),
("All files", "*.*") ("All files", "*.*")
] ]
) )
added = 0
for f in files: for f in files:
video_path = Path(f)
gen_num = self._get_next_gen_number()
# Show dialog for topic
dialog = AddVideoDialog(self.root, video_path, gen_num)
result = dialog.show()
if result is None:
continue # Cancelled
try: try:
self.current_project.add_video(Path(f), copy=True) # Copy with new name
dest = self.current_project.videos_dir / result["new_filename"]
shutil.copy2(video_path, dest)
# Add to manifest (using new filename)
from .project import VideoEntry
entry = VideoEntry(
filename=result["new_filename"],
added_at=datetime.now().isoformat(),
)
self.current_project.manifest.videos.append(entry)
self.current_project.save()
added += 1
except Exception as e: except Exception as e:
messagebox.showerror("Error", f"Failed to add {f}: {e}") messagebox.showerror("Error", f"Failed to add {f}: {e}")
self._refresh_video_list() if added > 0:
self.status_var.set(f"Added {len(files)} video(s)") self._refresh_video_list()
self.status_var.set(f"✓ Added {added} video(s)")
def _remove_video(self): def _remove_video(self):
"""Remove selected video from project.""" """Remove selected video from project."""
# TODO: Implement video removal selection = self.video_listbox.curselection()
messagebox.showinfo("Info", "Video removal not yet implemented") if not selection:
return
if not messagebox.askyesno("Confirm", "Remove selected video(s) from project?"):
return
# Remove in reverse order to maintain indices
for idx in reversed(selection):
video = self.current_project.manifest.videos[idx]
# Delete file
video_path = self.current_project.videos_dir / video.filename
if video_path.exists():
video_path.unlink()
# Remove from manifest
del self.current_project.manifest.videos[idx]
self.current_project.save()
self._refresh_video_list()
self.status_var.set(f"✓ Removed {len(selection)} video(s)")
def _process_videos(self): def _process_videos(self):
"""Process videos in background thread.""" """Process videos in background thread."""
@@ -245,7 +463,7 @@ class CADDocumenterGUI:
# Disable button during processing # Disable button during processing
self.btn_process.configure(state="disabled") self.btn_process.configure(state="disabled")
self.status_var.set("Processing...") self.status_var.set(f"Processing {len(pending)} video(s)...")
if USE_CTK: if USE_CTK:
self.progress.set(0) self.progress.set(0)
@@ -262,6 +480,8 @@ class CADDocumenterGUI:
self.root.after(0, lambda: self._on_process_complete(results)) self.root.after(0, lambda: self._on_process_complete(results))
except Exception as e: except Exception as e:
import traceback
traceback.print_exc()
self.root.after(0, lambda: self._on_process_error(str(e))) self.root.after(0, lambda: self._on_process_error(str(e)))
thread = threading.Thread(target=process_thread, daemon=True) thread = threading.Thread(target=process_thread, daemon=True)
@@ -278,11 +498,13 @@ class CADDocumenterGUI:
self.btn_process.configure(state="normal") self.btn_process.configure(state="normal")
self._refresh_video_list() self._refresh_video_list()
msg = f"Processed {results.get('processed', 0)} video(s)" name = self.current_project.manifest.name
if self.export_only_var.get(): msg = f"✓ Processed {results.get('processed', 0)} video(s)"
msg += f"\n\nExports ready! Tell Clawdbot:\n\"Process CAD report for {self.current_project.manifest.name}\""
self.status_var.set("Complete!") if self.export_only_var.get():
msg += f"\n\n📤 Exports ready in clawdbot_export/\n\nTell Mario:\n\"Process CAD session for {name}\""
self.status_var.set("✓ Complete!")
messagebox.showinfo("Success", msg) messagebox.showinfo("Success", msg)
def _on_process_error(self, error): def _on_process_error(self, error):
@@ -294,8 +516,8 @@ class CADDocumenterGUI:
self.progress.stop() self.progress.stop()
self.btn_process.configure(state="normal") self.btn_process.configure(state="normal")
self.status_var.set("Error") self.status_var.set("Error")
messagebox.showerror("Error", f"Processing failed: {error}") messagebox.showerror("Error", f"Processing failed:\n\n{error}")
def run(self): def run(self):
"""Run the application.""" """Run the application."""