| 1 | """Persistence helpers for Web-only generation workflows.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import subprocess |
| 6 | from datetime import datetime |
| 7 | from pathlib import Path |
| 8 | from typing import Any |
| 9 | |
| 10 | from loguru import logger |
| 11 | |
| 12 | |
| 13 | def _probe_video_duration(video_path: str) -> float: |
| 14 | cmd = [ |
| 15 | "ffprobe", |
| 16 | "-v", |
| 17 | "error", |
| 18 | "-show_entries", |
| 19 | "format=duration", |
| 20 | "-of", |
| 21 | "default=noprint_wrappers=1:nokey=1", |
| 22 | video_path, |
| 23 | ] |
| 24 | try: |
| 25 | result = subprocess.run(cmd, capture_output=True, text=True, check=True) |
| 26 | return float(result.stdout.strip()) |
| 27 | except Exception as exc: |
| 28 | logger.warning(f"Failed to probe video duration for {video_path}: {exc}") |
| 29 | return 0.0 |
| 30 | |
| 31 | |
| 32 | async def save_web_generation_history( |
| 33 | pixelle_video: Any, |
| 34 | *, |
| 35 | task_id: str, |
| 36 | video_path: str, |
| 37 | pipeline: str, |
| 38 | input_params: dict, |
| 39 | title: str | None = None, |
| 40 | n_frames: int = 1, |
| 41 | ) -> None: |
| 42 | """Save a minimal history record for workflows implemented directly in Web UI.""" |
| 43 | if not getattr(pixelle_video, "persistence", None): |
| 44 | logger.warning("Pixelle persistence service is not initialized; skipping history save") |
| 45 | return |
| 46 | |
| 47 | path = Path(video_path) |
| 48 | if not task_id: |
| 49 | task_id = path.parent.name |
| 50 | if not path.exists(): |
| 51 | logger.warning(f"Cannot save history; video file does not exist: {video_path}") |
| 52 | return |
| 53 | |
| 54 | created_at = datetime.fromtimestamp(path.parent.stat().st_ctime).isoformat() |
| 55 | completed_at = datetime.fromtimestamp(path.stat().st_mtime).isoformat() |
| 56 | duration = _probe_video_duration(str(path)) |
| 57 | |
| 58 | normalized_input = dict(input_params) |
| 59 | normalized_input.setdefault("mode", pipeline) |
| 60 | normalized_input.setdefault("title", title or pipeline) |
| 61 | if title: |
| 62 | normalized_input["title"] = title |
| 63 | |
| 64 | metadata = { |
| 65 | "task_id": task_id, |
| 66 | "created_at": created_at, |
| 67 | "completed_at": completed_at, |
| 68 | "status": "completed", |
| 69 | "input": normalized_input, |
| 70 | "result": { |
| 71 | "video_path": str(path), |
| 72 | "duration": duration, |
| 73 | "file_size": path.stat().st_size, |
| 74 | "n_frames": n_frames, |
| 75 | }, |
| 76 | "config": { |
| 77 | "llm_model": pixelle_video.config.get("llm", {}).get("model", "unknown"), |
| 78 | "llm_base_url": pixelle_video.config.get("llm", {}).get("base_url", "unknown"), |
| 79 | "source": "web", |
| 80 | }, |
| 81 | } |
| 82 | |
| 83 | await pixelle_video.persistence.save_task_metadata(task_id, metadata) |
| 84 | logger.info(f"Saved web workflow history: {task_id}") |
| 85 |