364 lines
11 KiB
Python
364 lines
11 KiB
Python
"""
|
|
Template Manager for YouTube Summarizer
|
|
Manages custom export templates with validation and rendering
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
import jinja2
|
|
from jinja2 import Template, Environment, FileSystemLoader, TemplateError
|
|
|
|
|
|
class TemplateType(Enum):
|
|
"""Supported template types"""
|
|
MARKDOWN = "markdown"
|
|
HTML = "html"
|
|
TEXT = "text"
|
|
|
|
|
|
@dataclass
|
|
class ExportTemplate:
|
|
"""Export template model"""
|
|
id: str
|
|
name: str
|
|
description: str
|
|
type: TemplateType
|
|
content: str
|
|
variables: List[str]
|
|
is_default: bool = False
|
|
created_at: Optional[str] = None
|
|
updated_at: Optional[str] = None
|
|
|
|
|
|
class TemplateManager:
|
|
"""Manages custom templates for exports"""
|
|
|
|
def __init__(self, templates_dir: str = "./templates"):
|
|
self.templates_dir = Path(templates_dir)
|
|
self.templates_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create subdirectories for each template type
|
|
for template_type in TemplateType:
|
|
(self.templates_dir / template_type.value).mkdir(exist_ok=True)
|
|
|
|
# Initialize Jinja2 environment
|
|
self.env = Environment(
|
|
loader=FileSystemLoader(self.templates_dir),
|
|
autoescape=True
|
|
)
|
|
|
|
# Load default templates
|
|
self._initialize_default_templates()
|
|
|
|
def _initialize_default_templates(self):
|
|
"""Initialize default templates if they don't exist"""
|
|
|
|
# Default Markdown template
|
|
markdown_default = """# {{ video_metadata.title }}
|
|
|
|
## Summary
|
|
{{ summary }}
|
|
|
|
## Key Points
|
|
{% for point in key_points %}
|
|
- {{ point }}
|
|
{% endfor %}
|
|
|
|
## Main Themes
|
|
{% for theme in main_themes %}
|
|
- {{ theme }}
|
|
{% endfor %}
|
|
|
|
## Actionable Insights
|
|
{% for insight in actionable_insights %}
|
|
1. {{ insight }}
|
|
{% endfor %}
|
|
|
|
---
|
|
*Generated on {{ export_metadata.exported_at }}*
|
|
"""
|
|
|
|
# Default HTML template
|
|
html_default = """<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>{{ video_metadata.title }} - Summary</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
h1 { color: #333; }
|
|
h2 { color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px; }
|
|
ul { list-style-type: disc; }
|
|
.metadata { color: #888; font-size: 0.9em; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>{{ video_metadata.title }}</h1>
|
|
|
|
<div class="metadata">
|
|
<p>Channel: {{ video_metadata.channel_name }}</p>
|
|
<p>Duration: {{ video_metadata.duration }}</p>
|
|
<p>URL: <a href="{{ video_url }}">{{ video_url }}</a></p>
|
|
</div>
|
|
|
|
<h2>Summary</h2>
|
|
<p>{{ summary }}</p>
|
|
|
|
<h2>Key Points</h2>
|
|
<ul>
|
|
{% for point in key_points %}
|
|
<li>{{ point }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
<h2>Main Themes</h2>
|
|
<ul>
|
|
{% for theme in main_themes %}
|
|
<li>{{ theme }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
|
|
<h2>Actionable Insights</h2>
|
|
<ol>
|
|
{% for insight in actionable_insights %}
|
|
<li>{{ insight }}</li>
|
|
{% endfor %}
|
|
</ol>
|
|
|
|
<footer>
|
|
<p class="metadata">Generated on {{ export_metadata.exported_at }}</p>
|
|
</footer>
|
|
</body>
|
|
</html>"""
|
|
|
|
# Default plain text template
|
|
text_default = """{{ video_metadata.title }}
|
|
{{ '=' * video_metadata.title|length }}
|
|
|
|
VIDEO INFORMATION
|
|
-----------------
|
|
Channel: {{ video_metadata.channel_name }}
|
|
Duration: {{ video_metadata.duration }}
|
|
URL: {{ video_url }}
|
|
|
|
SUMMARY
|
|
-------
|
|
{{ summary }}
|
|
|
|
KEY POINTS
|
|
----------
|
|
{% for point in key_points %}
|
|
* {{ point }}
|
|
{% endfor %}
|
|
|
|
MAIN THEMES
|
|
-----------
|
|
{% for theme in main_themes %}
|
|
* {{ theme }}
|
|
{% endfor %}
|
|
|
|
ACTIONABLE INSIGHTS
|
|
------------------
|
|
{% for insight in actionable_insights %}
|
|
{{ loop.index }}. {{ insight }}
|
|
{% endfor %}
|
|
|
|
Generated on {{ export_metadata.exported_at }}
|
|
"""
|
|
|
|
# Save default templates
|
|
self._save_template("default", TemplateType.MARKDOWN, markdown_default, is_default=True)
|
|
self._save_template("default", TemplateType.HTML, html_default, is_default=True)
|
|
self._save_template("default", TemplateType.TEXT, text_default, is_default=True)
|
|
|
|
def _save_template(self, name: str, template_type: TemplateType, content: str, is_default: bool = False):
|
|
"""Save a template to disk"""
|
|
template_path = self.templates_dir / template_type.value / f"{name}.j2"
|
|
|
|
# Only save if doesn't exist (for default templates)
|
|
if is_default and template_path.exists():
|
|
return
|
|
|
|
template_path.write_text(content)
|
|
|
|
def get_template(self, name: str, template_type: TemplateType) -> Optional[ExportTemplate]:
|
|
"""Get a specific template"""
|
|
template_path = self.templates_dir / template_type.value / f"{name}.j2"
|
|
|
|
if not template_path.exists():
|
|
return None
|
|
|
|
content = template_path.read_text()
|
|
|
|
# Extract variables from template
|
|
try:
|
|
ast = self.env.parse(content)
|
|
variables = list(jinja2.meta.find_undeclared_variables(ast))
|
|
except Exception:
|
|
variables = []
|
|
|
|
return ExportTemplate(
|
|
id=f"{template_type.value}_{name}",
|
|
name=name,
|
|
description=f"{template_type.value.title()} template: {name}",
|
|
type=template_type,
|
|
content=content,
|
|
variables=variables,
|
|
is_default=(name == "default")
|
|
)
|
|
|
|
def list_templates(self, template_type: Optional[TemplateType] = None) -> List[ExportTemplate]:
|
|
"""List all available templates"""
|
|
templates = []
|
|
|
|
types_to_check = [template_type] if template_type else list(TemplateType)
|
|
|
|
for t_type in types_to_check:
|
|
template_dir = self.templates_dir / t_type.value
|
|
if template_dir.exists():
|
|
for template_file in template_dir.glob("*.j2"):
|
|
name = template_file.stem
|
|
template = self.get_template(name, t_type)
|
|
if template:
|
|
templates.append(template)
|
|
|
|
return templates
|
|
|
|
def create_template(self, name: str, template_type: TemplateType, content: str, description: str = "") -> ExportTemplate:
|
|
"""Create a new custom template"""
|
|
|
|
# Validate template syntax
|
|
try:
|
|
template = Template(content)
|
|
# Try to extract variables
|
|
ast = self.env.parse(content)
|
|
variables = list(jinja2.meta.find_undeclared_variables(ast))
|
|
except TemplateError as e:
|
|
raise ValueError(f"Invalid template syntax: {e}")
|
|
|
|
# Save template
|
|
self._save_template(name, template_type, content)
|
|
|
|
return ExportTemplate(
|
|
id=f"{template_type.value}_{name}",
|
|
name=name,
|
|
description=description or f"Custom {template_type.value} template",
|
|
type=template_type,
|
|
content=content,
|
|
variables=variables,
|
|
is_default=False
|
|
)
|
|
|
|
def update_template(self, name: str, template_type: TemplateType, content: str) -> ExportTemplate:
|
|
"""Update an existing template"""
|
|
|
|
# Don't allow updating default templates
|
|
if name == "default":
|
|
raise ValueError("Cannot modify default templates")
|
|
|
|
# Validate template syntax
|
|
try:
|
|
template = Template(content)
|
|
ast = self.env.parse(content)
|
|
variables = list(jinja2.meta.find_undeclared_variables(ast))
|
|
except TemplateError as e:
|
|
raise ValueError(f"Invalid template syntax: {e}")
|
|
|
|
# Update template
|
|
self._save_template(name, template_type, content)
|
|
|
|
return self.get_template(name, template_type)
|
|
|
|
def delete_template(self, name: str, template_type: TemplateType):
|
|
"""Delete a custom template"""
|
|
|
|
# Don't allow deleting default templates
|
|
if name == "default":
|
|
raise ValueError("Cannot delete default templates")
|
|
|
|
template_path = self.templates_dir / template_type.value / f"{name}.j2"
|
|
|
|
if template_path.exists():
|
|
template_path.unlink()
|
|
|
|
def render_template(self, name: str, template_type: TemplateType, data: Dict[str, Any]) -> str:
|
|
"""Render a template with provided data"""
|
|
|
|
template = self.get_template(name, template_type)
|
|
|
|
if not template:
|
|
raise ValueError(f"Template not found: {name} ({template_type.value})")
|
|
|
|
try:
|
|
# Create Jinja2 template
|
|
jinja_template = Template(template.content)
|
|
|
|
# Render with data
|
|
rendered = jinja_template.render(**data)
|
|
|
|
return rendered
|
|
except Exception as e:
|
|
raise ValueError(f"Failed to render template: {e}")
|
|
|
|
def validate_template_data(self, name: str, template_type: TemplateType, data: Dict[str, Any]) -> List[str]:
|
|
"""Validate that required template variables are provided"""
|
|
|
|
template = self.get_template(name, template_type)
|
|
|
|
if not template:
|
|
return ["Template not found"]
|
|
|
|
missing_vars = []
|
|
for var in template.variables:
|
|
if var not in data and not self._is_nested_available(var, data):
|
|
missing_vars.append(var)
|
|
|
|
return missing_vars
|
|
|
|
def _is_nested_available(self, var_path: str, data: Dict[str, Any]) -> bool:
|
|
"""Check if a nested variable path is available in data"""
|
|
parts = var_path.split('.')
|
|
current = data
|
|
|
|
for part in parts:
|
|
if isinstance(current, dict) and part in current:
|
|
current = current[part]
|
|
else:
|
|
return False
|
|
|
|
return True
|
|
|
|
def get_template_preview(self, name: str, template_type: TemplateType) -> str:
|
|
"""Get a preview of template with sample data"""
|
|
|
|
sample_data = {
|
|
"video_metadata": {
|
|
"title": "Sample Video Title",
|
|
"channel_name": "Sample Channel",
|
|
"duration": "10:30",
|
|
"published_at": "2025-01-25"
|
|
},
|
|
"video_url": "https://youtube.com/watch?v=sample",
|
|
"summary": "This is a sample summary of the video content.",
|
|
"key_points": [
|
|
"First key point",
|
|
"Second key point",
|
|
"Third key point"
|
|
],
|
|
"main_themes": ["Technology", "Innovation", "Future"],
|
|
"actionable_insights": [
|
|
"Apply this concept to your workflow",
|
|
"Consider this approach for better results"
|
|
],
|
|
"export_metadata": {
|
|
"exported_at": "2025-01-25 12:00:00"
|
|
}
|
|
}
|
|
|
|
try:
|
|
return self.render_template(name, template_type, sample_data)
|
|
except Exception as e:
|
|
return f"Preview generation failed: {e}" |