Files
AI-Video/pixelle_video/utils/template_util.py
2025-11-07 16:59:12 +08:00

352 lines
11 KiB
Python

"""
Template utility functions for size parsing and template management
"""
import os
from pathlib import Path
from typing import List, Tuple, Optional, Literal
from pydantic import BaseModel, Field
def parse_template_size(template_path: str) -> Tuple[int, int]:
"""
Parse video size from template path
Args:
template_path: Template path like "templates/1080x1920/default.html"
or "1080x1920/default.html"
Returns:
Tuple of (width, height) in pixels
Raises:
ValueError: If template path format is invalid
Examples:
>>> parse_template_size("templates/1080x1920/default.html")
(1080, 1920)
>>> parse_template_size("1920x1080/modern.html")
(1920, 1080)
"""
path = Path(template_path)
# Get parent directory name (should be like "1080x1920")
dir_name = path.parent.name
# Special case: if parent is "templates", go up one more level
if dir_name == "templates":
# This shouldn't happen in new structure, but handle it
raise ValueError(
f"Invalid template path format: {template_path}. "
f"Expected format: 'WIDTHxHEIGHT/template.html' or 'templates/WIDTHxHEIGHT/template.html'"
)
# Parse size from directory name
if 'x' not in dir_name:
raise ValueError(
f"Invalid size format in path: {template_path}. "
f"Directory name should be 'WIDTHxHEIGHT' (e.g., '1080x1920')"
)
try:
width_str, height_str = dir_name.split('x')
width = int(width_str)
height = int(height_str)
# Sanity check
if width < 100 or height < 100 or width > 10000 or height > 10000:
raise ValueError(f"Invalid size dimensions: {width}x{height}")
return (width, height)
except ValueError as e:
raise ValueError(
f"Failed to parse size from path: {template_path}. "
f"Expected format: 'WIDTHxHEIGHT/template.html' (e.g., '1080x1920/default.html'). "
f"Error: {e}"
)
def list_available_sizes() -> List[str]:
"""
List all available video sizes
Returns:
List of size strings like ["1080x1920", "1920x1080", "1080x1080"]
Examples:
>>> list_available_sizes()
['1080x1920', '1920x1080', '1080x1080']
"""
templates_dir = Path("templates")
if not templates_dir.exists():
return []
sizes = []
for item in templates_dir.iterdir():
if item.is_dir() and 'x' in item.name:
# Validate it's a proper size format
try:
width, height = item.name.split('x')
int(width)
int(height)
sizes.append(item.name)
except (ValueError, AttributeError):
# Skip invalid directories
continue
return sorted(sizes)
def list_templates_for_size(size: str) -> List[str]:
"""
List all templates available for a given size
Args:
size: Size string like "1080x1920"
Returns:
List of template filenames (without path) like ["default.html", "modern.html"]
Examples:
>>> list_templates_for_size("1080x1920")
['cartoon.html', 'default.html', 'elegant.html', 'modern.html', ...]
"""
size_dir = Path("templates") / size
if not size_dir.exists() or not size_dir.is_dir():
return []
templates = []
for item in size_dir.iterdir():
if item.is_file() and item.suffix == '.html':
templates.append(item.name)
return sorted(templates)
def get_template_full_path(size: str, template_name: str) -> str:
"""
Get full template path from size and template name
Args:
size: Size string like "1080x1920"
template_name: Template filename like "default.html"
Returns:
Full path like "templates/1080x1920/default.html"
Raises:
FileNotFoundError: If template file doesn't exist
Examples:
>>> get_template_full_path("1080x1920", "default.html")
'templates/1080x1920/default.html'
"""
template_path = Path("templates") / size / template_name
if not template_path.exists():
available_templates = list_templates_for_size(size)
raise FileNotFoundError(
f"Template not found: {template_path}\n"
f"Available templates for size {size}: {available_templates}"
)
return str(template_path)
class TemplateDisplayInfo(BaseModel):
"""Template display information for UI layer"""
name: str = Field(..., description="Template name without extension")
size: str = Field(..., description="Size string like '1080x1920'")
width: int = Field(..., description="Width in pixels")
height: int = Field(..., description="Height in pixels")
orientation: Literal['portrait', 'landscape', 'square'] = Field(
...,
description="Video orientation"
)
is_standard: bool = Field(
...,
description="True only for standard sizes: 1080x1920, 1920x1080, 1080x1080"
)
class TemplateInfo(BaseModel):
"""Complete template information with path and display info"""
template_path: str = Field(..., description="Full template path like '1080x1920/default.html'")
display_info: TemplateDisplayInfo = Field(..., description="Display information")
def format_template_display_info(template_name: str, size: str) -> TemplateDisplayInfo:
"""
Format template display information for UI
Returns structured data for UI layer to handle display and i18n.
Args:
template_name: Template filename like "default.html"
size: Size string like "1080x1920"
Returns:
TemplateDisplayInfo object with name, size, dimensions, orientation, and standard flag
Examples:
>>> info = format_template_display_info("default.html", "1080x1920")
>>> info.name
'default'
>>> info.is_standard
True
>>> info = format_template_display_info("custom.html", "1080x1921")
>>> info.orientation
'portrait'
>>> info.is_standard
False
"""
# Keep full template name with .html extension
name = template_name
# Parse size
width, height = map(int, size.split('x'))
# Detect orientation
if height > width:
orientation = 'portrait'
elif width > height:
orientation = 'landscape'
else:
orientation = 'square'
# Check if it's a standard size (only these three)
is_standard = (width, height) in [(1080, 1920), (1920, 1080), (1080, 1080)]
return TemplateDisplayInfo(
name=name,
size=size,
width=width,
height=height,
orientation=orientation,
is_standard=is_standard
)
def get_all_templates_with_info() -> List[TemplateInfo]:
"""
Get all templates with their display information
Returns:
List of TemplateInfo objects
Example:
>>> templates = get_all_templates_with_info()
>>> for t in templates:
... print(f"{t.display_info.name} - {t.display_info.orientation}")
... print(f" Path: {t.template_path}")
... print(f" Standard: {t.display_info.is_standard}")
"""
result = []
sizes = list_available_sizes()
for size in sizes:
templates = list_templates_for_size(size)
for template in templates:
display_info = format_template_display_info(template, size)
full_path = f"{size}/{template}"
result.append(TemplateInfo(
template_path=full_path,
display_info=display_info
))
return result
def get_templates_grouped_by_size() -> dict:
"""
Get templates grouped by size
Returns:
Dict with size as key, list of TemplateInfo as value
Ordered by orientation priority: portrait > landscape > square
Example:
>>> grouped = get_templates_grouped_by_size()
>>> for size, templates in grouped.items():
... print(f"Size: {size}")
... for t in templates:
... print(f" - {t.display_info.name}")
"""
from collections import defaultdict
templates = get_all_templates_with_info()
grouped = defaultdict(list)
for t in templates:
grouped[t.display_info.size].append(t)
# Sort groups by orientation priority: portrait > landscape > square
orientation_priority = {'portrait': 0, 'landscape': 1, 'square': 2}
sorted_grouped = {}
for size in sorted(grouped.keys(), key=lambda s: (
orientation_priority.get(grouped[s][0].display_info.orientation, 3),
s
)):
sorted_grouped[size] = sorted(grouped[size], key=lambda t: t.display_info.name)
return sorted_grouped
def resolve_template_path(template_input: Optional[str]) -> str:
"""
Resolve template input to full path with validation
Args:
template_input: Can be:
- None: Use default "1080x1920/default.html"
- "template.html": Use default size + this template
- "1080x1920/template.html": Full relative path
- "templates/1080x1920/template.html": Absolute-ish path
Returns:
Resolved full path like "templates/1080x1920/default.html"
Raises:
FileNotFoundError: If template doesn't exist
Examples:
>>> resolve_template_path(None)
'templates/1080x1920/default.html'
>>> resolve_template_path("modern.html")
'templates/1080x1920/modern.html'
>>> resolve_template_path("1920x1080/default.html")
'templates/1920x1080/default.html'
"""
# Default case
if template_input is None:
template_input = "1080x1920/default.html"
# If already starts with "templates/", use as-is
if template_input.startswith("templates/"):
template_path = Path(template_input)
# If contains size directory (e.g., "1080x1920/default.html")
elif '/' in template_input and 'x' in template_input.split('/')[0]:
template_path = Path("templates") / template_input
# Just template name (e.g., "default.html")
else:
# Use default size
template_path = Path("templates") / "1080x1920" / template_input
# Validate existence
if not template_path.exists():
available_sizes = list_available_sizes()
raise FileNotFoundError(
f"Template not found: {template_path}\n"
f"Available sizes: {available_sizes}\n"
f"Hint: Use format 'SIZExSIZE/template.html' (e.g., '1080x1920/default.html')"
)
return str(template_path)