From 7443cbf9c2de4fc0db5a4f91984c0f6c5593d724 Mon Sep 17 00:00:00 2001 From: puke <1129090915@qq.com> Date: Wed, 12 Nov 2025 17:19:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E4=B8=AD=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E5=B0=BA=E5=AF=B8=E6=94=B9=E4=B8=BA=E9=A2=84=E7=BD=AE=E6=96=B9?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/routers/video.py | 6 +- api/schemas/video.py | 3 +- pixelle_video/pipelines/custom.py | 7 +- pixelle_video/pipelines/standard.py | 16 +- pixelle_video/services/frame_html.py | 52 +++++ pixelle_video/services/video.py | 59 ++++-- pyproject.toml | 1 + templates/1080x1080/minimal_framed.html | 2 + templates/1080x1920/blur_card.html | 2 + templates/1080x1920/cartoon.html | 2 + templates/1080x1920/default.html | 2 + templates/1080x1920/elegant.html | 2 + templates/1080x1920/fashion_vintage.html | 2 + templates/1080x1920/full.html | 2 + templates/1080x1920/life_insights.html | 2 + templates/1080x1920/modern.html | 2 + templates/1080x1920/neon.html | 2 + templates/1080x1920/psychology_card.html | 2 + templates/1080x1920/purple.html | 2 + templates/1080x1920/simple.html | 2 + templates/1080x1920/video_simple.html | 185 +++++++++++++++++ templates/1920x1080/film.html | 2 + templates/1920x1080/full.html | 2 + templates/1920x1080/ultrawide_minimal.html | 2 + templates/1920x1080/wide_darktech.html | 2 + uv.lock | 24 +++ web/app.py | 61 ++---- web/i18n/locales/en_US.json | 13 +- web/i18n/locales/zh_CN.json | 13 +- .../runninghub/video_wan2.1_fusionx.json | 5 + workflows/selfhost/video_wan2.1_fusionx.json | 187 ++++++++++++++++++ 31 files changed, 576 insertions(+), 90 deletions(-) create mode 100644 templates/1080x1920/video_simple.html create mode 100644 workflows/runninghub/video_wan2.1_fusionx.json create mode 100644 workflows/selfhost/video_wan2.1_fusionx.json diff --git a/api/routers/video.py b/api/routers/video.py index e7a47cd..207e3c2 100644 --- a/api/routers/video.py +++ b/api/routers/video.py @@ -73,8 +73,7 @@ async def generate_video_sync( "max_narration_words": request_body.max_narration_words, "min_image_prompt_words": request_body.min_image_prompt_words, "max_image_prompt_words": request_body.max_image_prompt_words, - "image_width": request_body.image_width, - "image_height": request_body.image_height, + # Note: image_width and image_height are now auto-determined from template "image_workflow": request_body.image_workflow, "video_fps": request_body.video_fps, "frame_template": request_body.frame_template, @@ -161,8 +160,7 @@ async def generate_video_async( "max_narration_words": request_body.max_narration_words, "min_image_prompt_words": request_body.min_image_prompt_words, "max_image_prompt_words": request_body.max_image_prompt_words, - "image_width": request_body.image_width, - "image_height": request_body.image_height, + # Note: image_width and image_height are now auto-determined from template "image_workflow": request_body.image_workflow, "video_fps": request_body.video_fps, "frame_template": request_body.frame_template, diff --git a/api/schemas/video.py b/api/schemas/video.py index 93070f9..d37dd80 100644 --- a/api/schemas/video.py +++ b/api/schemas/video.py @@ -57,8 +57,7 @@ class VideoGenerateRequest(BaseModel): max_image_prompt_words: int = Field(60, ge=10, le=200, description="Max image prompt words") # === Image Parameters === - image_width: int = Field(1024, description="Image width") - image_height: int = Field(1024, description="Image height") + # Note: image_width and image_height are now auto-determined from template meta tags image_workflow: Optional[str] = Field(None, description="Custom image workflow") # === Video Parameters === diff --git a/pixelle_video/pipelines/custom.py b/pixelle_video/pipelines/custom.py index e1779c4..ce6dbe3 100644 --- a/pixelle_video/pipelines/custom.py +++ b/pixelle_video/pipelines/custom.py @@ -92,8 +92,7 @@ class CustomPipeline(BasePipeline): ref_audio: Optional[str] = None, image_workflow: Optional[str] = None, - image_width: int = 1024, - image_height: int = 1024, + # Note: image_width and image_height are now auto-determined from template frame_template: Optional[str] = None, video_fps: int = 30, @@ -161,6 +160,10 @@ class CustomPipeline(BasePipeline): generator = HTMLFrameGenerator(template_path) template_requires_image = generator.requires_image() + # Read media size from template meta tags + image_width, image_height = generator.get_media_size() + logger.info(f"📐 Media size from template: {image_width}x{image_height}") + if template_requires_image: logger.info(f"📸 Template requires image generation") else: diff --git a/pixelle_video/pipelines/standard.py b/pixelle_video/pipelines/standard.py index 5864659..f57b9ce 100644 --- a/pixelle_video/pipelines/standard.py +++ b/pixelle_video/pipelines/standard.py @@ -94,8 +94,7 @@ class StandardPipeline(BasePipeline): max_image_prompt_words: int = 60, # === Image Parameters === - image_width: int = 1024, - image_height: int = 1024, + # Note: image_width and image_height are now auto-determined from template meta tags image_workflow: Optional[str] = None, # === Video Parameters === @@ -151,9 +150,8 @@ class StandardPipeline(BasePipeline): min_image_prompt_words: Min image prompt length max_image_prompt_words: Max image prompt length - image_width: Generated image width (default 1024) - image_height: Generated image height (default 1024) image_workflow: Image workflow filename (e.g., "image_flux.json", None = use default) + Note: Image/video size is now auto-determined from template meta tags video_fps: Video frame rate (default 30) @@ -239,6 +237,16 @@ class StandardPipeline(BasePipeline): template_config = self.core.config.get("template", {}) frame_template = template_config.get("default_template", "1080x1920/default.html") + # Read media size from template meta tags + from pixelle_video.services.frame_html import HTMLFrameGenerator + from pixelle_video.utils.template_util import resolve_template_path + + template_path = resolve_template_path(frame_template) + temp_generator = HTMLFrameGenerator(template_path) + image_width, image_height = temp_generator.get_media_size() + + logger.info(f"📐 Media size from template: {image_width}x{image_height}") + # Create storyboard config config = StoryboardConfig( task_id=task_id, diff --git a/pixelle_video/services/frame_html.py b/pixelle_video/services/frame_html.py index 4efd02d..7629864 100644 --- a/pixelle_video/services/frame_html.py +++ b/pixelle_video/services/frame_html.py @@ -141,6 +141,58 @@ class HTMLFrameGenerator: logger.debug(f"Template loaded: {len(content)} chars") return content + def _parse_media_size_from_meta(self) -> tuple[Optional[int], Optional[int]]: + """ + Parse media size from meta tags in template + + Looks for meta tags: + - + - + + Returns: + Tuple of (width, height) or (None, None) if not found + """ + from bs4 import BeautifulSoup + + try: + soup = BeautifulSoup(self.template, 'html.parser') + + # Find width and height meta tags + width_meta = soup.find('meta', attrs={'name': 'template:media-width'}) + height_meta = soup.find('meta', attrs={'name': 'template:media-height'}) + + if width_meta and height_meta: + width = int(width_meta.get('content', 0)) + height = int(height_meta.get('content', 0)) + + if width > 0 and height > 0: + logger.debug(f"Found media size in meta tags: {width}x{height}") + return width, height + + return None, None + + except Exception as e: + logger.warning(f"Failed to parse media size from meta tags: {e}") + return None, None + + def get_media_size(self) -> tuple[int, int]: + """ + Get media size for image/video generation + + Returns media size specified in template meta tags. + + Returns: + Tuple of (width, height) + """ + media_width, media_height = self._parse_media_size_from_meta() + + if media_width and media_height: + return media_width, media_height + + # Fallback to default if not specified (should not happen with properly configured templates) + logger.warning(f"No media size meta tags found in template {self.template_path}, using fallback 1024x1024") + return 1024, 1024 + def parse_template_parameters(self) -> Dict[str, Dict[str, Any]]: """ Parse custom parameters from HTML template diff --git a/pixelle_video/services/video.py b/pixelle_video/services/video.py index 35e7a56..5cbe31c 100644 --- a/pixelle_video/services/video.py +++ b/pixelle_video/services/video.py @@ -224,20 +224,43 @@ class VideoService: -map "[v]" -map "[a]" output.mp4 """ try: - inputs = [ffmpeg.input(v) for v in videos] - ( - ffmpeg - .concat(*inputs, v=1, a=1) - .output(output) - .overwrite_output() - .run(capture_stdout=True, capture_stderr=True) + # Build filter_complex string manually + n = len(videos) + + # Build input stream labels: [0:v][0:a][1:v][1:a]... + stream_spec = "".join([f"[{i}:v][{i}:a]" for i in range(n)]) + filter_complex = f"{stream_spec}concat=n={n}:v=1:a=1[v][a]" + + # Build ffmpeg command + cmd = ['ffmpeg'] + for video in videos: + cmd.extend(['-i', video]) + cmd.extend([ + '-filter_complex', filter_complex, + '-map', '[v]', + '-map', '[a]', + '-y', # Overwrite output + output + ]) + + # Run command + import subprocess + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True ) + logger.success(f"Videos concatenated successfully: {output}") return output - except ffmpeg.Error as e: - error_msg = e.stderr.decode() if e.stderr else str(e) + except subprocess.CalledProcessError as e: + error_msg = e.stderr if e.stderr else str(e) logger.error(f"FFmpeg concat filter error: {error_msg}") raise RuntimeError(f"Failed to concatenate videos: {error_msg}") + except Exception as e: + logger.error(f"Concatenation error: {e}") + raise RuntimeError(f"Failed to concatenate videos: {e}") def _get_video_duration(self, video: str) -> float: """Get video duration in seconds""" @@ -382,10 +405,17 @@ class VideoService: # Concatenate original video with black padding video_stream = ffmpeg.concat(video_stream, black_input.video, v=1, a=0) - # Prepare audio stream + # Prepare audio stream (pad if needed to match target duration) input_audio = ffmpeg.input(audio) audio_stream = input_audio.audio.filter('volume', audio_volume) + # Pad audio with silence if video is longer + if video_duration > audio_duration: + pad_duration = video_duration - audio_duration + logger.info(f"Video is longer, padding audio with {pad_duration:.2f}s silence") + # Use apad to add silence at the end + audio_stream = audio_stream.filter('apad', whole_dur=target_duration) + if not video_has_audio: logger.info(f"Video has no audio stream, adding audio track") # Video is silent, just add the audio @@ -398,8 +428,7 @@ class VideoService: output, vcodec='libx264', # Re-encode video if padded acodec='aac', - audio_bitrate='192k', - t=target_duration # Trim to target duration + audio_bitrate='192k' ) .overwrite_output() .run(capture_stdout=True, capture_stderr=True) @@ -426,8 +455,7 @@ class VideoService: output, vcodec='libx264', # Re-encode video if padded acodec='aac', - audio_bitrate='192k', - t=target_duration # Trim to target duration + audio_bitrate='192k' ) .overwrite_output() .run(capture_stdout=True, capture_stderr=True) @@ -452,8 +480,7 @@ class VideoService: output, vcodec='libx264', # Re-encode video if padded acodec='aac', - audio_bitrate='192k', - t=target_duration # Trim to target duration + audio_bitrate='192k' ) .overwrite_output() .run(capture_stdout=True, capture_stderr=True) diff --git a/pyproject.toml b/pyproject.toml index 07c7eb7..d98dda3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "uvicorn[standard]>=0.32.0", "python-multipart>=0.0.12", "comfykit>=0.1.9", + "beautifulsoup4>=4.14.2", ] [project.optional-dependencies] diff --git a/templates/1080x1080/minimal_framed.html b/templates/1080x1080/minimal_framed.html index 5e8f20a..99a7212 100644 --- a/templates/1080x1080/minimal_framed.html +++ b/templates/1080x1080/minimal_framed.html @@ -2,6 +2,8 @@ + + 极简边框风格 - 1080x1080 + + + +
+ +
+ +
+ + +
+ + +
+
{{title}}
+
+ + +
+
{{text}}
+
+ + + +
+ + \ No newline at end of file diff --git a/templates/1920x1080/film.html b/templates/1920x1080/film.html index 917fa0c..a16bd41 100644 --- a/templates/1920x1080/film.html +++ b/templates/1920x1080/film.html @@ -2,6 +2,8 @@ + + 视频模板 - 电影风格