From 2a07aa9567791b77fb8fb98d0721a559eb4bc662 Mon Sep 17 00:00:00 2001 From: puke <1129090915@qq.com> Date: Wed, 5 Nov 2025 23:01:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pixelle_video/models/storyboard.py | 3 +- pixelle_video/pipelines/standard.py | 6 +- pixelle_video/services/frame_html.py | 158 +++++++++++++++++++- pixelle_video/services/frame_processor.py | 4 + templates/1080x1920/modern.html | 6 +- web/app.py | 173 +++++++++++++++++++++- web/i18n/locales/en_US.json | 1 + web/i18n/locales/zh_CN.json | 1 + 8 files changed, 334 insertions(+), 18 deletions(-) diff --git a/pixelle_video/models/storyboard.py b/pixelle_video/models/storyboard.py index 402c079..8f541ad 100644 --- a/pixelle_video/models/storyboard.py +++ b/pixelle_video/models/storyboard.py @@ -4,7 +4,7 @@ Storyboard data models for video generation from dataclasses import dataclass, field from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Dict, Any @dataclass @@ -36,6 +36,7 @@ class StoryboardConfig: # Frame template (includes size information in path) frame_template: str = "1080x1920/default.html" # Template path with size (e.g., "1080x1920/default.html") + template_params: Optional[Dict[str, Any]] = None # Custom template parameters (e.g., {"accent_color": "#ff0000"}) @dataclass diff --git a/pixelle_video/pipelines/standard.py b/pixelle_video/pipelines/standard.py index 407e8f3..ca86944 100644 --- a/pixelle_video/pipelines/standard.py +++ b/pixelle_video/pipelines/standard.py @@ -7,7 +7,7 @@ This is the default pipeline for general-purpose video generation. from datetime import datetime from pathlib import Path -from typing import Optional, Callable, Literal +from typing import Optional, Callable, Literal, Dict, Any from loguru import logger @@ -84,6 +84,7 @@ class StandardPipeline(BasePipeline): # === Frame Template (determines video size) === frame_template: Optional[str] = None, + template_params: Optional[Dict[str, Any]] = None, # Custom template parameters # === Image Style === prompt_prefix: Optional[str] = None, @@ -206,7 +207,8 @@ class StandardPipeline(BasePipeline): image_width=image_width, image_height=image_height, image_workflow=image_workflow, - frame_template=frame_template or "1080x1920/default.html" + frame_template=frame_template or "1080x1920/default.html", + template_params=template_params ) # Create storyboard diff --git a/pixelle_video/services/frame_html.py b/pixelle_video/services/frame_html.py index 567bfa8..6b7883c 100644 --- a/pixelle_video/services/frame_html.py +++ b/pixelle_video/services/frame_html.py @@ -12,6 +12,7 @@ Linux Environment Requirements: """ import os +import re import uuid from typing import Dict, Any, Optional from pathlib import Path @@ -103,6 +104,156 @@ class HTMLFrameGenerator: logger.debug(f"Template loaded: {len(content)} chars") return content + def parse_template_parameters(self) -> Dict[str, Dict[str, Any]]: + """ + Parse custom parameters from HTML template + + Supports syntax: {{param:type=default}} + - {{param}} -> text type, no default + - {{param=value}} -> text type, with default + - {{param:type}} -> specified type, no default + - {{param:type=value}} -> specified type, with default + + Supported types: text, number, color, bool + + Returns: + Dictionary of custom parameters with their configurations: + { + 'param_name': { + 'type': 'text' | 'number' | 'color' | 'bool', + 'default': Any, + 'label': str # same as param_name + } + } + """ + # Preset parameters that should be ignored + PRESET_PARAMS = {'title', 'text', 'image', 'content_title', 'content_author', + 'content_subtitle', 'content_genre'} + + # Pattern: {{param_name:type=default}} or {{param_name=default}} or {{param_name:type}} or {{param_name}} + # Param name: must start with letter or underscore, can contain letters, digits, underscores + PARAM_PATTERN = r'\{\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-z]+))?(?:=([^}]+))?\}\}' + + params = {} + + for match in re.finditer(PARAM_PATTERN, self.template): + param_name = match.group(1) + param_type = match.group(2) or 'text' # Default to text + default_value = match.group(3) + + # Skip preset parameters + if param_name in PRESET_PARAMS: + continue + + # Skip if already parsed (use first occurrence) + if param_name in params: + continue + + # Validate type + if param_type not in {'text', 'number', 'color', 'bool'}: + logger.warning(f"Unknown parameter type '{param_type}' for '{param_name}', defaulting to 'text'") + param_type = 'text' + + # Parse default value based on type + parsed_default = self._parse_default_value(param_type, default_value) + + params[param_name] = { + 'type': param_type, + 'default': parsed_default, + 'label': param_name, # Use param name as label + } + + if params: + logger.debug(f"Parsed {len(params)} custom parameter(s) from template: {list(params.keys())}") + + return params + + def _parse_default_value(self, param_type: str, value_str: Optional[str]) -> Any: + """ + Parse default value based on parameter type + + Args: + param_type: Type of parameter (text, number, color, bool) + value_str: String value to parse (can be None) + + Returns: + Parsed value with appropriate type + """ + if value_str is None: + # No default value specified, return type-specific defaults + return { + 'text': '', + 'number': 0, + 'color': '#000000', + 'bool': False, + }.get(param_type, '') + + if param_type == 'number': + try: + # Try int first, then float + if '.' in value_str: + return float(value_str) + else: + return int(value_str) + except ValueError: + logger.warning(f"Invalid number value '{value_str}', using 0") + return 0 + + elif param_type == 'bool': + # Accept: true/false, 1/0, yes/no, on/off (case-insensitive) + return value_str.lower() in {'true', '1', 'yes', 'on'} + + elif param_type == 'color': + # Auto-add # if missing + if value_str.startswith('#'): + return value_str + else: + return f'#{value_str}' + + else: # text + return value_str + + def _replace_parameters(self, html: str, values: Dict[str, Any]) -> str: + """ + Replace parameter placeholders with actual values + + Supports DSL syntax: {{param:type=default}} + - If value provided in values dict, use it + - Otherwise, use default value from placeholder + - If no default, use empty string + + Args: + html: HTML template content + values: Dictionary of parameter values + + Returns: + HTML with placeholders replaced + """ + PARAM_PATTERN = r'\{\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-z]+))?(?:=([^}]+))?\}\}' + + def replacer(match): + param_name = match.group(1) + param_type = match.group(2) or 'text' + default_value_str = match.group(3) + + # Check if value is provided + if param_name in values: + value = values[param_name] + # Convert bool to string for HTML + if isinstance(value, bool): + return 'true' if value else 'false' + return str(value) if value is not None else '' + + # Use default value from placeholder if available + elif default_value_str: + return default_value_str + + # No value and no default + else: + return '' + + return re.sub(PARAM_PATTERN, replacer, html) + def _find_chrome_executable(self) -> Optional[str]: """ Find suitable Chrome/Chromium executable, preferring non-snap versions @@ -248,11 +399,8 @@ class HTMLFrameGenerator: if ext: context.update(ext) - # Replace variables in HTML - html = self.template - for key, value in context.items(): - placeholder = f"{{{{{key}}}}}" - html = html.replace(placeholder, str(value) if value is not None else "") + # Replace variables in HTML (supports DSL syntax: {{param:type=default}}) + html = self._replace_parameters(self.template, context) # Use provided output path or auto-generate if output_path is None: diff --git a/pixelle_video/services/frame_processor.py b/pixelle_video/services/frame_processor.py index c10127b..3472d4b 100644 --- a/pixelle_video/services/frame_processor.py +++ b/pixelle_video/services/frame_processor.py @@ -213,6 +213,10 @@ class FrameProcessor: ext["content_subtitle"] = content_metadata.subtitle or "" ext["content_genre"] = content_metadata.genre or "" + # Add custom template parameters + if config.template_params: + ext.update(config.template_params) + # Generate frame using HTML (size is auto-parsed from template path) generator = HTMLFrameGenerator(template_path) composed_path = await generator.generate_frame( diff --git a/templates/1080x1920/modern.html b/templates/1080x1920/modern.html index 13d590e..1d33e33 100644 --- a/templates/1080x1920/modern.html +++ b/templates/1080x1920/modern.html @@ -12,7 +12,7 @@ margin: 0; padding: 0; width: 1080px; - background: #764ba2; + background: {{accent_color:color=#764ba2}}; font-family: 'PingFang SC', 'Source Han Sans', 'Microsoft YaHei', sans-serif; position: relative; overflow: hidden; @@ -174,7 +174,7 @@ .video-title-dot:nth-child(3) { background: #FFE66D; } .video-title { - font-size: 72px; + font-size: {{title_font_size:number=72}}px; font-weight: bold; color: white; line-height: 1.3; @@ -558,7 +558,7 @@