更改视频尺寸的定义方式
This commit is contained in:
1 parent
14a31592e5
commit
8333397615
16 files changed
+284
-81
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user