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:
1 parent
af52cf674a
commit
fd88c62363
3 files changed
+42
-60
No files matched your search
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user