webui适配视频功能; 统一模板命名规范;

This commit is contained in:
puke
2025-11-12 20:01:09 +08:00
parent 7443cbf9c2
commit f7e3162a4a
26 changed files with 272 additions and 90 deletions

View File

@@ -38,14 +38,28 @@ comfyui:
# Image prompt prefix (optional) # Image prompt prefix (optional)
prompt_prefix: "Minimalist black-and-white matchstick figure style illustration, clean lines, simple sketch style" prompt_prefix: "Minimalist black-and-white matchstick figure style illustration, clean lines, simple sketch style"
# Video-specific configuration
video:
# Required: Default workflow to use (no fallback)
# Options: runninghub/video_wan2.1_fusionx.json (recommended, no local setup)
# selfhost/video_wan2.1_fusionx.json (requires local ComfyUI)
default_workflow: runninghub/video_wan2.1_fusionx.json
# Video prompt prefix (optional)
prompt_prefix: "Minimalist black-and-white matchstick figure style illustration, clean lines, simple sketch style"
# ==================== Template Configuration ==================== # ==================== Template Configuration ====================
# Configure default template for video generation # Configure default template for video generation
template: template:
# Default frame template to use when not explicitly specified # Default frame template to use when not explicitly specified
# Determines video aspect ratio and layout style # Determines video aspect ratio and layout style
# Template naming convention:
# - static_*.html: Static style templates (no AI-generated media)
# - image_*.html: Templates requiring AI-generated images
# - video_*.html: Templates requiring AI-generated videos
# Options: # Options:
# - 1080x1920 (vertical/portrait): default.html, modern.html, elegant.html, etc. # - 1080x1920 (vertical/portrait): image_default.html, image_modern.html, image_elegant.html, static_simple.html, etc.
# - 1080x1080 (square): minimal_framed.html, magazine_cover.html, etc. # - 1080x1080 (square): image_minimal_framed.html, etc.
# - 1920x1080 (horizontal/landscape): film.html, full.html, etc. # - 1920x1080 (horizontal/landscape): image_film.html, image_full.html, etc.
# See templates/ directory for all available templates # See templates/ directory for all available templates
default_template: "1080x1920/default.html" default_template: "1080x1920/image_default.html"

View File

@@ -117,9 +117,10 @@ class CustomPipeline(BasePipeline):
VideoGenerationResult VideoGenerationResult
Image Generation Logic: Image Generation Logic:
- If template has {{image}} → automatically generates images - image_*.html templates → automatically generates images
- If template has no {{image}} → skips image generation (faster, cheaper) - video_*.html templates → automatically generates videos
- To customize: Override the template_requires_image logic in your subclass - static_*.html templates → skips media generation (faster, cheaper)
- To customize: Override the template type detection logic in your subclass
""" """
logger.info("Starting CustomPipeline") logger.info("Starting CustomPipeline")
logger.info(f"Input text length: {len(text)} chars") logger.info(f"Input text length: {len(text)} chars")
@@ -151,23 +152,27 @@ class CustomPipeline(BasePipeline):
frame_template = template_config.get("default_template", "1080x1920/default.html") frame_template = template_config.get("default_template", "1080x1920/default.html")
# ========== Step 0.5: Check template requirements ========== # ========== Step 0.5: Check template requirements ==========
# Detect if template requires {{image}} parameter # Detect template type by filename prefix
# This allows skipping the entire image generation pipeline for text-only templates from pathlib import Path
from pixelle_video.services.frame_html import HTMLFrameGenerator from pixelle_video.services.frame_html import HTMLFrameGenerator
from pixelle_video.utils.template_util import resolve_template_path from pixelle_video.utils.template_util import resolve_template_path, get_template_type
template_path = resolve_template_path(frame_template) template_name = Path(frame_template).name
generator = HTMLFrameGenerator(template_path) template_type = get_template_type(template_name)
template_requires_image = generator.requires_image() template_requires_image = (template_type == "image")
# Read media size from template meta tags # Read media size from template meta tags
template_path = resolve_template_path(frame_template)
generator = HTMLFrameGenerator(template_path)
image_width, image_height = generator.get_media_size() image_width, image_height = generator.get_media_size()
logger.info(f"📐 Media size from template: {image_width}x{image_height}") logger.info(f"📐 Media size from template: {image_width}x{image_height}")
if template_requires_image: if template_type == "image":
logger.info(f"📸 Template requires image generation") logger.info(f"📸 Template requires image generation")
else: elif template_type == "video":
logger.info(f" Template does not require images - skipping image generation pipeline") logger.info(f"🎬 Template requires video generation")
else: # static
logger.info(f"⚡ Static template - skipping media generation pipeline")
logger.info(f" 💡 Benefits: Faster generation + Lower cost + No ComfyUI dependency") logger.info(f" 💡 Benefits: Faster generation + Lower cost + No ComfyUI dependency")
# ========== Step 1: Process content (CUSTOMIZE THIS) ========== # ========== Step 1: Process content (CUSTOMIZE THIS) ==========
@@ -197,8 +202,8 @@ class CustomPipeline(BasePipeline):
# ========== Step 2: Generate image prompts (CONDITIONAL - CUSTOMIZE THIS) ========== # ========== Step 2: Generate image prompts (CONDITIONAL - CUSTOMIZE THIS) ==========
self._report_progress(progress_callback, "generating_image_prompts", 0.25) self._report_progress(progress_callback, "generating_image_prompts", 0.25)
# IMPORTANT: Check if template actually needs images # IMPORTANT: Check if template is image type
# If your template doesn't use {{image}}, you can skip this entire step! # If your template is static_*.html, you can skip this entire step!
if template_requires_image: if template_requires_image:
# Template requires images - generate image prompts using LLM # Template requires images - generate image prompts using LLM
from pixelle_video.utils.content_generators import generate_image_prompts from pixelle_video.utils.content_generators import generate_image_prompts

