840 lines
35 KiB
Python
840 lines
35 KiB
Python
#!/usr/bin/env python3
|
|
"""YouTube Summarizer Interactive CLI
|
|
|
|
A beautiful interactive shell application for managing YouTube video summaries.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, Dict, Any, List, Tuple
|
|
import logging
|
|
from enum import Enum
|
|
|
|
import click
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
from rich.panel import Panel
|
|
from rich.layout import Layout
|
|
from rich.live import Live
|
|
from rich.align import Align
|
|
from rich.text import Text
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
from rich.prompt import Prompt, Confirm, IntPrompt
|
|
from rich.markdown import Markdown
|
|
from rich.syntax import Syntax
|
|
from rich import box
|
|
from rich.columns import Columns
|
|
from rich.tree import Tree
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.append(str(Path(__file__).parent.parent))
|
|
|
|
from backend.cli import SummaryManager, SummaryPipelineCLI
|
|
from backend.mermaid_renderer import MermaidRenderer, DiagramEnhancer
|
|
|
|
# Initialize Rich console
|
|
console = Console()
|
|
logging.basicConfig(level=logging.WARNING)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MenuOption(Enum):
|
|
"""Menu options for the interactive interface."""
|
|
ADD_SUMMARY = "1"
|
|
LIST_SUMMARIES = "2"
|
|
VIEW_SUMMARY = "3"
|
|
REGENERATE = "4"
|
|
REFINE = "5"
|
|
BATCH_PROCESS = "6"
|
|
COMPARE = "7"
|
|
STATISTICS = "8"
|
|
SETTINGS = "9"
|
|
HELP = "h"
|
|
EXIT = "q"
|
|
|
|
|
|
class InteractiveSummarizer:
|
|
"""Interactive shell interface for YouTube Summarizer."""
|
|
|
|
def __init__(self):
|
|
self.manager = SummaryManager()
|
|
self.current_model = "deepseek"
|
|
self.current_length = "standard"
|
|
self.include_diagrams = False
|
|
self.session_summaries = []
|
|
self.running = True
|
|
|
|
# Color scheme
|
|
self.primary_color = "cyan"
|
|
self.secondary_color = "yellow"
|
|
self.success_color = "green"
|
|
self.error_color = "red"
|
|
self.accent_color = "magenta"
|
|
|
|
def clear_screen(self):
|
|
"""Clear the terminal screen."""
|
|
os.system('clear' if os.name == 'posix' else 'cls')
|
|
|
|
def display_banner(self):
|
|
"""Display the application banner."""
|
|
banner = """
|
|
╔═══════════════════════════════════════════════════════════════════╗
|
|
║ ║
|
|
║ ▄▄▄█████▓ █ ██ ▄▄▄▄ ▓█████ ▄▄▄ ██▓ ║
|
|
║ ▓ ██▒ ▓▒ ██ ▓██▒▓█████▄ ▓█ ▀ ▒████▄ ▓██▒ ║
|
|
║ ▒ ▓██░ ▒░▓██ ▒██░▒██▒ ▄██▒███ ▒██ ▀█▄ ▒██▒ ║
|
|
║ ░ ▓██▓ ░ ▓▓█ ░██░▒██░█▀ ▒▓█ ▄ ░██▄▄▄▄██ ░██░ ║
|
|
║ ▒██▒ ░ ▒▒█████▓ ░▓█ ▀█▓░▒████▒ ▓█ ▓██▒░██░ ║
|
|
║ ▒ ░░ ░▒▓▒ ▒ ▒ ░▒▓███▀▒░░ ▒░ ░ ▒▒ ▓▒█░░▓ ║
|
|
║ ░ ░░▒░ ░ ░ ▒░▒ ░ ░ ░ ░ ▒ ▒▒ ░ ▒ ░ ║
|
|
║ ░ ░░░ ░ ░ ░ ░ ░ ░ ▒ ▒ ░ ║
|
|
║ ░ ░ ░ ░ ░ ░ ░ ║
|
|
║ ░ ║
|
|
║ ║
|
|
║ YouTube Summarizer Interactive CLI ║
|
|
║ Powered by AI Intelligence ║
|
|
║ ║
|
|
╚═══════════════════════════════════════════════════════════════════╝
|
|
"""
|
|
|
|
styled_banner = Text(banner, style=f"bold {self.primary_color}")
|
|
console.print(styled_banner)
|
|
|
|
def display_menu(self):
|
|
"""Display the main menu."""
|
|
menu = Panel(
|
|
"[bold cyan]📹 Main Menu[/bold cyan]\n\n"
|
|
"[yellow]1.[/yellow] Add New Summary\n"
|
|
"[yellow]2.[/yellow] List Summaries\n"
|
|
"[yellow]3.[/yellow] View Summary\n"
|
|
"[yellow]4.[/yellow] Regenerate Summary\n"
|
|
"[yellow]5.[/yellow] Refine Summary\n"
|
|
"[yellow]6.[/yellow] Batch Process\n"
|
|
"[yellow]7.[/yellow] Compare Summaries\n"
|
|
"[yellow]8.[/yellow] Statistics\n"
|
|
"[yellow]9.[/yellow] Settings\n\n"
|
|
"[dim]h - Help | q - Exit[/dim]",
|
|
title="[bold magenta]Choose an Option[/bold magenta]",
|
|
border_style="cyan",
|
|
box=box.ROUNDED
|
|
)
|
|
console.print(menu)
|
|
|
|
def display_status_bar(self):
|
|
"""Display a status bar with current settings."""
|
|
status_items = [
|
|
f"[cyan]Model:[/cyan] {self.current_model}",
|
|
f"[cyan]Length:[/cyan] {self.current_length}",
|
|
f"[cyan]Diagrams:[/cyan] {'✓' if self.include_diagrams else '✗'}",
|
|
f"[cyan]Session:[/cyan] {len(self.session_summaries)} summaries"
|
|
]
|
|
|
|
status_bar = " | ".join(status_items)
|
|
console.print(Panel(status_bar, style="dim", box=box.MINIMAL))
|
|
|
|
async def add_summary_interactive(self):
|
|
"""Interactive flow for adding a new summary."""
|
|
self.clear_screen()
|
|
console.print(Panel("[bold cyan]🎥 Add New Video Summary[/bold cyan]", box=box.DOUBLE))
|
|
|
|
# Get URL
|
|
video_url = Prompt.ask("\n[green]Enter YouTube URL[/green]")
|
|
|
|
# Show options
|
|
console.print("\n[yellow]Configuration Options:[/yellow]")
|
|
|
|
# Model selection
|
|
models = ["deepseek", "anthropic", "openai", "gemini"]
|
|
console.print("\nAvailable models:")
|
|
for i, model in enumerate(models, 1):
|
|
console.print(f" {i}. {model}")
|
|
|
|
model_choice = IntPrompt.ask("Select model", default=1, choices=["1", "2", "3", "4"])
|
|
selected_model = models[model_choice - 1]
|
|
|
|
# Length selection
|
|
lengths = ["brief", "standard", "detailed"]
|
|
console.print("\nSummary length:")
|
|
for i, length in enumerate(lengths, 1):
|
|
console.print(f" {i}. {length}")
|
|
|
|
length_choice = IntPrompt.ask("Select length", default=2, choices=["1", "2", "3"])
|
|
selected_length = lengths[length_choice - 1]
|
|
|
|
# Diagrams
|
|
include_diagrams = Confirm.ask("\nInclude Mermaid diagrams?", default=False)
|
|
|
|
# Custom prompt
|
|
use_custom = Confirm.ask("\nUse custom prompt?", default=False)
|
|
custom_prompt = None
|
|
if use_custom:
|
|
console.print("\n[dim]Enter your custom prompt (press Enter twice to finish):[/dim]")
|
|
lines = []
|
|
while True:
|
|
line = input()
|
|
if line == "":
|
|
break
|
|
lines.append(line)
|
|
custom_prompt = "\n".join(lines)
|
|
|
|
# Focus areas
|
|
focus_areas = []
|
|
if Confirm.ask("\nAdd focus areas?", default=False):
|
|
console.print("[dim]Enter focus areas (empty line to finish):[/dim]")
|
|
while True:
|
|
area = input("Focus area: ")
|
|
if not area:
|
|
break
|
|
focus_areas.append(area)
|
|
|
|
# Process the video
|
|
console.print(f"\n[cyan]Processing video with {selected_model}...[/cyan]")
|
|
|
|
pipeline = SummaryPipelineCLI(model=selected_model)
|
|
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
console=console
|
|
) as progress:
|
|
task = progress.add_task("[cyan]Generating summary...", total=None)
|
|
|
|
try:
|
|
result = await pipeline.process_video(
|
|
video_url=video_url,
|
|
custom_prompt=custom_prompt,
|
|
summary_length=selected_length,
|
|
focus_areas=focus_areas if focus_areas else None,
|
|
include_diagrams=include_diagrams
|
|
)
|
|
|
|
# Save to database
|
|
summary_data = {
|
|
"video_id": result.get("video_id"),
|
|
"video_url": video_url,
|
|
"video_title": result.get("metadata", {}).get("title"),
|
|
"transcript": result.get("transcript"),
|
|
"summary": result.get("summary", {}).get("content"),
|
|
"key_points": result.get("summary", {}).get("key_points"),
|
|
"main_themes": result.get("summary", {}).get("main_themes"),
|
|
"model_used": selected_model,
|
|
"processing_time": result.get("processing_time"),
|
|
"quality_score": result.get("quality_metrics", {}).get("overall_score"),
|
|
"summary_length": selected_length,
|
|
"focus_areas": focus_areas
|
|
}
|
|
|
|
saved = pipeline.summary_manager.save_summary(summary_data)
|
|
self.session_summaries.append(saved.id)
|
|
|
|
progress.update(task, description="[green]✓ Summary created successfully!")
|
|
|
|
# Display preview
|
|
console.print(f"\n[green]✓ Summary created![/green]")
|
|
console.print(f"[yellow]ID:[/yellow] {saved.id}")
|
|
console.print(f"[yellow]Title:[/yellow] {saved.video_title}")
|
|
|
|
if saved.summary:
|
|
preview = saved.summary[:300] + "..." if len(saved.summary) > 300 else saved.summary
|
|
console.print(f"\n[bold]Preview:[/bold]\n{preview}")
|
|
|
|
# Ask if user wants to view full summary
|
|
if Confirm.ask("\nView full summary?", default=True):
|
|
await self.view_summary_interactive(saved.id)
|
|
|
|
except Exception as e:
|
|
progress.update(task, description=f"[red]✗ Error: {e}")
|
|
console.print(f"\n[red]Failed to create summary: {e}[/red]")
|
|
|
|
input("\nPress Enter to continue...")
|
|
|
|
def list_summaries_interactive(self):
|
|
"""Interactive listing of summaries."""
|
|
self.clear_screen()
|
|
console.print(Panel("[bold cyan]📚 Summary Library[/bold cyan]", box=box.DOUBLE))
|
|
|
|
# Get filter options
|
|
limit = IntPrompt.ask("\nHow many summaries to show?", default=10)
|
|
|
|
summaries = self.manager.list_summaries(limit=limit)
|
|
|
|
if not summaries:
|
|
console.print("\n[yellow]No summaries found[/yellow]")
|
|
else:
|
|
# Create interactive table
|
|
table = Table(title=f"Recent {len(summaries)} Summaries", box=box.ROUNDED)
|
|
table.add_column("#", style="dim", width=3)
|
|
table.add_column("ID", style="cyan", width=8)
|
|
table.add_column("Title", style="green", width=35)
|
|
table.add_column("Model", style="yellow", width=10)
|
|
table.add_column("Created", style="magenta", width=16)
|
|
table.add_column("Quality", style="blue", width=7)
|
|
|
|
for i, summary in enumerate(summaries, 1):
|
|
quality = f"{summary.quality_score:.1f}" if summary.quality_score else "N/A"
|
|
created = summary.created_at.strftime("%Y-%m-%d %H:%M")
|
|
title = summary.video_title[:32] + "..." if len(summary.video_title or "") > 35 else summary.video_title
|
|
|
|
# Highlight session summaries
|
|
id_display = summary.id[:8]
|
|
if summary.id in self.session_summaries:
|
|
id_display = f"[bold]{id_display}[/bold] ✨"
|
|
|
|
table.add_row(
|
|
str(i),
|
|
id_display,
|
|
title or "Unknown",
|
|
summary.model_used or "Unknown",
|
|
created,
|
|
quality
|
|
)
|
|
|
|
console.print(table)
|
|
|
|
# Allow selection
|
|
if Confirm.ask("\nSelect a summary to view?", default=False):
|
|
selection = IntPrompt.ask("Enter number", default=1, choices=[str(i) for i in range(1, len(summaries) + 1)])
|
|
selected = summaries[selection - 1]
|
|
asyncio.run(self.view_summary_interactive(selected.id))
|
|
|
|
input("\nPress Enter to continue...")
|
|
|
|
async def view_summary_interactive(self, summary_id: Optional[str] = None):
|
|
"""Interactive summary viewing with rich formatting."""
|
|
self.clear_screen()
|
|
|
|
if not summary_id:
|
|
summary_id = Prompt.ask("[green]Enter Summary ID[/green]")
|
|
|
|
summary = self.manager.get_summary(summary_id)
|
|
|
|
if not summary:
|
|
console.print(f"[red]Summary not found: {summary_id}[/red]")
|
|
input("\nPress Enter to continue...")
|
|
return
|
|
|
|
# Create a rich layout
|
|
layout = Layout()
|
|
layout.split_column(
|
|
Layout(name="header", size=3),
|
|
Layout(name="body"),
|
|
Layout(name="footer", size=3)
|
|
)
|
|
|
|
# Header
|
|
header_text = f"[bold cyan]📄 {summary.video_title or 'Untitled'}[/bold cyan]"
|
|
layout["header"].update(Panel(header_text, box=box.DOUBLE))
|
|
|
|
# Body content
|
|
body_parts = []
|
|
|
|
# Metadata
|
|
metadata = Table(box=box.SIMPLE)
|
|
metadata.add_column("Property", style="yellow")
|
|
metadata.add_column("Value", style="white")
|
|
metadata.add_row("ID", summary.id[:12] + "...")
|
|
metadata.add_row("URL", summary.video_url)
|
|
metadata.add_row("Model", summary.model_used or "Unknown")
|
|
metadata.add_row("Created", summary.created_at.strftime("%Y-%m-%d %H:%M") if summary.created_at else "Unknown")
|
|
metadata.add_row("Quality", f"{summary.quality_score:.2f}" if summary.quality_score else "N/A")
|
|
|
|
body_parts.append(Panel(metadata, title="[bold]Metadata[/bold]", border_style="dim"))
|
|
|
|
# Summary content
|
|
if summary.summary:
|
|
# Check for Mermaid diagrams
|
|
if '```mermaid' in summary.summary:
|
|
# Split summary by mermaid blocks
|
|
parts = summary.summary.split('```mermaid')
|
|
formatted_summary = parts[0]
|
|
|
|
for i, part in enumerate(parts[1:], 1):
|
|
if '```' in part:
|
|
diagram_code, rest = part.split('```', 1)
|
|
formatted_summary += f"\n[cyan]📊 Diagram {i}:[/cyan]\n"
|
|
formatted_summary += f"[dim]```mermaid{diagram_code}```[/dim]\n"
|
|
formatted_summary += rest
|
|
else:
|
|
formatted_summary += part
|
|
else:
|
|
formatted_summary = summary.summary
|
|
|
|
summary_panel = Panel(
|
|
Markdown(formatted_summary) if len(formatted_summary) < 2000 else formatted_summary,
|
|
title="[bold]Summary[/bold]",
|
|
border_style="green"
|
|
)
|
|
body_parts.append(summary_panel)
|
|
|
|
# Key points
|
|
if summary.key_points:
|
|
points_tree = Tree("[bold]Key Points[/bold]")
|
|
for point in summary.key_points:
|
|
points_tree.add(f"• {point}")
|
|
body_parts.append(Panel(points_tree, border_style="yellow"))
|
|
|
|
# Main themes
|
|
if summary.main_themes:
|
|
themes_list = "\n".join([f"🏷️ {theme}" for theme in summary.main_themes])
|
|
body_parts.append(Panel(themes_list, title="[bold]Main Themes[/bold]", border_style="magenta"))
|
|
|
|
# Combine body parts
|
|
layout["body"].update(Columns(body_parts, equal=False, expand=True))
|
|
|
|
# Footer with actions
|
|
footer_text = "[dim]r - Refine | d - Diagrams | e - Export | b - Back[/dim]"
|
|
layout["footer"].update(Panel(footer_text, box=box.MINIMAL))
|
|
|
|
console.print(layout)
|
|
|
|
# Handle actions
|
|
action = Prompt.ask("\n[green]Action[/green]", choices=["r", "d", "e", "b"], default="b")
|
|
|
|
if action == "r":
|
|
await self.refine_summary_interactive(summary_id)
|
|
elif action == "d":
|
|
self.show_diagram_options(summary)
|
|
elif action == "e":
|
|
self.export_summary(summary)
|
|
elif action == "b":
|
|
return
|
|
|
|
async def refine_summary_interactive(self, summary_id: Optional[str] = None):
|
|
"""Interactive refinement interface."""
|
|
self.clear_screen()
|
|
console.print(Panel("[bold cyan]🔄 Refine Summary[/bold cyan]", box=box.DOUBLE))
|
|
|
|
if not summary_id:
|
|
summary_id = Prompt.ask("[green]Enter Summary ID[/green]")
|
|
|
|
summary = self.manager.get_summary(summary_id)
|
|
|
|
if not summary:
|
|
console.print(f"[red]Summary not found: {summary_id}[/red]")
|
|
input("\nPress Enter to continue...")
|
|
return
|
|
|
|
console.print(f"\n[yellow]Refining:[/yellow] {summary.video_title}")
|
|
console.print(f"[yellow]Current Model:[/yellow] {summary.model_used}")
|
|
|
|
# Display current summary
|
|
if summary.summary:
|
|
preview = summary.summary[:400] + "..." if len(summary.summary) > 400 else summary.summary
|
|
console.print(f"\n[dim]Current summary:[/dim]\n{preview}\n")
|
|
|
|
# Refinement loop
|
|
refinement_history = []
|
|
console.print("[cyan]Interactive Refinement Mode[/cyan]")
|
|
console.print("[dim]Commands: 'done' to finish | 'undo' to revert | 'help' for tips[/dim]\n")
|
|
|
|
while True:
|
|
instruction = Prompt.ask("[green]Refinement instruction[/green]")
|
|
|
|
if instruction.lower() == 'done':
|
|
console.print("[green]✓ Refinement complete![/green]")
|
|
break
|
|
|
|
if instruction.lower() == 'help':
|
|
self.show_refinement_tips()
|
|
continue
|
|
|
|
if instruction.lower() == 'undo':
|
|
if refinement_history:
|
|
previous = refinement_history.pop()
|
|
updates = {
|
|
"summary": previous['summary'],
|
|
"key_points": previous.get('key_points'),
|
|
"main_themes": previous.get('main_themes')
|
|
}
|
|
summary = self.manager.update_summary(summary_id, updates)
|
|
console.print("[yellow]✓ Reverted to previous version[/yellow]")
|
|
else:
|
|
console.print("[yellow]No previous versions to revert to[/yellow]")
|
|
continue
|
|
|
|
# Save current state
|
|
refinement_history.append({
|
|
"summary": summary.summary,
|
|
"key_points": summary.key_points,
|
|
"main_themes": summary.main_themes
|
|
})
|
|
|
|
# Process refinement
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
console=console
|
|
) as progress:
|
|
task = progress.add_task(f"[cyan]Applying: {instruction[:50]}...", total=None)
|
|
|
|
try:
|
|
pipeline = SummaryPipelineCLI(model=summary.model_used or 'deepseek')
|
|
|
|
refinement_prompt = f"""
|
|
Original summary:
|
|
{summary.summary}
|
|
|
|
Refinement instruction:
|
|
{instruction}
|
|
|
|
Please provide an improved summary based on the refinement instruction above.
|
|
"""
|
|
|
|
result = await pipeline.process_video(
|
|
video_url=summary.video_url,
|
|
custom_prompt=refinement_prompt,
|
|
summary_length=summary.summary_length or 'standard',
|
|
focus_areas=summary.focus_areas
|
|
)
|
|
|
|
updates = {
|
|
"summary": result.get("summary", {}).get("content"),
|
|
"key_points": result.get("summary", {}).get("key_points"),
|
|
"main_themes": result.get("summary", {}).get("main_themes")
|
|
}
|
|
|
|
summary = self.manager.update_summary(summary_id, updates)
|
|
progress.update(task, description="[green]✓ Refinement applied!")
|
|
|
|
# Show updated preview
|
|
if summary.summary:
|
|
preview = summary.summary[:400] + "..." if len(summary.summary) > 400 else summary.summary
|
|
console.print(f"\n[green]Updated summary:[/green]\n{preview}\n")
|
|
|
|
except Exception as e:
|
|
progress.update(task, description=f"[red]✗ Error: {e}")
|
|
console.print(f"[red]Refinement failed: {e}[/red]")
|
|
|
|
input("\nPress Enter to continue...")
|
|
|
|
def show_refinement_tips(self):
|
|
"""Display refinement tips."""
|
|
tips = Panel(
|
|
"[bold yellow]Refinement Tips:[/bold yellow]\n\n"
|
|
"• 'Make it more concise' - Shorten the summary\n"
|
|
"• 'Focus on [topic]' - Emphasize specific aspects\n"
|
|
"• 'Add implementation details' - Include technical details\n"
|
|
"• 'Include examples' - Add concrete examples\n"
|
|
"• 'Add a timeline' - Include chronological information\n"
|
|
"• 'Include a flowchart for [process]' - Add visual diagram\n"
|
|
"• 'Make it more actionable' - Focus on practical steps\n"
|
|
"• 'Simplify the language' - Make it more accessible\n"
|
|
"• 'Add key statistics' - Include numerical data\n"
|
|
"• 'Structure as bullet points' - Change formatting",
|
|
border_style="yellow",
|
|
box=box.ROUNDED
|
|
)
|
|
console.print(tips)
|
|
|
|
def show_diagram_options(self, summary):
|
|
"""Show diagram-related options for a summary."""
|
|
self.clear_screen()
|
|
console.print(Panel("[bold cyan]📊 Diagram Options[/bold cyan]", box=box.DOUBLE))
|
|
|
|
if summary.summary and '```mermaid' in summary.summary:
|
|
# Extract and display diagrams
|
|
renderer = MermaidRenderer()
|
|
diagrams = renderer.extract_diagrams(summary.summary)
|
|
|
|
if diagrams:
|
|
console.print(f"\n[green]Found {len(diagrams)} diagram(s)[/green]\n")
|
|
|
|
for i, diagram in enumerate(diagrams, 1):
|
|
console.print(f"[yellow]Diagram {i}: {diagram['title']} ({diagram['type']})[/yellow]")
|
|
|
|
# Show ASCII preview
|
|
ascii_art = renderer.render_to_ascii(diagram)
|
|
if ascii_art:
|
|
console.print(Panel(ascii_art, border_style="dim"))
|
|
|
|
if Confirm.ask("\nRender diagrams to files?", default=False):
|
|
output_dir = f"diagrams/{summary.id}"
|
|
results = renderer.extract_and_render_all(summary.summary)
|
|
console.print(f"[green]✓ Rendered to {output_dir}[/green]")
|
|
else:
|
|
console.print("[yellow]No diagrams found in this summary[/yellow]")
|
|
|
|
# Suggest diagrams
|
|
if Confirm.ask("\nWould you like diagram suggestions?", default=True):
|
|
suggestions = DiagramEnhancer.suggest_diagrams(summary.summary or "")
|
|
|
|
if suggestions:
|
|
for suggestion in suggestions:
|
|
console.print(f"\n[yellow]{suggestion['type'].title()} Diagram[/yellow]")
|
|
console.print(f"[dim]{suggestion['reason']}[/dim]")
|
|
console.print(Syntax(suggestion['template'], "mermaid", theme="monokai"))
|
|
|
|
input("\nPress Enter to continue...")
|
|
|
|
def export_summary(self, summary):
|
|
"""Export summary to file."""
|
|
console.print("\n[cyan]Export Options:[/cyan]")
|
|
console.print("1. JSON")
|
|
console.print("2. Markdown")
|
|
console.print("3. Plain Text")
|
|
|
|
format_choice = IntPrompt.ask("Select format", default=1, choices=["1", "2", "3"])
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
if format_choice == 1:
|
|
# JSON export
|
|
filename = f"summary_{summary.id[:8]}_{timestamp}.json"
|
|
export_data = {
|
|
"id": summary.id,
|
|
"video_id": summary.video_id,
|
|
"video_title": summary.video_title,
|
|
"video_url": summary.video_url,
|
|
"summary": summary.summary,
|
|
"key_points": summary.key_points,
|
|
"main_themes": summary.main_themes,
|
|
"model_used": summary.model_used,
|
|
"created_at": summary.created_at.isoformat() if summary.created_at else None
|
|
}
|
|
with open(filename, 'w') as f:
|
|
json.dump(export_data, f, indent=2)
|
|
|
|
elif format_choice == 2:
|
|
# Markdown export
|
|
filename = f"summary_{summary.id[:8]}_{timestamp}.md"
|
|
with open(filename, 'w') as f:
|
|
f.write(f"# {summary.video_title}\n\n")
|
|
f.write(f"**URL:** {summary.video_url}\n")
|
|
f.write(f"**Model:** {summary.model_used}\n")
|
|
f.write(f"**Date:** {summary.created_at}\n\n")
|
|
f.write("## Summary\n\n")
|
|
f.write(summary.summary or "No summary available")
|
|
if summary.key_points:
|
|
f.write("\n\n## Key Points\n\n")
|
|
for point in summary.key_points:
|
|
f.write(f"- {point}\n")
|
|
|
|
else:
|
|
# Plain text export
|
|
filename = f"summary_{summary.id[:8]}_{timestamp}.txt"
|
|
with open(filename, 'w') as f:
|
|
f.write(f"{summary.video_title}\n")
|
|
f.write("=" * len(summary.video_title or "") + "\n\n")
|
|
f.write(summary.summary or "No summary available")
|
|
|
|
console.print(f"[green]✓ Exported to {filename}[/green]")
|
|
|
|
def show_statistics(self):
|
|
"""Display statistics dashboard."""
|
|
self.clear_screen()
|
|
console.print(Panel("[bold cyan]📊 Statistics Dashboard[/bold cyan]", box=box.DOUBLE))
|
|
|
|
from sqlalchemy import func
|
|
|
|
with self.manager.get_session() as session:
|
|
from backend.models import Summary
|
|
|
|
total = session.query(Summary).count()
|
|
|
|
# Model distribution
|
|
model_stats = session.query(
|
|
Summary.model_used,
|
|
func.count(Summary.id)
|
|
).group_by(Summary.model_used).all()
|
|
|
|
# Recent activity
|
|
recent_date = datetime.utcnow() - timedelta(days=7)
|
|
recent = session.query(Summary).filter(
|
|
Summary.created_at >= recent_date
|
|
).count()
|
|
|
|
# Average scores
|
|
avg_quality = session.query(func.avg(Summary.quality_score)).scalar()
|
|
avg_time = session.query(func.avg(Summary.processing_time)).scalar()
|
|
|
|
# Create statistics panels
|
|
stats_grid = Table.grid(padding=1)
|
|
|
|
# Total summaries
|
|
total_panel = Panel(
|
|
f"[bold cyan]{total}[/bold cyan]\n[dim]Total Summaries[/dim]",
|
|
border_style="cyan"
|
|
)
|
|
|
|
# Recent activity
|
|
recent_panel = Panel(
|
|
f"[bold green]{recent}[/bold green]\n[dim]Last 7 Days[/dim]",
|
|
border_style="green"
|
|
)
|
|
|
|
# Average quality
|
|
quality_panel = Panel(
|
|
f"[bold yellow]{avg_quality:.1f}[/bold yellow]\n[dim]Avg Quality[/dim]" if avg_quality else "[dim]No data[/dim]",
|
|
border_style="yellow"
|
|
)
|
|
|
|
# Session stats
|
|
session_panel = Panel(
|
|
f"[bold magenta]{len(self.session_summaries)}[/bold magenta]\n[dim]This Session[/dim]",
|
|
border_style="magenta"
|
|
)
|
|
|
|
stats_grid.add_row(total_panel, recent_panel, quality_panel, session_panel)
|
|
console.print(stats_grid)
|
|
|
|
# Model distribution chart
|
|
if model_stats:
|
|
console.print("\n[bold]Model Usage:[/bold]")
|
|
for model, count in model_stats:
|
|
bar_length = int((count / total) * 40)
|
|
bar = "█" * bar_length + "░" * (40 - bar_length)
|
|
percentage = (count / total) * 100
|
|
console.print(f" {(model or 'Unknown'):12} {bar} {percentage:.1f}% ({count})")
|
|
|
|
input("\nPress Enter to continue...")
|
|
|
|
def settings_menu(self):
|
|
"""Display settings menu."""
|
|
self.clear_screen()
|
|
console.print(Panel("[bold cyan]⚙️ Settings[/bold cyan]", box=box.DOUBLE))
|
|
|
|
console.print("\n[yellow]Current Settings:[/yellow]")
|
|
console.print(f" Default Model: {self.current_model}")
|
|
console.print(f" Default Length: {self.current_length}")
|
|
console.print(f" Include Diagrams: {self.include_diagrams}")
|
|
|
|
if Confirm.ask("\nChange settings?", default=False):
|
|
# Model
|
|
models = ["deepseek", "anthropic", "openai", "gemini"]
|
|
console.print("\n[yellow]Select default model:[/yellow]")
|
|
for i, model in enumerate(models, 1):
|
|
console.print(f" {i}. {model}")
|
|
choice = IntPrompt.ask("Choice", default=1)
|
|
self.current_model = models[choice - 1]
|
|
|
|
# Length
|
|
lengths = ["brief", "standard", "detailed"]
|
|
console.print("\n[yellow]Select default length:[/yellow]")
|
|
for i, length in enumerate(lengths, 1):
|
|
console.print(f" {i}. {length}")
|
|
choice = IntPrompt.ask("Choice", default=2)
|
|
self.current_length = lengths[choice - 1]
|
|
|
|
# Diagrams
|
|
self.include_diagrams = Confirm.ask("\nInclude diagrams by default?", default=False)
|
|
|
|
console.print("\n[green]✓ Settings updated![/green]")
|
|
|
|
input("\nPress Enter to continue...")
|
|
|
|
def show_help(self):
|
|
"""Display help information."""
|
|
self.clear_screen()
|
|
|
|
help_text = """
|
|
[bold cyan]YouTube Summarizer Help[/bold cyan]
|
|
|
|
[yellow]Quick Start:[/yellow]
|
|
1. Add a new summary with option [1]
|
|
2. View your summaries with option [2]
|
|
3. Refine summaries with option [5]
|
|
|
|
[yellow]Key Features:[/yellow]
|
|
• Multi-model support (DeepSeek, Anthropic, OpenAI, Gemini)
|
|
• Interactive refinement until satisfaction
|
|
• Mermaid diagram generation and rendering
|
|
• Batch processing for multiple videos
|
|
• Summary comparison across models
|
|
|
|
[yellow]Refinement Tips:[/yellow]
|
|
• Be specific with instructions
|
|
• Use "undo" to revert changes
|
|
• Try different models for variety
|
|
• Add diagrams for visual content
|
|
|
|
[yellow]Keyboard Shortcuts:[/yellow]
|
|
• q - Exit application
|
|
• h - Show this help
|
|
• Numbers 1-9 - Quick menu selection
|
|
|
|
[yellow]Pro Tips:[/yellow]
|
|
• Start with standard length, refine if needed
|
|
• Use focus areas for targeted summaries
|
|
• Export important summaries for backup
|
|
• Compare models to find best results
|
|
"""
|
|
|
|
console.print(Panel(help_text, border_style="cyan", box=box.ROUNDED))
|
|
input("\nPress Enter to continue...")
|
|
|
|
async def run(self):
|
|
"""Main application loop."""
|
|
self.clear_screen()
|
|
self.display_banner()
|
|
time.sleep(2)
|
|
|
|
while self.running:
|
|
self.clear_screen()
|
|
self.display_status_bar()
|
|
self.display_menu()
|
|
|
|
choice = Prompt.ask("\n[bold green]Select option[/bold green]", default="2")
|
|
|
|
try:
|
|
if choice == MenuOption.ADD_SUMMARY.value:
|
|
await self.add_summary_interactive()
|
|
elif choice == MenuOption.LIST_SUMMARIES.value:
|
|
self.list_summaries_interactive()
|
|
elif choice == MenuOption.VIEW_SUMMARY.value:
|
|
await self.view_summary_interactive()
|
|
elif choice == MenuOption.REGENERATE.value:
|
|
console.print("[yellow]Feature coming soon![/yellow]")
|
|
input("\nPress Enter to continue...")
|
|
elif choice == MenuOption.REFINE.value:
|
|
await self.refine_summary_interactive()
|
|
elif choice == MenuOption.BATCH_PROCESS.value:
|
|
console.print("[yellow]Feature coming soon![/yellow]")
|
|
input("\nPress Enter to continue...")
|
|
elif choice == MenuOption.COMPARE.value:
|
|
console.print("[yellow]Feature coming soon![/yellow]")
|
|
input("\nPress Enter to continue...")
|
|
elif choice == MenuOption.STATISTICS.value:
|
|
self.show_statistics()
|
|
elif choice == MenuOption.SETTINGS.value:
|
|
self.settings_menu()
|
|
elif choice == MenuOption.HELP.value:
|
|
self.show_help()
|
|
elif choice == MenuOption.EXIT.value:
|
|
if Confirm.ask("\n[yellow]Are you sure you want to exit?[/yellow]", default=False):
|
|
self.running = False
|
|
console.print("\n[cyan]Thank you for using YouTube Summarizer![/cyan]")
|
|
console.print("[dim]Goodbye! 👋[/dim]\n")
|
|
else:
|
|
console.print("[red]Invalid option. Please try again.[/red]")
|
|
input("\nPress Enter to continue...")
|
|
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]Operation cancelled[/yellow]")
|
|
input("\nPress Enter to continue...")
|
|
except Exception as e:
|
|
console.print(f"\n[red]Error: {e}[/red]")
|
|
logger.exception("Error in main loop")
|
|
input("\nPress Enter to continue...")
|
|
|
|
|
|
def main():
|
|
"""Entry point for the interactive CLI."""
|
|
app = InteractiveSummarizer()
|
|
|
|
try:
|
|
asyncio.run(app.run())
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]Application interrupted[/yellow]")
|
|
except Exception as e:
|
|
console.print(f"\n[red]Fatal error: {e}[/red]")
|
|
logger.exception("Fatal error")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |