feat(aurora-ui): add interactive pre-analysis controls and quality report
This commit is contained in:
@@ -1392,9 +1392,11 @@ async def api_aurora_compare(job_id: str) -> Dict[str, Any]:
|
|||||||
"download_url": (result_file or {}).get("url"),
|
"download_url": (result_file or {}).get("url"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output_media_path: Optional[Path] = None
|
||||||
if result_file and output_dir:
|
if result_file and output_dir:
|
||||||
out_path = Path(output_dir) / result_file["name"]
|
out_path = Path(output_dir) / result_file["name"]
|
||||||
if out_path.exists():
|
if out_path.exists():
|
||||||
|
output_media_path = out_path
|
||||||
after["file_size_mb"] = round(out_path.stat().st_size / (1024 * 1024), 2)
|
after["file_size_mb"] = round(out_path.stat().st_size / (1024 * 1024), 2)
|
||||||
_probe = _ffprobe_quick(out_path)
|
_probe = _ffprobe_quick(out_path)
|
||||||
if _probe:
|
if _probe:
|
||||||
@@ -1419,6 +1421,14 @@ async def api_aurora_compare(job_id: str) -> Dict[str, Any]:
|
|||||||
"time_ms": step.get("time_ms"),
|
"time_ms": step.get("time_ms"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
frame_preview = _aurora_ensure_compare_frame_preview(
|
||||||
|
job_id=job_id,
|
||||||
|
media_type=str(status.get("media_type") or ""),
|
||||||
|
input_path=Path(input_path) if input_path else None,
|
||||||
|
output_path=output_media_path,
|
||||||
|
output_dir=Path(output_dir) if output_dir else None,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"job_id": job_id,
|
"job_id": job_id,
|
||||||
"status": status.get("status"),
|
"status": status.get("status"),
|
||||||
@@ -1429,11 +1439,97 @@ async def api_aurora_compare(job_id: str) -> Dict[str, Any]:
|
|||||||
"after": after,
|
"after": after,
|
||||||
"faces_detected": faces_total,
|
"faces_detected": faces_total,
|
||||||
"enhance_steps": enhance_steps,
|
"enhance_steps": enhance_steps,
|
||||||
|
"frame_preview": frame_preview,
|
||||||
"folder_path": output_dir,
|
"folder_path": output_dir,
|
||||||
"input_path": input_path,
|
"input_path": input_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _aurora_extract_frame_preview(source: Path, target: Path, *, second: float = 1.0) -> bool:
|
||||||
|
"""Write a JPEG preview frame for image/video sources."""
|
||||||
|
if not source.exists():
|
||||||
|
return False
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
ext = source.suffix.lower()
|
||||||
|
if ext in {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}:
|
||||||
|
try:
|
||||||
|
target.write_bytes(source.read_bytes())
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ffmpeg = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
"-y",
|
||||||
|
"-ss",
|
||||||
|
f"{max(0.0, float(second)):.3f}",
|
||||||
|
"-i",
|
||||||
|
str(source),
|
||||||
|
"-frames:v",
|
||||||
|
"1",
|
||||||
|
"-q:v",
|
||||||
|
"2",
|
||||||
|
str(target),
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
run = subprocess.run(ffmpeg, capture_output=True, text=True, timeout=20)
|
||||||
|
if run.returncode == 0 and target.exists() and target.stat().st_size > 0:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback for short videos / odd timestamps.
|
||||||
|
ffmpeg_fallback = ffmpeg[:]
|
||||||
|
ffmpeg_fallback[6] = "0.0"
|
||||||
|
try:
|
||||||
|
run = subprocess.run(ffmpeg_fallback, capture_output=True, text=True, timeout=20)
|
||||||
|
return run.returncode == 0 and target.exists() and target.stat().st_size > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _aurora_ensure_compare_frame_preview(
|
||||||
|
*,
|
||||||
|
job_id: str,
|
||||||
|
media_type: str,
|
||||||
|
input_path: Optional[Path],
|
||||||
|
output_path: Optional[Path],
|
||||||
|
output_dir: Optional[Path],
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
if not output_dir or not output_dir.exists():
|
||||||
|
return None
|
||||||
|
if not input_path or not input_path.exists():
|
||||||
|
return None
|
||||||
|
if not output_path or not output_path.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
before_name = "_compare_before.jpg"
|
||||||
|
after_name = "_compare_after.jpg"
|
||||||
|
before_path = output_dir / before_name
|
||||||
|
after_path = output_dir / after_name
|
||||||
|
ts = 1.0 if media_type == "video" else 0.0
|
||||||
|
|
||||||
|
if not before_path.exists() or before_path.stat().st_size == 0:
|
||||||
|
_aurora_extract_frame_preview(input_path, before_path, second=ts)
|
||||||
|
if not after_path.exists() or after_path.stat().st_size == 0:
|
||||||
|
_aurora_extract_frame_preview(output_path, after_path, second=ts)
|
||||||
|
|
||||||
|
if not before_path.exists() or not after_path.exists():
|
||||||
|
return None
|
||||||
|
if before_path.stat().st_size <= 0 or after_path.stat().st_size <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
quoted_job = quote(job_id, safe="")
|
||||||
|
return {
|
||||||
|
"timestamp_sec": ts,
|
||||||
|
"before_url": f"/api/aurora/files/{quoted_job}/{quote(before_name, safe='')}",
|
||||||
|
"after_url": f"/api/aurora/files/{quoted_job}/{quote(after_name, safe='')}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _ffprobe_quick(filepath: Path) -> Dict[str, Any]:
|
def _ffprobe_quick(filepath: Path) -> Dict[str, Any]:
|
||||||
"""Quick ffprobe for resolution, codec, duration, fps, frame count."""
|
"""Quick ffprobe for resolution, codec, duration, fps, frame count."""
|
||||||
if not filepath.exists():
|
if not filepath.exists():
|
||||||
|
|||||||
8467
services/sofiia-console/static/index.html
Normal file
8467
services/sofiia-console/static/index.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user