更改视频尺寸的定义方式

This commit is contained in:
puke
2025-11-03 10:57:56 +08:00
parent 14a31592e5
commit 8333397615
16 changed files with 284 additions and 81 deletions

View File

@@ -65,8 +65,6 @@ async def generate_video_sync(
image_width=request_body.image_width,
image_height=request_body.image_height,
image_workflow=request_body.image_workflow,
video_width=request_body.video_width,
video_height=request_body.video_height,
video_fps=request_body.video_fps,
frame_template=request_body.frame_template,
prompt_prefix=request_body.prompt_prefix,

View File

@@ -37,12 +37,13 @@ class VideoGenerateRequest(BaseModel):
image_workflow: Optional[str] = Field(None, description="Custom image workflow")
# === Video Parameters ===
video_width: int = Field(1080, description="Video width")
video_height: int = Field(1920, description="Video height")
video_fps: int = Field(30, ge=15, le=60, description="Video FPS")
# === Frame Template ===
frame_template: Optional[str] = Field(None, description="HTML template name (e.g., 'default.html')")
# === Frame Template (determines video size) ===
frame_template: Optional[str] = Field(
None,
description="HTML template path with size (e.g., '1080x1920/default.html'). Video size is auto-determined from template."
)
# === Image Style ===
prompt_prefix: Optional[str] = Field(None, description="Image style prefix")

View File

@@ -20,9 +20,7 @@ class StoryboardConfig:
min_image_prompt_words: int = 30 # Min image prompt word count
max_image_prompt_words: int = 60 # Max image prompt word count
# Video parameters
video_width: int = 1080 # Video width
video_height: int = 1920 # Video height (9:16 portrait)
# Video parameters (fps only, size is determined by frame template)
video_fps: int = 30 # Frame rate
# Audio parameters
@@ -36,8 +34,8 @@ class StoryboardConfig:
image_height: int = 1024
image_workflow: Optional[str] = None # Image workflow filename (None = use default)
# Frame template
frame_template: str = "default.html" # HTML template name or path (e.g., "default.html", "modern.html")
# Frame template (includes size information in path)
frame_template: str = "1080x1920/default.html" # Template path with size (e.g., "1080x1920/default.html")
@dataclass

View File

@@ -18,6 +18,8 @@ from pathlib import Path
from html2image import Html2Image
from loguru import logger
from pixelle_video.utils.template_util import parse_template_size
class HTMLFrameGenerator:
"""
@@ -41,13 +43,17 @@ class HTMLFrameGenerator:
Initialize HTML frame generator
Args:
template_path: Path to HTML template file
template_path: Path to HTML template file (e.g., "templates/1080x1920/default.html")
"""
self.template_path = template_path
self.template = self._load_template(template_path)
# Parse video size from template path
self.width, self.height = parse_template_size(template_path)
self.hti = None # Lazy init to avoid overhead
self._check_linux_dependencies()
logger.debug(f"Loaded HTML template: {template_path}")
logger.debug(f"Loaded HTML template: {template_path} (size: {self.width}x{self.height})")
def _check_linux_dependencies(self):
"""Check Linux system dependencies and warn if missing"""
@@ -197,20 +203,18 @@ class HTMLFrameGenerator:
text: str,
image: str,
ext: Optional[Dict[str, Any]] = None,
width: int = 1080,
height: int = 1920,
output_path: Optional[str] = None
) -> str:
"""
Generate frame from HTML template
Video size is automatically determined from template path during initialization.
Args:
title: Video title
text: Narration text for this frame
image: Path to AI-generated image (supports relative path, absolute path, or HTTP URL)
ext: Additional data (content_title, content_author, etc.)
width: Frame width in pixels
height: Frame height in pixels
output_path: Custom output path (auto-generated if None)
Returns:
@@ -266,11 +270,11 @@ class HTMLFrameGenerator:
output_filename = os.path.basename(output_path)
output_dir = os.path.dirname(output_path)
# Ensure Html2Image is initialized
self._ensure_hti(width, height)
# Ensure Html2Image is initialized with template's size
self._ensure_hti(self.width, self.height)
# Render HTML to image
logger.debug(f"Rendering HTML template to {output_path}")
logger.debug(f"Rendering HTML template to {output_path} (size: {self.width}x{self.height})")
try:
self.hti.screenshot(
html_str=html,

View File

@@ -197,21 +197,10 @@ class FrameProcessor:
) -> str:
"""Compose frame using HTML template"""
from pixelle_video.services.frame_html import HTMLFrameGenerator
from pathlib import Path
from pixelle_video.utils.template_util import resolve_template_path
# Resolve template path
template_filename = config.frame_template
# Try templates/ directory first
template_path = Path(f"templates/{template_filename}")
if not template_path.exists():
# Try as absolute/relative path
template_path = Path(template_filename)
if not template_path.exists():
raise FileNotFoundError(
f"Template not found: {template_filename}. "
f"Built-in templates: default.html, modern.html, neon.html"
)
# Resolve template path (handles various input formats)
template_path = resolve_template_path(config.frame_template)
# Get content metadata from storyboard
content_metadata = storyboard.content_metadata if storyboard else None
@@ -224,15 +213,13 @@ class FrameProcessor:
ext["content_subtitle"] = content_metadata.subtitle or ""
ext["content_genre"] = content_metadata.genre or ""
# Generate frame using HTML
generator = HTMLFrameGenerator(str(template_path))
# Generate frame using HTML (size is auto-parsed from template path)
generator = HTMLFrameGenerator(template_path)
composed_path = await generator.generate_frame(
title=storyboard.title,
text=frame.narration,
image=frame.image_path,
ext=ext,
width=config.video_width,
height=config.video_height,
output_path=output_path
)

View File

@@ -72,11 +72,9 @@ class VideoGeneratorService:
image_workflow: Optional[str] = None,
# === Video Parameters ===
video_width: int = 1080,
video_height: int = 1920,
video_fps: int = 30,
# === Frame Template ===
# === Frame Template (determines video size) ===
frame_template: Optional[str] = None,
# === Image Style ===
@@ -128,12 +126,11 @@ class VideoGeneratorService:
image_height: Generated image height (default 1024)
image_workflow: Image workflow filename (e.g., "image_flux.json", None = use default)
video_width: Final video width (default 1080)
video_height: Final video height (default 1920)
video_fps: Video frame rate (default 30)
frame_template: HTML template filename or path (None = use default template)
e.g., "default.html", "modern.html", "neon.html", or custom path
frame_template: HTML template path with size (None = use default "1080x1920/default.html")
Format: "SIZExSIZE/template.html" (e.g., "1080x1920/default.html", "1920x1080/modern.html")
Video size is automatically determined from template path
prompt_prefix: Image prompt prefix (overrides config.yaml if provided)
e.g., "anime style, vibrant colors" or "" for no prefix
@@ -220,8 +217,6 @@ class VideoGeneratorService:
max_narration_words=max_narration_words,
min_image_prompt_words=min_image_prompt_words,
max_image_prompt_words=max_image_prompt_words,
video_width=video_width,
video_height=video_height,
video_fps=video_fps,
voice_id=voice_id,
tts_workflow=tts_workflow,
@@ -230,7 +225,7 @@ class VideoGeneratorService:
image_width=image_width,
image_height=image_height,
image_workflow=image_workflow,
frame_template=frame_template or "default.html"
frame_template=frame_template or "1080x1920/default.html"
)
# Create storyboard

View File

@@ -0,0 +1,207 @@
"""
Template utility functions for size parsing and template management
"""
import os
from pathlib import Path
from typing import List, Tuple, Optional
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)
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)

View File

@@ -666,24 +666,49 @@ def main():
st.markdown(f"**{tr('help.how')}**")
st.markdown(tr("template.how"))
# Dynamically scan templates folder for HTML files
templates_folder = Path("templates")
template_files = []
if templates_folder.exists():
template_files = sorted([f.name for f in templates_folder.glob("*.html")])
# Import template utilities
from pixelle_video.utils.template_util import list_available_sizes, list_templates_for_size
# Step 1: Select video size
VIDEO_SIZE_OPTIONS = {
"📱 竖屏视频 (1080×1920)": "1080x1920",
"🖥 横屏视频 (1920×1080)": "1920x1080",
"⬜ 方形视频 (1080×1080)": "1080x1080",
}
# Filter available sizes (only show sizes that exist)
available_sizes = list_available_sizes()
available_size_options = {k: v for k, v in VIDEO_SIZE_OPTIONS.items() if v in available_sizes}
if not available_size_options:
st.error("No template sizes found. Please ensure templates are in correct directory structure.")
st.stop()
selected_size_label = st.selectbox(
tr("template.video_size"),
list(available_size_options.keys()),
label_visibility="collapsed"
)
selected_size = available_size_options[selected_size_label]
# Step 2: Select template for the chosen size
template_files = list_templates_for_size(selected_size)
# Default to default.html if exists, otherwise first option
default_template_index = 0
if "default.html" in template_files:
default_template_index = template_files.index("default.html")
frame_template = st.selectbox(
"Template",
template_name = st.selectbox(
tr("template.style"),
template_files if template_files else ["default.html"],
index=default_template_index,
label_visibility="collapsed"
)
# Combine size and template name to get full path
frame_template = f"{selected_size}/{template_name}"
# Template preview expander
with st.expander(tr("template.preview_title"), expanded=False):
col1, col2 = st.columns(2)
@@ -709,26 +734,10 @@ def main():
key="preview_text"
)
# Size settings in a compact row
col3, col4 = st.columns(2)
with col3:
preview_width = st.number_input(
tr("template.preview_param_width"),
value=1080,
min_value=100,
max_value=4096,
step=10,
key="preview_width"
)
with col4:
preview_height = st.number_input(
tr("template.preview_param_height"),
value=1920,
min_value=100,
max_value=4096,
step=10,
key="preview_height"
)
# Info: Size is auto-determined from template
from pixelle_video.utils.template_util import parse_template_size
template_width, template_height = parse_template_size(f"templates/{frame_template}")
st.info(f"📐 {tr('template.size_info')}: {template_width} × {template_height}")
# Preview button
if st.button(tr("template.preview_button"), key="btn_preview_template", use_container_width=True):
@@ -736,17 +745,15 @@ def main():
try:
from pixelle_video.services.frame_html import HTMLFrameGenerator
# Use the currently selected template
# Use the currently selected template (size is auto-parsed)
template_path = f"templates/{frame_template}"
generator = HTMLFrameGenerator(template_path)
# Generate preview
# Generate preview (size is auto-determined from template)
preview_path = run_async(generator.generate_frame(
title=preview_title,
text=preview_text,
image=preview_image,
width=preview_width,
height=preview_height
image=preview_image
))
# Display preview
@@ -857,11 +864,17 @@ def main():
# Video information (compact display)
file_size_mb = result.file_size / (1024 * 1024)
# Parse video size from template path
from pixelle_video.utils.template_util import parse_template_size, resolve_template_path
template_path = resolve_template_path(result.storyboard.config.frame_template)
video_width, video_height = parse_template_size(template_path)
info_text = (
f"⏱️ {result.duration:.1f}s "
f"📦 {file_size_mb:.2f}MB "
f"🎬 {len(result.storyboard.frames)}{tr('info.scenes_unit')} "
f"📐 {result.storyboard.config.video_width}x{result.storyboard.config.video_height}"
f"📐 {video_width}x{video_height}"
)
st.caption(info_text)