View File

@@ -282,8 +282,8 @@ class StandardPipeline(BasePipeline):
logger.info(f"🎬 Template requires video generation") logger.info(f"🎬 Template requires video generation")
elif template_media_type == "image": elif template_media_type == "image":
logger.info(f"📸 Template requires image generation") logger.info(f"📸 Template requires image generation")
else: # text else: # static
logger.info(f"Template does not require media - skipping media generation pipeline") logger.info(f"Static template - skipping media generation pipeline")
logger.info(f" 💡 Benefits: Faster generation + Lower cost + No ComfyUI dependency") logger.info(f" 💡 Benefits: Faster generation + Lower cost + No ComfyUI dependency")
try: try:
@@ -525,35 +525,23 @@ class StandardPipeline(BasePipeline):
- Media generation API calls - Media generation API calls
- ComfyUI dependency - ComfyUI dependency
Template naming rules: Template naming convention:
- static_*.html: Static style template (returns "static")
- image_*.html: Image template (returns "image")
- video_*.html: Video template (returns "video") - video_*.html: Video template (returns "video")
- Other templates with {{image}}: Image template (returns "image")
- Other templates without {{image}}: Text-only template (returns "text")
Args: Args:
frame_template: Template path (e.g., "1080x1920/default.html" or "1080x1920/video_default.html") frame_template: Template path (e.g., "1080x1920/image_default.html" or "1080x1920/video_default.html")
Returns: Returns:
"video", "image", or "text" "static", "image", or "video"
""" """
from pixelle_video.services.frame_html import HTMLFrameGenerator from pixelle_video.utils.template_util import get_template_type
from pixelle_video.utils.template_util import resolve_template_path
# Check if template name starts with video_ # Determine type by template filename prefix
template_name = Path(frame_template).name template_name = Path(frame_template).name
if template_name.startswith("video_"): template_type = get_template_type(template_name)
logger.debug(f"Template '{frame_template}' is video template (video_ prefix)")
return "video"
# Check if template contains {{image}} logger.debug(f"Template '{frame_template}' is {template_type} template")
template_path = resolve_template_path(frame_template) return template_type
generator = HTMLFrameGenerator(template_path)
requires_image = generator.requires_image()
if requires_image:
logger.debug(f"Template '{frame_template}' is image template (has {{image}})")
return "image"
else:
logger.debug(f"Template '{frame_template}' is text-only template")
return "text"

View File

