From f7e3162a4a10a3a3f7758d6bb5f89408febc7253 Mon Sep 17 00:00:00 2001 From: puke <1129090915@qq.com> Date: Wed, 12 Nov 2025 20:01:09 +0800 Subject: [PATCH] =?UTF-8?q?webui=E9=80=82=E9=85=8D=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E5=8A=9F=E8=83=BD;=20=E7=BB=9F=E4=B8=80=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.example.yaml | 22 ++- pixelle_video/pipelines/custom.py | 33 ++-- pixelle_video/pipelines/standard.py | 36 ++--- pixelle_video/services/frame_html.py | 15 -- pixelle_video/utils/template_util.py | 145 +++++++++++++++++- ..._framed.html => image_minimal_framed.html} | 0 .../{blur_card.html => image_blur_card.html} | 0 .../{cartoon.html => image_cartoon.html} | 0 .../{default.html => image_default.html} | 0 .../{elegant.html => image_elegant.html} | 0 ...intage.html => image_fashion_vintage.html} | 0 .../1080x1920/{full.html => image_full.html} | 0 ...insights.html => image_life_insights.html} | 0 .../{modern.html => image_modern.html} | 0 .../1080x1920/{neon.html => image_neon.html} | 0 ...y_card.html => image_psychology_card.html} | 0 .../{purple.html => image_purple.html} | 0 .../{simple.html => static_default.html} | 0 .../{video_simple.html => video_default.html} | 0 .../1920x1080/{film.html => image_film.html} | 0 .../1920x1080/{full.html => image_full.html} | 0 ...imal.html => image_ultrawide_minimal.html} | 0 ...darktech.html => image_wide_darktech.html} | 0 web/app.py | 93 ++++++++--- web/i18n/locales/en_US.json | 9 +- web/i18n/locales/zh_CN.json | 9 +- 26 files changed, 272 insertions(+), 90 deletions(-) rename templates/1080x1080/{minimal_framed.html => image_minimal_framed.html} (100%) rename templates/1080x1920/{blur_card.html => image_blur_card.html} (100%) rename templates/1080x1920/{cartoon.html => image_cartoon.html} (100%) rename templates/1080x1920/{default.html => image_default.html} (100%) rename templates/1080x1920/{elegant.html => image_elegant.html} (100%) rename templates/1080x1920/{fashion_vintage.html => image_fashion_vintage.html} (100%) rename templates/1080x1920/{full.html => image_full.html} (100%) rename templates/1080x1920/{life_insights.html => image_life_insights.html} (100%) rename templates/1080x1920/{modern.html => image_modern.html} (100%) rename templates/1080x1920/{neon.html => image_neon.html} (100%) rename templates/1080x1920/{psychology_card.html => image_psychology_card.html} (100%) rename templates/1080x1920/{purple.html => image_purple.html} (100%) rename templates/1080x1920/{simple.html => static_default.html} (100%) rename templates/1080x1920/{video_simple.html => video_default.html} (100%) rename templates/1920x1080/{film.html => image_film.html} (100%) rename templates/1920x1080/{full.html => image_full.html} (100%) rename templates/1920x1080/{ultrawide_minimal.html => image_ultrawide_minimal.html} (100%) rename templates/1920x1080/{wide_darktech.html => image_wide_darktech.html} (100%) diff --git a/config.example.yaml b/config.example.yaml index 0ecffec..02e68be 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -37,15 +37,29 @@ comfyui: # Image prompt prefix (optional) 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 ==================== # Configure default template for video generation template: # Default frame template to use when not explicitly specified # 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: - # - 1080x1920 (vertical/portrait): default.html, modern.html, elegant.html, etc. - # - 1080x1080 (square): minimal_framed.html, magazine_cover.html, etc. - # - 1920x1080 (horizontal/landscape): film.html, full.html, etc. + # - 1080x1920 (vertical/portrait): image_default.html, image_modern.html, image_elegant.html, static_simple.html, etc. + # - 1080x1080 (square): image_minimal_framed.html, etc. + # - 1920x1080 (horizontal/landscape): image_film.html, image_full.html, etc. # See templates/ directory for all available templates - default_template: "1080x1920/default.html" + default_template: "1080x1920/image_default.html" diff --git a/pixelle_video/pipelines/custom.py b/pixelle_video/pipelines/custom.py index ce6dbe3..a453de4 100644 --- a/pixelle_video/pipelines/custom.py +++ b/pixelle_video/pipelines/custom.py @@ -117,9 +117,10 @@ class CustomPipeline(BasePipeline): VideoGenerationResult Image Generation Logic: - - If template has {{image}} → automatically generates images - - If template has no {{image}} → skips image generation (faster, cheaper) - - To customize: Override the template_requires_image logic in your subclass + - image_*.html templates → automatically generates images + - video_*.html templates → automatically generates videos + - 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(f"Input text length: {len(text)} chars") @@ -151,23 +152,27 @@ class CustomPipeline(BasePipeline): frame_template = template_config.get("default_template", "1080x1920/default.html") # ========== Step 0.5: Check template requirements ========== - # Detect if template requires {{image}} parameter - # This allows skipping the entire image generation pipeline for text-only templates + # Detect template type by filename prefix + from pathlib import Path 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) - generator = HTMLFrameGenerator(template_path) - template_requires_image = generator.requires_image() + template_name = Path(frame_template).name + template_type = get_template_type(template_name) + template_requires_image = (template_type == "image") # 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() 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") - else: - logger.info(f"⚡ Template does not require images - skipping image generation pipeline") + elif template_type == "video": + 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") # ========== Step 1: Process content (CUSTOMIZE THIS) ========== @@ -197,8 +202,8 @@ class CustomPipeline(BasePipeline): # ========== Step 2: Generate image prompts (CONDITIONAL - CUSTOMIZE THIS) ========== self._report_progress(progress_callback, "generating_image_prompts", 0.25) - # IMPORTANT: Check if template actually needs images - # If your template doesn't use {{image}}, you can skip this entire step! + # IMPORTANT: Check if template is image type + # If your template is static_*.html, you can skip this entire step! if template_requires_image: # Template requires images - generate image prompts using LLM from pixelle_video.utils.content_generators import generate_image_prompts diff --git a/pixelle_video/pipelines/standard.py b/pixelle_video/pipelines/standard.py index f57b9ce..fceedfb 100644 --- a/pixelle_video/pipelines/standard.py +++ b/pixelle_video/pipelines/standard.py @@ -282,8 +282,8 @@ class StandardPipeline(BasePipeline): logger.info(f"🎬 Template requires video generation") elif template_media_type == "image": logger.info(f"📸 Template requires image generation") - else: # text - logger.info(f"⚡ Template does not require media - skipping media generation pipeline") + else: # static + logger.info(f"⚡ Static template - skipping media generation pipeline") logger.info(f" 💡 Benefits: Faster generation + Lower cost + No ComfyUI dependency") try: @@ -525,35 +525,23 @@ class StandardPipeline(BasePipeline): - Media generation API calls - 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") - - Other templates with {{image}}: Image template (returns "image") - - Other templates without {{image}}: Text-only template (returns "text") 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: - "video", "image", or "text" + "static", "image", or "video" """ - 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 get_template_type - # Check if template name starts with video_ + # Determine type by template filename prefix template_name = Path(frame_template).name - if template_name.startswith("video_"): - logger.debug(f"Template '{frame_template}' is video template (video_ prefix)") - return "video" + template_type = get_template_type(template_name) - # Check if template contains {{image}} - template_path = resolve_template_path(frame_template) - 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" + logger.debug(f"Template '{frame_template}' is {template_type} template") + return template_type diff --git a/pixelle_video/services/frame_html.py b/pixelle_video/services/frame_html.py index 7629864..01ffe70 100644 --- a/pixelle_video/services/frame_html.py +++ b/pixelle_video/services/frame_html.py @@ -77,21 +77,6 @@ class HTMLFrameGenerator: self._check_linux_dependencies() 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): """Check Linux system dependencies and warn if missing""" diff --git a/pixelle_video/utils/template_util.py b/pixelle_video/utils/template_util.py index 501a723..6fa0827 100644 --- a/pixelle_video/utils/template_util.py +++ b/pixelle_video/utils/template_util.py @@ -18,6 +18,7 @@ import os from pathlib import Path from typing import List, Tuple, Optional, Literal from pydantic import BaseModel, Field +import logging from pixelle_video.utils.os_util import ( get_resource_path, @@ -26,6 +27,8 @@ from pixelle_video.utils.os_util import ( resource_exists ) +logger = logging.getLogger(__name__) + def parse_template_size(template_path: str) -> Tuple[int, int]: """ @@ -316,7 +319,7 @@ def resolve_template_path(template_input: Optional[str]) -> str: Args: template_input: Can be: - - None: Use default "1080x1920/default.html" + - None: Use default "1080x1920/image_default.html" - "template.html": Use default size + this template - "1080x1920/template.html": Full relative path - "templates/1080x1920/template.html": Absolute-ish path (legacy) @@ -330,15 +333,15 @@ def resolve_template_path(template_input: Optional[str]) -> str: 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' + 'templates/1080x1920/image_default.html' + >>> resolve_template_path("image_modern.html") + 'templates/1080x1920/image_modern.html' + >>> resolve_template_path("1920x1080/image_default.html") + 'templates/1920x1080/image_default.html' """ # Default case if template_input is None: - template_input = "1080x1920/default.html" + template_input = "1080x1920/image_default.html" # Parse input to extract size and template name size = None @@ -359,6 +362,18 @@ def resolve_template_path(template_input: Optional[str]) -> str: size = "1080x1920" 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) try: return get_resource_path("templates", size, template_name) @@ -367,6 +382,120 @@ def resolve_template_path(template_input: Optional[str]) -> str: raise FileNotFoundError( f"Template not found: {size}/{template_name}\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 + diff --git a/templates/1080x1080/minimal_framed.html b/templates/1080x1080/image_minimal_framed.html similarity index 100% rename from templates/1080x1080/minimal_framed.html rename to templates/1080x1080/image_minimal_framed.html diff --git a/templates/1080x1920/blur_card.html b/templates/1080x1920/image_blur_card.html similarity index 100% rename from templates/1080x1920/blur_card.html rename to templates/1080x1920/image_blur_card.html diff --git a/templates/1080x1920/cartoon.html b/templates/1080x1920/image_cartoon.html similarity index 100% rename from templates/1080x1920/cartoon.html rename to templates/1080x1920/image_cartoon.html diff --git a/templates/1080x1920/default.html b/templates/1080x1920/image_default.html similarity index 100% rename from templates/1080x1920/default.html rename to templates/1080x1920/image_default.html diff --git a/templates/1080x1920/elegant.html b/templates/1080x1920/image_elegant.html similarity index 100% rename from templates/1080x1920/elegant.html rename to templates/1080x1920/image_elegant.html diff --git a/templates/1080x1920/fashion_vintage.html b/templates/1080x1920/image_fashion_vintage.html similarity index 100% rename from templates/1080x1920/fashion_vintage.html rename to templates/1080x1920/image_fashion_vintage.html diff --git a/templates/1080x1920/full.html b/templates/1080x1920/image_full.html similarity index 100% rename from templates/1080x1920/full.html rename to templates/1080x1920/image_full.html diff --git a/templates/1080x1920/life_insights.html b/templates/1080x1920/image_life_insights.html similarity index 100% rename from templates/1080x1920/life_insights.html rename to templates/1080x1920/image_life_insights.html diff --git a/templates/1080x1920/modern.html b/templates/1080x1920/image_modern.html similarity index 100% rename from templates/1080x1920/modern.html rename to templates/1080x1920/image_modern.html diff --git a/templates/1080x1920/neon.html b/templates/1080x1920/image_neon.html similarity index 100% rename from templates/1080x1920/neon.html rename to templates/1080x1920/image_neon.html diff --git a/templates/1080x1920/psychology_card.html b/templates/1080x1920/image_psychology_card.html similarity index 100% rename from templates/1080x1920/psychology_card.html rename to templates/1080x1920/image_psychology_card.html diff --git a/templates/1080x1920/purple.html b/templates/1080x1920/image_purple.html similarity index 100% rename from templates/1080x1920/purple.html rename to templates/1080x1920/image_purple.html diff --git a/templates/1080x1920/simple.html b/templates/1080x1920/static_default.html similarity index 100% rename from templates/1080x1920/simple.html rename to templates/1080x1920/static_default.html diff --git a/templates/1080x1920/video_simple.html b/templates/1080x1920/video_default.html similarity index 100% rename from templates/1080x1920/video_simple.html rename to templates/1080x1920/video_default.html diff --git a/templates/1920x1080/film.html b/templates/1920x1080/image_film.html similarity index 100% rename from templates/1920x1080/film.html rename to templates/1920x1080/image_film.html diff --git a/templates/1920x1080/full.html b/templates/1920x1080/image_full.html similarity index 100% rename from templates/1920x1080/full.html rename to templates/1920x1080/image_full.html diff --git a/templates/1920x1080/ultrawide_minimal.html b/templates/1920x1080/image_ultrawide_minimal.html similarity index 100% rename from templates/1920x1080/ultrawide_minimal.html rename to templates/1920x1080/image_ultrawide_minimal.html diff --git a/templates/1920x1080/wide_darktech.html b/templates/1920x1080/image_wide_darktech.html similarity index 100% rename from templates/1920x1080/wide_darktech.html rename to templates/1920x1080/image_wide_darktech.html diff --git a/web/app.py b/web/app.py index f39a8b1..f1e5550 100644 --- a/web/app.py +++ b/web/app.py @@ -684,13 +684,41 @@ def main(): st.markdown(f"🔗 [{tr('template.preview_link')}]({template_docs_url})") # 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 - grouped_templates = get_templates_grouped_by_size() + # Template type selector + 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: - 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() # Build display options with group separators @@ -707,7 +735,19 @@ def main(): # Get default template from config 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(): if not templates: @@ -733,10 +773,12 @@ def main(): display_options.append(display_name) 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: 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 current_index += 1 @@ -789,20 +831,11 @@ def main(): # Detect template media type 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_"): - # Video template - template_media_type = "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 + template_name = Path(frame_template).name + template_media_type = get_template_type(template_name) + template_requires_media = (template_media_type in ["image", "video"]) # Store in session state for workflow filtering 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 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: default_workflow_index = workflow_keys.index(saved_workflow) @@ -1040,8 +1075,8 @@ def main(): st.info(f"📐 {size_info_text}") # Prompt prefix input - # Get current prompt_prefix from config - current_prefix = comfyui_config["image"]["prompt_prefix"] + # Get current prompt_prefix from config (based on media type) + current_prefix = comfyui_config.get(media_config_key, {}).get("prompt_prefix", "") # Prompt prefix input (temporary, not saved to config) prompt_prefix = st.text_area( @@ -1268,6 +1303,18 @@ def main(): # Video preview if os.path.exists(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: st.error(tr("status.video_not_found", path=result.video_path)) diff --git a/web/i18n/locales/en_US.json b/web/i18n/locales/en_US.json index 32eef9a..f554d11 100644 --- a/web/i18n/locales/en_US.json +++ b/web/i18n/locales/en_US.json @@ -86,8 +86,15 @@ "template.modern": "Modern", "template.neon": "Neon", "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.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.landscape": "Landscape", diff --git a/web/i18n/locales/zh_CN.json b/web/i18n/locales/zh_CN.json index ebaff93..773eed0 100644 --- a/web/i18n/locales/zh_CN.json +++ b/web/i18n/locales/zh_CN.json @@ -86,8 +86,15 @@ "template.modern": "现代", "template.neon": "霓虹", "template.what": "控制视频每一帧的视觉布局和设计风格(标题、文本、图片的排版样式)", - "template.how": "将 .html 模板文件放入 templates/尺寸/ 目录(如 templates/1080x1920/),系统会自动按尺寸分组。支持自定义 CSS 样式。\n\n**注意**\n\n您的计算机上必须安装以下至少一种浏览器才能正常运行:\n1. Google Chrome(Windows、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 Chrome(Windows、MacOS)\n2. Chromium 浏览器(Linux)\n3. Microsoft Edge", "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.landscape": "横屏",