更改视频尺寸的定义方式

This commit is contained in:
puke committed 2025-11-07 16:59:12 +08:00
1 parent 14a31592e5
commit 8333397615
16 files changed
+284 -81

No files matched your search

-2
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,
+5 -4
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")
+3 -5
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
+13 -9
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,
+5 -18
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
)
+5 -10
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
+207
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)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+46 -33
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)