@@ -77,21 +77,6 @@ class HTMLFrameGenerator:
self._check_linux_dependencies() self._check_linux_dependencies()
logger.debug(f"Loaded HTML template: {template_path} (size: {self.width}x{self.height})") logger.debug(f"Loaded HTML template: {template_path} (size: {self.width}x{self.height})")
def requires_image(self) -> bool:
"""
Detect if template requires {{image}} parameter
This method checks if the template uses the {{image}} variable.
If the template doesn't use images, the entire image generation
pipeline can be skipped, significantly improving:
- Generation speed (no image generation API calls)
- Cost efficiency (no LLM calls for image prompts)
- Dependency requirements (no ComfyUI needed)
Returns:
True if template contains {{image}}, False otherwise
"""
return '{{image}}' in self.template
def _check_linux_dependencies(self): def _check_linux_dependencies(self):
"""Check Linux system dependencies and warn if missing""" """Check Linux system dependencies and warn if missing"""

View File

@@ -18,6 +18,7 @@ import os
from pathlib import Path from pathlib import Path
from typing import List, Tuple, Optional, Literal from typing import List, Tuple, Optional, Literal
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import logging
from pixelle_video.utils.os_util import ( from pixelle_video.utils.os_util import (
get_resource_path, get_resource_path,
@@ -26,6 +27,8 @@ from pixelle_video.utils.os_util import (
resource_exists resource_exists
) )
logger = logging.getLogger(__name__)
def parse_template_size(template_path: str) -> Tuple[int, int]: def parse_template_size(template_path: str) -> Tuple[int, int]:
""" """
@@ -316,7 +319,7 @@ def resolve_template_path(template_input: Optional[str]) -> str:
Args: Args:
template_input: Can be: template_input: Can be:
- None: Use default "1080x1920/default.html" - None: Use default "1080x1920/image_default.html"
- "template.html": Use default size + this template - "template.html": Use default size + this template
- "1080x1920/template.html": Full relative path - "1080x1920/template.html": Full relative path
- "templates/1080x1920/template.html": Absolute-ish path (legacy) - "templates/1080x1920/template.html": Absolute-ish path (legacy)
@@ -330,15 +333,15 @@ def resolve_template_path(template_input: Optional[str]) -> str:
Examples: Examples:
>>> resolve_template_path(None) >>> resolve_template_path(None)
'templates/1080x1920/default.html' 'templates/1080x1920/image_default.html'
>>> resolve_template_path("modern.html") >>> resolve_template_path("image_modern.html")
'templates/1080x1920/modern.html' 'templates/1080x1920/image_modern.html'
>>> resolve_template_path("1920x1080/default.html") >>> resolve_template_path("1920x1080/image_default.html")
'templates/1920x1080/default.html' 'templates/1920x1080/image_default.html'
""" """
# Default case # Default case
if template_input is None: if template_input is None:
template_input = "1080x1920/default.html" template_input = "1080x1920/image_default.html"
# Parse input to extract size and template name # Parse input to extract size and template name
size = None size = None
@@ -359,6 +362,18 @@ def resolve_template_path(template_input: Optional[str]) -> str:
size = "1080x1920" size = "1080x1920"
template_name = template_input template_name = template_input
# Backward compatibility: migrate "default.html" to "image_default.html"
if template_name == "default.html":
migrated_name = "image_default.html"
try:
# Try migrated name first
path = get_resource_path("templates", size, migrated_name)
logger.info(f"Backward compatibility: migrated '{template_input}' to '{size}/{migrated_name}'")
return path
except FileNotFoundError:
# Fall through to try original name
logger.warning(f"Migrated template '{size}/{migrated_name}' not found, trying original name")
# Use resource API to resolve path (custom > default) # Use resource API to resolve path (custom > default)
try: try:
return get_resource_path("templates", size, template_name) return get_resource_path("templates", size, template_name)
@@ -367,6 +382,120 @@ def resolve_template_path(template_input: Optional[str]) -> str:
raise FileNotFoundError( raise FileNotFoundError(
f"Template not found: {size}/{template_name}\n" f"Template not found: {size}/{template_name}\n"
f"Available sizes: {available_sizes}\n" f"Available sizes: {available_sizes}\n"
f"Hint: Use format 'SIZExSIZE/template.html' (e.g., '1080x1920/default.html')" f"Hint: Use format 'SIZExSIZE/template.html' (e.g., '1080x1920/image_default.html')"
) )
def get_template_type(template_name: str) -> Literal['static', 'image', 'video']:
"""
Detect template type from template filename
Template naming convention:
- static_*.html: Static style templates (no AI-generated media)
- image_*.html: Templates requiring AI-generated images
- video_*.html: Templates requiring AI-generated videos
Args:
template_name: Template filename like "image_default.html" or "video_simple.html"
Returns:
Template type: 'static', 'image', or 'video'
Examples:
>>> get_template_type("static_simple.html")
'static'
>>> get_template_type("image_default.html")
'image'
>>> get_template_type("video_simple.html")
'video'
"""
name = Path(template_name).name
if name.startswith("static_"):
return "static"
elif name.startswith("video_"):
return "video"
elif name.startswith("image_"):
return "image"
else:
# Fallback: try to detect from legacy names
logger.warning(
f"Template '{template_name}' doesn't follow naming convention (static_/image_/video_). "
f"Defaulting to 'image' type."
)
return "image"
def filter_templates_by_type(
templates: List[TemplateInfo],
template_type: Literal['static', 'image', 'video']
) -> List[TemplateInfo]:
"""
Filter templates by type
Args:
templates: List of TemplateInfo objects
template_type: Type to filter by ('static', 'image', or 'video')
Returns:
Filtered list of TemplateInfo objects
Examples:
>>> all_templates = get_all_templates_with_info()
>>> image_templates = filter_templates_by_type(all_templates, 'image')
>>> len(image_templates) > 0
True
"""
filtered = []
for t in templates:
template_name = t.display_info.name
if get_template_type(template_name) == template_type:
filtered.append(t)
return filtered
def get_templates_grouped_by_size_and_type(
template_type: Optional[Literal['static', 'image', 'video']] = None
) -> dict:
"""
Get templates grouped by size, optionally filtered by type
Args:
template_type: Optional type filter ('static', 'image', or 'video')
Returns:
Dict with size as key, list of TemplateInfo as value
Ordered by orientation priority: portrait > landscape > square
Examples:
>>> # Get all templates
>>> all_grouped = get_templates_grouped_by_size_and_type()
>>> # Get only image templates
>>> image_grouped = get_templates_grouped_by_size_and_type('image')
"""
from collections import defaultdict
templates = get_all_templates_with_info()
# Filter by type if specified
if template_type is not None:
templates = filter_templates_by_type(templates, template_type)
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

View File

@@ -684,13 +684,41 @@ def main():
st.markdown(f"🔗 [{tr('template.preview_link')}]({template_docs_url})") st.markdown(f"🔗 [{tr('template.preview_link')}]({template_docs_url})")
# Import template utilities # Import template utilities
from pixelle_video.utils.template_util import get_templates_grouped_by_size from pixelle_video.utils.template_util import get_templates_grouped_by_size_and_type, get_template_type
# Get templates grouped by size # Template type selector
grouped_templates = get_templates_grouped_by_size() st.markdown(f"**{tr('template.type_selector')}**")
template_type_options = {
'static': tr('template.type.static'),
'image': tr('template.type.image'),
'video': tr('template.type.video')
}
# Radio buttons in horizontal layout
selected_template_type = st.radio(
tr('template.type_selector'),
options=list(template_type_options.keys()),
format_func=lambda x: template_type_options[x],
index=1, # Default to 'image'
key="template_type_selector",
label_visibility="collapsed",
horizontal=True
)
# Display hint based on selected type (below radio buttons)
if selected_template_type == 'static':
st.info(tr('template.type.static_hint'))
elif selected_template_type == 'image':
st.info(tr('template.type.image_hint'))
elif selected_template_type == 'video':
st.info(tr('template.type.video_hint'))
# Get templates grouped by size, filtered by selected type
grouped_templates = get_templates_grouped_by_size_and_type(selected_template_type)
if not grouped_templates: if not grouped_templates:
st.error("No templates found. Please ensure templates are in templates/ directory with proper structure (e.g., templates/1080x1920/default.html).") st.warning(f"No {template_type_options[selected_template_type]} templates found. Please select a different type or add templates.")
st.stop() st.stop()
# Build display options with group separators # Build display options with group separators
@@ -707,7 +735,19 @@ def main():
# Get default template from config # Get default template from config
template_config = pixelle_video.config.get("template", {}) template_config = pixelle_video.config.get("template", {})
config_default_template = template_config.get("default_template", "1080x1920/default.html") config_default_template = template_config.get("default_template", "1080x1920/image_default.html")
# Backward compatibility
if config_default_template == "1080x1920/default.html":
config_default_template = "1080x1920/image_default.html"
# Determine type-specific default template
type_default_templates = {
'static': '1080x1920/static_default.html',
'image': '1080x1920/image_default.html',
'video': '1080x1920/video_default.html'
}
type_specific_default = type_default_templates.get(selected_template_type, config_default_template)
for size, templates in grouped_templates.items(): for size, templates in grouped_templates.items():
if not templates: if not templates:
@@ -733,10 +773,12 @@ def main():
display_options.append(display_name) display_options.append(display_name)
template_paths_ordered.append(t.template_path) # Add to ordered list template_paths_ordered.append(t.template_path) # Add to ordered list
# Set default based on config (priority: config > first default.html in portrait) # Set default: priority is config > type-specific default > first in portrait
if t.template_path == config_default_template: if t.template_path == config_default_template:
default_index = current_index default_index = current_index
elif default_index == 0 and "default.html" in t.display_info.name and t.display_info.orientation == 'portrait': elif default_index == 0 and t.template_path == type_specific_default:
default_index = current_index
elif default_index == 0 and t.display_info.orientation == 'portrait':
default_index = current_index default_index = current_index
current_index += 1 current_index += 1
@@ -789,20 +831,11 @@ def main():
# Detect template media type # Detect template media type
from pathlib import Path from pathlib import Path
template_name = Path(frame_template).name from pixelle_video.utils.template_util import get_template_type
if template_name.startswith("video_"): template_name = Path(frame_template).name
# Video template template_media_type = get_template_type(template_name)
template_media_type = "video" template_requires_media = (template_media_type in ["image", "video"])
template_requires_media = True
elif generator_for_params.requires_image():
# Image template
template_media_type = "image"
template_requires_media = True
else:
# Text-only template
template_media_type = "text"
template_requires_media = False
# Store in session state for workflow filtering # Store in session state for workflow filtering
st.session_state['template_media_type'] = template_media_type st.session_state['template_media_type'] = template_media_type
@@ -1009,7 +1042,9 @@ def main():
# If user has a saved preference in config, try to match it # If user has a saved preference in config, try to match it
comfyui_config = config_manager.get_comfyui_config() comfyui_config = config_manager.get_comfyui_config()
saved_workflow = comfyui_config["image"]["default_workflow"] # Select config based on template type (image or video)
media_config_key = "video" if template_media_type == "video" else "image"
saved_workflow = comfyui_config.get(media_config_key, {}).get("default_workflow", "")
if saved_workflow and saved_workflow in workflow_keys: if saved_workflow and saved_workflow in workflow_keys:
default_workflow_index = workflow_keys.index(saved_workflow) default_workflow_index = workflow_keys.index(saved_workflow)
@@ -1040,8 +1075,8 @@ def main():
st.info(f"📐 {size_info_text}") st.info(f"📐 {size_info_text}")
# Prompt prefix input # Prompt prefix input
# Get current prompt_prefix from config # Get current prompt_prefix from config (based on media type)
current_prefix = comfyui_config["image"]["prompt_prefix"] current_prefix = comfyui_config.get(media_config_key, {}).get("prompt_prefix", "")
# Prompt prefix input (temporary, not saved to config) # Prompt prefix input (temporary, not saved to config)
prompt_prefix = st.text_area( prompt_prefix = st.text_area(
@@ -1268,6 +1303,18 @@ def main():
# Video preview # Video preview
if os.path.exists(result.video_path): if os.path.exists(result.video_path):
st.video(result.video_path) st.video(result.video_path)
# Download button
with open(result.video_path, "rb") as video_file:
video_bytes = video_file.read()
video_filename = os.path.basename(result.video_path)
st.download_button(
label="⬇️ 下载视频" if get_language() == "zh_CN" else "⬇️ Download Video",
data=video_bytes,
file_name=video_filename,
mime="video/mp4",
use_container_width=True
)
else: else:
st.error(tr("status.video_not_found", path=result.video_path)) st.error(tr("status.video_not_found", path=result.video_path))

View File

@@ -86,8 +86,15 @@
"template.modern": "Modern", "template.modern": "Modern",
"template.neon": "Neon", "template.neon": "Neon",
"template.what": "Controls the visual layout and design style of each frame (title, text, image arrangement)", "template.what": "Controls the visual layout and design style of each frame (title, text, image arrangement)",
"template.how": "Place .html template files in templates/SIZE/ directories (e.g., templates/1080x1920/). Templates are automatically grouped by size. Custom CSS styles are supported.\n\n**Note**\n\nAt least one of the following browsers must be installed on your computer for proper operation:\n1. Google Chrome (Windows, macOS)\n2. Chromium Browser (Linux)\n3. Microsoft Edge", "template.how": "Place .html template files in templates/SIZE/ directories (e.g., templates/1080x1920/). Templates are automatically grouped by size. Custom CSS styles are supported.\n\n**Template Naming Convention**\n\n- `static_*.html` → Static style templates (no AI-generated media)\n- `image_*.html` → Image generation templates (AI-generated images)\n- `video_*.html` → Video generation templates (AI-generated videos)\n\n**Note**\n\nAt least one of the following browsers must be installed on your computer for proper operation:\n1. Google Chrome (Windows, macOS)\n2. Chromium Browser (Linux)\n3. Microsoft Edge",
"template.size_info": "Template Size", "template.size_info": "Template Size",
"template.type_selector": "Template Type",
"template.type.static": "📄 Static Style",
"template.type.image": "🖼️ Generate Images",
"template.type.video": "🎬 Generate Videos",
"template.type.static_hint": "Uses template's built-in styles, no AI-generated media required. You can customize background images and other parameters in the template.",
"template.type.image_hint": "AI automatically generates illustrations matching the narration content. Image size is determined by the template.",
"template.type.video_hint": "AI automatically generates video clips matching the narration content. Video size is determined by the template.",
"orientation.portrait": "Portrait", "orientation.portrait": "Portrait",
"orientation.landscape": "Landscape", "orientation.landscape": "Landscape",

View File

@@ -86,8 +86,15 @@
"template.modern": "现代", "template.modern": "现代",
"template.neon": "霓虹", "template.neon": "霓虹",
"template.what": "控制视频每一帧的视觉布局和设计风格(标题、文本、图片的排版样式)", "template.what": "控制视频每一帧的视觉布局和设计风格(标题、文本、图片的排版样式)",
"template.how": "将 .html 模板文件放入 templates/尺寸/ 目录(如 templates/1080x1920/),系统会自动按尺寸分组。支持自定义 CSS 样式。\n\n**注意**\n\n您的计算机上必须安装以下至少一种浏览器才能正常运行\n1. Google ChromeWindows、MacOS\n2. Chromium 浏览器Linux\n3. Microsoft Edge", "template.how": "将 .html 模板文件放入 templates/尺寸/ 目录(如 templates/1080x1920/),系统会自动按尺寸分组。支持自定义 CSS 样式。\n\n**模板命名规范**\n\n- `static_*.html` → 静态样式模板无需AI生成媒体\n- `image_*.html` → 生成插图模板AI生成图片\n- `video_*.html` → 生成视频模板AI生成视频\n\n**注意**\n\n您的计算机上必须安装以下至少一种浏览器才能正常运行\n1. Google ChromeWindows、MacOS\n2. Chromium 浏览器Linux\n3. Microsoft Edge",
"template.size_info": "模板尺寸", "template.size_info": "模板尺寸",
"template.type_selector": "分镜类型",
"template.type.static": "📄 静态样式",
"template.type.image": "🖼️ 生成插图",
"template.type.video": "🎬 生成视频",
"template.type.static_hint": "使用模板自带样式无需AI生成媒体。可在模板中自定义背景图片等参数。",
"template.type.image_hint": "AI自动根据文案内容生成与之匹配的插图插图尺寸由模板决定。",
"template.type.video_hint": "AI自动根据文案内容生成与之匹配的视频片段视频尺寸由模板决定。",
"orientation.portrait": "竖屏", "orientation.portrait": "竖屏",
"orientation.landscape": "横屏", "orientation.landscape": "横屏",