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

373 lines
12 KiB
Python

# Copyright (C) 2025 AIDC-AI
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
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
from pixelle_video.utils.os_util import (
get_resource_path,
list_resource_files,
list_resource_dirs,
resource_exists
)
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 (merged from templates/ and data/templates/)
Returns:
List of size strings like ["1080x1920", "1920x1080", "1080x1080"]
Examples:
>>> list_available_sizes()
['1080x1920', '1920x1080', '1080x1080']
"""
# Use new resource API to merge default and custom directories
all_dirs = list_resource_dirs("templates")
# Filter to only valid size formats (WIDTHxHEIGHT)
sizes = []
for dir_name in all_dirs:
if 'x' in dir_name:
try:
width, height = dir_name.split('x')
int(width)
int(height)
sizes.append(dir_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 (merged from templates/ and data/templates/)
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', ...]
"""
# Use new resource API to merge default and custom templates
all_files = list_resource_files("templates", size)
# Filter to only HTML files
templates = [f for f in all_files if f.endswith('.html')]
return sorted(templates)
def get_template_full_path(size: str, template_name: str) -> str:
"""
Get full template path from size and template name (checks data/templates/ first, then templates/)
Args:
size: Size string like "1080x1920"
template_name: Template filename like "default.html"
Returns:
Full path like "templates/1080x1920/default.html" or "data/templates/1080x1920/default.html"
Raises:
FileNotFoundError: If template file doesn't exist in either location
Examples:
>>> get_template_full_path("1080x1920", "default.html")
'templates/1080x1920/default.html'
"""
# Use new resource API to search custom first, then default
try:
return get_resource_path("templates", size, template_name)
except FileNotFoundError:
available_templates = list_templates_for_size(size)
raise FileNotFoundError(
f"Template not found: {size}/{template_name}\n"
f"Available templates for size {size}: {available_templates}"
)
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 (checks data/templates/ first, then templates/)
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 (legacy)
- "data/templates/1080x1920/template.html": Custom path (legacy)
Returns:
Resolved full path (custom if exists, otherwise default)
Raises:
FileNotFoundError: If template doesn't exist in either location
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"
# Parse input to extract size and template name
size = None
template_name = None
# Handle different input formats
if template_input.startswith("templates/") or template_input.startswith("data/templates/"):
# Legacy full path format - extract size and name
parts = Path(template_input).parts
if len(parts) >= 3:
size = parts[-2]
template_name = parts[-1]
elif '/' in template_input and 'x' in template_input.split('/')[0]:
# "1080x1920/template.html" format
size, template_name = template_input.split('/', 1)
else:
# Just template name - use default size
size = "1080x1920"
template_name = template_input
# Use resource API to resolve path (custom > default)
try:
return get_resource_path("templates", size, template_name)
except FileNotFoundError:
available_sizes = list_available_sizes()
raise FileNotFoundError(
f"Template not found: {size}/{template_name}\n"
f"Available sizes: {available_sizes}\n"
f"Hint: Use format 'SIZExSIZE/template.html' (e.g., '1080x1920/default.html')"
)