Preview: simplify pyvips timing to single processing ms + req total.
This commit is contained in:
+10
-16
@@ -354,10 +354,14 @@ async def preview(req, path):
|
||||
except PreviewError:
|
||||
return empty(422)
|
||||
if preview_resp and preview_resp.backend:
|
||||
load_ms = int(round(preview_resp.load_ms or 0.0))
|
||||
process_ms = int(round(preview_resp.process_ms or 0.0))
|
||||
save_ms = int(round(preview_resp.save_ms or 0.0))
|
||||
req.ctx._log_extra = f"{preview_resp.backend} {load_ms}/{process_ms}/{save_ms} ="
|
||||
if preview_resp.load_ms is not None:
|
||||
load_ms = int(round(preview_resp.load_ms))
|
||||
process_ms = int(round(preview_resp.process_ms or 0.0))
|
||||
save_ms = int(round(preview_resp.save_ms or 0.0))
|
||||
timing_detail = f"{load_ms}/{process_ms}/{save_ms}"
|
||||
else:
|
||||
timing_detail = str(int(round(preview_resp.total_ms or 0.0)))
|
||||
req.ctx._log_extra = f"{preview_resp.backend} {timing_detail} ➛"
|
||||
if not img:
|
||||
# Preview generation failed, redirect to the file itself
|
||||
return redirect(f"/files/{path}", status=303)
|
||||
@@ -415,16 +419,12 @@ def process_image_with_timing(path, *, maxsize, quality):
|
||||
def process_image_pyvips(path, *, maxsize, quality):
|
||||
import pyvips
|
||||
|
||||
t_load = perf_counter()
|
||||
t_start = perf_counter()
|
||||
img = pyvips.Image.new_from_file(str(path), access="sequential")
|
||||
t_proc = perf_counter()
|
||||
|
||||
img = img.autorot()
|
||||
scale = min(maxsize / img.width, maxsize / img.height, 1.0)
|
||||
if scale < 1.0:
|
||||
img = img.resize(scale)
|
||||
|
||||
t_save = perf_counter()
|
||||
ret = img.write_to_buffer(
|
||||
".avif",
|
||||
Q=quality,
|
||||
@@ -433,17 +433,11 @@ def process_image_pyvips(path, *, maxsize, quality):
|
||||
)
|
||||
t_end = perf_counter()
|
||||
|
||||
load_ms = (t_proc - t_load) * 1000
|
||||
proc_ms = (t_save - t_proc) * 1000
|
||||
save_ms = (t_end - t_save) * 1000
|
||||
return ret, PreviewResponse(
|
||||
ok=True,
|
||||
mime="image/avif",
|
||||
backend="pyvips",
|
||||
load_ms=round(load_ms, 1),
|
||||
process_ms=round(proc_ms, 1),
|
||||
save_ms=round(save_ms, 1),
|
||||
total_ms=round((t_end - t_load) * 1000, 1),
|
||||
total_ms=round((t_end - t_start) * 1000, 1),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import argparse
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from cista.preview import process_image_with_timing
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate image previews for all files in a folder, one at a time.",
|
||||
)
|
||||
parser.add_argument("folder", type=Path, help="Folder to scan recursively")
|
||||
parser.add_argument(
|
||||
"--px",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="Maximum preview dimension in pixels (default: 1024)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quality",
|
||||
type=int,
|
||||
default=60,
|
||||
help="AVIF quality passed to preview generation (default: 60)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def is_image_file(path: Path) -> bool:
|
||||
mime_type, _ = mimetypes.guess_type(path.name)
|
||||
return bool(mime_type and mime_type.startswith("image/"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
folder = args.folder.resolve()
|
||||
if not folder.is_dir():
|
||||
raise SystemExit(f"Not a directory: {folder}")
|
||||
|
||||
files = sorted(path for path in folder.rglob("*") if path.is_file() and is_image_file(path))
|
||||
if not files:
|
||||
print(f"No image files found under {folder}")
|
||||
return 0
|
||||
|
||||
total_files = 0
|
||||
total_bytes = 0
|
||||
total_load_ms: float = 0.0
|
||||
total_process_ms: float = 0.0
|
||||
total_save_ms: float = 0.0
|
||||
total_preview_ms: float = 0.0
|
||||
failures = 0
|
||||
|
||||
print(f"Scanning {folder}")
|
||||
print(f"Generating previews for {len(files)} image files")
|
||||
|
||||
for path in files:
|
||||
total_files += 1
|
||||
rel = path.relative_to(folder)
|
||||
try:
|
||||
preview, timing = process_image_with_timing(
|
||||
path,
|
||||
maxsize=args.px,
|
||||
quality=args.quality,
|
||||
)
|
||||
except Exception as exc:
|
||||
failures += 1
|
||||
print(f"FAIL {rel} error={exc}")
|
||||
continue
|
||||
|
||||
total_bytes += len(preview)
|
||||
total_load_ms += timing.load_ms or 0.0
|
||||
total_process_ms += timing.process_ms or 0.0
|
||||
total_save_ms += timing.save_ms or 0.0
|
||||
total_preview_ms += timing.total_ms or 0.0
|
||||
|
||||
if timing.load_ms is not None:
|
||||
detail = (
|
||||
f"load={timing.load_ms:.1f}ms process={timing.process_ms:.1f}ms "
|
||||
f"save={timing.save_ms:.1f}ms total={timing.total_ms:.1f}ms"
|
||||
)
|
||||
else:
|
||||
detail = f"total={timing.total_ms:.1f}ms"
|
||||
print(f"OK {rel} backend={timing.backend} bytes={len(preview)} {detail}")
|
||||
|
||||
completed = total_files - failures
|
||||
print()
|
||||
print("Summary")
|
||||
print(f" files={total_files}")
|
||||
print(f" completed={completed}")
|
||||
print(f" failed={failures}")
|
||||
print(f" preview_bytes={total_bytes}")
|
||||
if completed:
|
||||
if total_load_ms or total_process_ms or total_save_ms:
|
||||
print(f" load_total_ms={total_load_ms:.1f}")
|
||||
print(f" process_total_ms={total_process_ms:.1f}")
|
||||
print(f" save_total_ms={total_save_ms:.1f}")
|
||||
print(f" preview_total_ms={total_preview_ms:.1f}")
|
||||
print(f" preview_avg_ms={total_preview_ms / completed:.1f}")
|
||||
return 0 if failures == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user