fix: lazy ffmpeg check, workflow scan caching, index-based task listing

Three fixes: (1) check_ffmpeg() was called at import time causing startup crash if FFmpeg not installed; moved to lazy _ensure_ffmpeg() on first use. (2) _workflows_cache was never populated so filesystem was rescanned on every call; now caches after first scan. (3) list_tasks() was reading every metadata.json individually; now uses the existing index for O(1) performance.
This commit is contained in:
Ziang Xie authored and GitHub committed 2026-05-06 11:40:39 +08:00
1 parent af52cf674a
commit fd88c62363
3 files changed
+42 -60

No files matched your search

+9 -3
View File
@@ -70,7 +70,9 @@ class ComfyBaseService:
def _scan_workflows(self) -> List[Dict[str, Any]]:
"""
Scan workflows/source/*.json files from all source directories (merged from workflows/ and data/workflows/)
Results are cached after first scan to avoid repeated filesystem I/O.
Returns:
List of workflow info dicts
Example: [
@@ -83,7 +85,7 @@ class ComfyBaseService:
},
{
"name": "image_flux.json",
"display_name": "image_flux.json - Runninghub",
"display_name": "image_flux.json - Runninghub",
"source": "runninghub",
"path": "workflows/runninghub/image_flux.json",
"key": "runninghub/image_flux.json",
@@ -91,6 +93,9 @@ class ComfyBaseService:
}
]
"""
if self._workflows_cache is not None:
return self._workflows_cache
workflows = []
# Get all workflow source directories (merged from workflows/ and data/workflows/)
@@ -122,7 +127,8 @@ class ComfyBaseService:
logger.error(f"Failed to parse workflow {source_name}/{filename}: {e}")
# Sort by key (source/name)
return sorted(workflows, key=lambda w: w["key"])
self._workflows_cache = sorted(workflows, key=lambda w: w["key"])
return self._workflows_cache
def _parse_workflow_file(self, file_path: Path, source: str) -> Dict[str, Any]:
"""
+9 -27
View File
@@ -277,37 +277,19 @@ class PersistenceService:
List of metadata dicts, sorted by created_at descending
"""
try:
tasks = []
# Scan all task directories
for task_dir in self.output_dir.iterdir():
if not task_dir.is_dir():
continue
metadata_path = task_dir / "metadata.json"
if not metadata_path.exists():
continue
try:
with open(metadata_path, "r", encoding="utf-8") as f:
metadata = json.load(f)
# Filter by status
if status and metadata.get("status") != status:
continue
tasks.append(metadata)
except Exception as e:
logger.warning(f"Failed to load metadata from {task_dir}: {e}")
continue
index = self._load_index()
tasks = index.get("tasks", [])
# Filter by status
if status:
tasks = [t for t in tasks if t.get("status") == status]
# Sort by created_at descending
tasks.sort(key=lambda t: t.get("created_at", ""), reverse=True)
# Apply pagination
return tasks[offset:offset + limit]
except Exception as e:
logger.error(f"Failed to list tasks: {e}")
return []
+24 -30
View File
@@ -57,33 +57,29 @@ def check_ffmpeg() -> None:
)
# Check FFmpeg availability on module import
check_ffmpeg()
class VideoService:
"""
Video compositor for common video processing tasks
Uses ffmpeg-python for high-performance video processing.
All operations preserve video quality when possible (stream copy).
Examples:
>>> compositor = VideoCompositor()
>>>
>>>
>>> # Concatenate videos
>>> compositor.concat_videos(
... ["intro.mp4", "main.mp4", "outro.mp4"],
... "final.mp4"
... )
>>>
>>>
>>> # Add voiceover
>>> compositor.merge_audio_video(
... "visual.mp4",
... "voiceover.mp3",
... "final.mp4"
... )
>>>
>>>
>>> # Add background music
>>> compositor.add_bgm(
... "video.mp4",
@@ -91,7 +87,7 @@ class VideoService:
... "final.mp4",
... bgm_volume=0.3
... )
>>>
>>>
>>> # Create video from image + audio
>>> compositor.create_video_from_image(
... "frame.png",
@@ -99,7 +95,16 @@ class VideoService:
... "segment.mp4"
... )
"""
def __init__(self):
self._ffmpeg_checked = False
def _ensure_ffmpeg(self):
"""Lazily check FFmpeg availability on first use, not at import time"""
if not self._ffmpeg_checked:
check_ffmpeg()
self._ffmpeg_checked = True
def concat_videos(
self,
videos: List[str],
@@ -111,7 +116,7 @@ class VideoService:
) -> str:
"""
Concatenate multiple videos into one
Args:
videos: List of video file paths to concatenate
output: Output video file path
@@ -120,25 +125,9 @@ class VideoService:
- "filter": Slower but handles different formats
bgm_path: Background music file path (optional)
- None: No BGM
- Filename (e.g., "default.mp3", "happy.mp3"): Use built-in BGM from bgm/ folder
- Custom path: Use custom BGM file
bgm_volume: BGM volume level (0.0-1.0), default 0.2
bgm_mode: BGM playback mode
- "once": Play BGM once
- "loop": Loop BGM to match video duration
Returns:
Path to the output video file
Raises:
ValueError: If videos list is empty
RuntimeError: If FFmpeg execution fails
Note:
- demuxer method requires all videos to have identical:
resolution, codec, fps, etc.
- filter method re-encodes videos, slower but more compatible
"""
self._ensure_ffmpeg()
if not videos:
raise ValueError("Videos list cannot be empty")
@@ -363,6 +352,8 @@ class VideoService:
- When replace_audio=True and video has audio, original audio is removed
- When replace_audio=False and video has audio, original and new audio are mixed
"""
self._ensure_ffmpeg()
# Get durations of video and audio
video_duration = self._get_video_duration(video)
audio_duration = self._get_audio_duration(audio)
@@ -551,6 +542,7 @@ class VideoService:
- Final video size matches overlay image size
- Video codec is re-encoded to support overlay
"""
self._ensure_ffmpeg()
logger.info(f"Overlaying image on video (scale_mode={scale_mode})")
try:
@@ -640,6 +632,7 @@ class VideoService:
... "segment.mp4"
... )
"""
self._ensure_ffmpeg()
logger.info("Creating video from image and audio")
try:
@@ -714,6 +707,7 @@ class VideoService:
- If loop=True, BGM repeats until video ends
- Fade effects are applied to BGM only
"""
self._ensure_ffmpeg()
logger.info(f"Adding BGM to video (volume={bgm_volume}, loop={loop})")
try: