Spaces:
Running on Zero
Running on Zero
Upload 12 files
Browse files- DEPLOY_HF.md +9 -8
- app.py +56 -12
- config.py +7 -4
- pipeline.py +79 -68
DEPLOY_HF.md
CHANGED
|
@@ -44,14 +44,15 @@ Do **not** upload `venv/`, `__pycache__/`, or model weights (downloaded at runti
|
|
| 44 |
|
| 45 |
## Troubleshooting
|
| 46 |
- **Runtime `gradio.exceptions.Error: 'GPU task aborted'` / "ZeroGPU worker error"** β
|
| 47 |
-
the GPU worker was killed
|
| 48 |
-
|
| 49 |
-
(
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
is
|
|
|
|
| 55 |
- **Build fails building `basicsr` with `KeyError: '__version__'` / `get_version`** β
|
| 56 |
the Space built on **Python 3.13**, whose PEP 667 broke BasicSR's `setup.py`
|
| 57 |
(`exec()` + `locals()`). Fixed by pinning **`python_version: "3.10"`** in the
|
|
|
|
| 44 |
|
| 45 |
## Troubleshooting
|
| 46 |
- **Runtime `gradio.exceptions.Error: 'GPU task aborted'` / "ZeroGPU worker error"** β
|
| 47 |
+
the GPU worker was killed because a single call ran too long. **Video is now
|
| 48 |
+
processed in chunks**: the whole clip is split into batches
|
| 49 |
+
(β{`chunk_frames_plain`} frames, {`chunk_frames_face`} with face-enhance), each
|
| 50 |
+
upscaled in its **own GPU call**, then joined with audio β so the full clip gets
|
| 51 |
+
done across multiple GPU turns instead of one short window. Output is capped to
|
| 52 |
+
~4K; total length up to `max_total_video_seconds` (less with face-enhance). More
|
| 53 |
+
chunks use more of your ZeroGPU quota. For true 8K video use a dedicated GPU;
|
| 54 |
+
8K is meant for the **Image** tab. Startup **prefetches** imports+weights, and
|
| 55 |
+
any error is printed to the logs via `traceback`.
|
| 56 |
- **Build fails building `basicsr` with `KeyError: '__version__'` / `get_version`** β
|
| 57 |
the Space built on **Python 3.13**, whose PEP 667 broke BasicSR's `setup.py`
|
| 58 |
(`exec()` + `locals()`). Fixed by pinning **`python_version: "3.10"`** in the
|
app.py
CHANGED
|
@@ -175,8 +175,21 @@ def enhance_image_fn(image, scale_mode, model_name, denoise, face_enhance,
|
|
| 175 |
|
| 176 |
|
| 177 |
@spaces.GPU(duration=PROCESS_DURATION)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
def enhance_video_fn(video_file, scale_mode, model_name, denoise, face_enhance,
|
| 179 |
progress=gr.Progress()):
|
|
|
|
|
|
|
|
|
|
| 180 |
if video_file is None:
|
| 181 |
gr.Warning("β οΈ Please upload a video first!")
|
| 182 |
return None, "β No video uploaded"
|
|
@@ -184,20 +197,49 @@ def enhance_video_fn(video_file, scale_mode, model_name, denoise, face_enhance,
|
|
| 184 |
def gp(v, t):
|
| 185 |
progress(v, desc=t)
|
| 186 |
|
|
|
|
| 187 |
try:
|
| 188 |
video_path = video_file if isinstance(video_file, str) else video_file.name
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
except Exception as e:
|
| 197 |
import traceback
|
| 198 |
traceback.print_exc() # surface the real error in the Space logs
|
| 199 |
gr.Warning(f"Error: {e}")
|
| 200 |
return None, f"β {e}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
|
| 203 |
# βββ UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -275,11 +317,13 @@ def create_app():
|
|
| 275 |
### Reaching 8K
|
| 276 |
- **Images** upscale to 8K easily. Pick **Max β up to 8K** and the output is
|
| 277 |
scaled to fill the 8K box (never larger). From ~1080p that's a 4Γ pass.
|
| 278 |
-
- **Video** is
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
| 283 |
|
| 284 |
### Models
|
| 285 |
- **General** β best for real-world photos/video; the **Denoise** dial trades grain vs. smoothness.
|
|
|
|
| 175 |
|
| 176 |
|
| 177 |
@spaces.GPU(duration=PROCESS_DURATION)
|
| 178 |
+
def _gpu_upscale_batch(frame_paths, out_dir, out_start_index, outscale,
|
| 179 |
+
model_name, denoise, face_enhance):
|
| 180 |
+
"""One GPU call: upscale a batch of frames. Kept small enough to fit the
|
| 181 |
+
ZeroGPU time budget; the orchestrator calls this once per chunk."""
|
| 182 |
+
return pipeline.upscale_frames(
|
| 183 |
+
frame_paths, out_dir, int(out_start_index), float(outscale),
|
| 184 |
+
model_name, float(denoise), bool(face_enhance),
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
def enhance_video_fn(video_file, scale_mode, model_name, denoise, face_enhance,
|
| 189 |
progress=gr.Progress()):
|
| 190 |
+
"""Orchestrator (CPU): extract frames once, upscale them in batches β each
|
| 191 |
+
batch is a separate GPU call β then assemble. This processes the WHOLE clip
|
| 192 |
+
across multiple GPU turns instead of just one short window."""
|
| 193 |
if video_file is None:
|
| 194 |
gr.Warning("β οΈ Please upload a video first!")
|
| 195 |
return None, "β No video uploaded"
|
|
|
|
| 197 |
def gp(v, t):
|
| 198 |
progress(v, desc=t)
|
| 199 |
|
| 200 |
+
prep = None
|
| 201 |
try:
|
| 202 |
video_path = video_file if isinstance(video_file, str) else video_file.name
|
| 203 |
+
|
| 204 |
+
# CPU: trim to overall cap + extract all frames.
|
| 205 |
+
prep = pipeline.prepare_video(video_path, scale_mode=scale_mode,
|
| 206 |
+
face_enhance=bool(face_enhance), progress=gp)
|
| 207 |
+
total = len(prep.files)
|
| 208 |
+
if total == 0:
|
| 209 |
+
return None, "β No frames found in the video"
|
| 210 |
+
|
| 211 |
+
batch = pipeline.batch_size(bool(face_enhance))
|
| 212 |
+
n_batches = (total + batch - 1) // batch
|
| 213 |
+
|
| 214 |
+
# GPU: process chunk by chunk (each a separate @spaces.GPU call).
|
| 215 |
+
out_idx = 0
|
| 216 |
+
for b, start in enumerate(range(0, total, batch)):
|
| 217 |
+
chunk = prep.files[start:start + batch]
|
| 218 |
+
end = min(start + batch, total)
|
| 219 |
+
gp(0.10 + 0.82 * (start / total),
|
| 220 |
+
f"π Upscaling chunk {b + 1}/{n_batches} "
|
| 221 |
+
f"(frames {start + 1}β{end} of {total}) β {prep.ow}Γ{prep.oh}...")
|
| 222 |
+
out_idx = _gpu_upscale_batch(
|
| 223 |
+
chunk, prep.out_dir, out_idx, prep.outscale,
|
| 224 |
+
model_name, float(denoise), bool(face_enhance),
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# CPU: join all upscaled frames + audio.
|
| 228 |
+
out_final = tempfile.mkdtemp(prefix="enh_vid_")
|
| 229 |
+
out_path = os.path.join(out_final, f"enhanced_{Path(video_path).stem}.mp4")
|
| 230 |
+
result, info = pipeline.finalize_video(prep, out_path, out_idx, progress=gp)
|
| 231 |
+
return result, info + f" Β· {n_batches} chunk(s)"
|
| 232 |
except Exception as e:
|
| 233 |
import traceback
|
| 234 |
traceback.print_exc() # surface the real error in the Space logs
|
| 235 |
gr.Warning(f"Error: {e}")
|
| 236 |
return None, f"β {e}"
|
| 237 |
+
finally:
|
| 238 |
+
if prep is not None:
|
| 239 |
+
try:
|
| 240 |
+
pipeline.cleanup(prep)
|
| 241 |
+
except Exception:
|
| 242 |
+
pass
|
| 243 |
|
| 244 |
|
| 245 |
# βββ UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 317 |
### Reaching 8K
|
| 318 |
- **Images** upscale to 8K easily. Pick **Max β up to 8K** and the output is
|
| 319 |
scaled to fill the 8K box (never larger). From ~1080p that's a 4Γ pass.
|
| 320 |
+
- **Video** is processed **in chunks** β the whole clip is split into batches,
|
| 321 |
+
each upscaled in its own GPU turn, then joined back with audio. Output is
|
| 322 |
+
capped to **~4K**; total length up to **{config.max_total_video_seconds}s**
|
| 323 |
+
(**{config.max_total_video_seconds_face}s** with face-enhance, which is
|
| 324 |
+
~10Γ slower). Longer input is trimmed. **For true 8K video, use a dedicated GPU.**
|
| 325 |
+
- More chunks = more GPU turns (and more of your ZeroGPU quota), but the whole
|
| 326 |
+
clip gets done instead of just the first second.
|
| 327 |
|
| 328 |
### Models
|
| 329 |
- **General** β best for real-world photos/video; the **Denoise** dial trades grain vs. smoothness.
|
config.py
CHANGED
|
@@ -65,11 +65,14 @@ class AppConfig:
|
|
| 65 |
max_video_duration: int = 8 # seconds (input; longer is auto-trimmed)
|
| 66 |
max_output_long_edge: int = EIGHT_K_W # cap output so it never exceeds 8K
|
| 67 |
|
| 68 |
-
# Video
|
|
|
|
|
|
|
| 69 |
max_video_long_edge: int = 3840 # video output capped to ~4K (8K stays for images)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
| 73 |
|
| 74 |
gpu_duration: int = 120 # seconds requested per @spaces.GPU call
|
| 75 |
server_port: int = 7860
|
|
|
|
| 65 |
max_video_duration: int = 8 # seconds (input; longer is auto-trimmed)
|
| 66 |
max_output_long_edge: int = EIGHT_K_W # cap output so it never exceeds 8K
|
| 67 |
|
| 68 |
+
# Video is processed in CHUNKS β each chunk is one @spaces.GPU call that
|
| 69 |
+
# fits the time budget; chunks are then joined. This lets the WHOLE clip be
|
| 70 |
+
# processed across multiple GPU calls instead of just one short window.
|
| 71 |
max_video_long_edge: int = 3840 # video output capped to ~4K (8K stays for images)
|
| 72 |
+
chunk_frames_plain: int = 120 # frames per GPU call (plain upscale)
|
| 73 |
+
chunk_frames_face: int = 30 # frames per GPU call (face-enhance is ~10Γ slower)
|
| 74 |
+
max_total_video_seconds: int = 20 # overall clip cap (plain); longer is trimmed
|
| 75 |
+
max_total_video_seconds_face: int = 10 # overall clip cap when face-enhance is on
|
| 76 |
|
| 77 |
gpu_duration: int = 120 # seconds requested per @spaces.GPU call
|
| 78 |
server_port: int = 7860
|
pipeline.py
CHANGED
|
@@ -2,20 +2,39 @@
|
|
| 2 |
Enhancement pipeline.
|
| 3 |
|
| 4 |
Image: decode -> Real-ESRGAN upscale -> encode.
|
| 5 |
-
Video
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
import os
|
| 8 |
import cv2
|
| 9 |
import time
|
| 10 |
import numpy as np
|
| 11 |
from pathlib import Path
|
| 12 |
-
from
|
|
|
|
| 13 |
|
| 14 |
from config import AppConfig, EIGHT_K_W, EIGHT_K_H
|
| 15 |
from enhancer import RealESRGANEnhancer, compute_effective_scale
|
| 16 |
from utils.video_processor import VideoProcessor
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
class EnhancePipeline:
|
| 20 |
def __init__(self, config: AppConfig):
|
| 21 |
self.config = config
|
|
@@ -43,30 +62,29 @@ class EnhancePipeline:
|
|
| 43 |
f"{', face-enhanced' if face_enhance else ''})"
|
| 44 |
return out, info
|
| 45 |
|
| 46 |
-
# ---------- video ----------
|
| 47 |
-
def
|
| 48 |
-
self,
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
| 53 |
info = self.video_processor.get_video_info(video_path)
|
| 54 |
fps = info.fps or 30.0
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
dur_cap = getattr(self.config, "max_video_duration", 8)
|
| 61 |
-
target_seconds = min(dur_cap, budget / fps)
|
| 62 |
-
if info.duration and info.duration > target_seconds:
|
| 63 |
self._p(progress, 0.03,
|
| 64 |
-
f"βοΈ Trimming to {
|
| 65 |
-
f"
|
| 66 |
-
video_path = self.video_processor.trim(video_path, 0,
|
| 67 |
info = self.video_processor.get_video_info(video_path)
|
| 68 |
|
| 69 |
-
# Effective scale, capped to the 8K box AND
|
| 70 |
outscale = compute_effective_scale(info.width, info.height, scale_mode,
|
| 71 |
EIGHT_K_W, EIGHT_K_H)
|
| 72 |
max_le = getattr(self.config, "max_video_long_edge", 3840)
|
|
@@ -77,56 +95,49 @@ class EnhancePipeline:
|
|
| 77 |
temp_dir = os.path.join(self.config.video.temp_dir, "processing")
|
| 78 |
frames_dir = os.path.join(temp_dir, "frames")
|
| 79 |
out_dir = os.path.join(temp_dir, "out")
|
| 80 |
-
self.video_processor.cleanup_temp(temp_dir)
|
| 81 |
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
ext = self.config.video.frame_format
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
if frame is None:
|
| 107 |
-
continue
|
| 108 |
-
up = self.enhancer.enhance(frame, outscale, face_enhance=face_enhance)
|
| 109 |
-
processed += 1
|
| 110 |
-
# Renumber sequentially so the output frames are always contiguous.
|
| 111 |
-
cv2.imwrite(os.path.join(out_dir, f"{processed:06d}.{ext}"), up)
|
| 112 |
-
if i % 2 == 0 or i == total - 1:
|
| 113 |
-
p = 0.15 + (i / max(total, 1)) * 0.75
|
| 114 |
-
self._p(progress, p, f"Upscaling frame {i+1}/{total}")
|
| 115 |
-
|
| 116 |
-
if processed == 0:
|
| 117 |
-
raise RuntimeError("No frames were processed.")
|
| 118 |
-
|
| 119 |
-
self._p(progress, 0.92, "π¬ Assembling video (+audio)...")
|
| 120 |
-
self.video_processor.assemble_video(out_dir, output_path, info.fps,
|
| 121 |
-
original_video=video_path)
|
| 122 |
-
elapsed = time.time() - start_t
|
| 123 |
-
note = " β οΈ partial (hit the GPU time limit)" if stopped_early else ""
|
| 124 |
-
msg = (f"β
{info.width}Γ{info.height} β {ow}Γ{oh} (Γ{outscale:.2f}) Β· "
|
| 125 |
-
f"{processed}/{total} frames Β· {elapsed:.0f}s{note}")
|
| 126 |
-
self._p(progress, 1.0, msg)
|
| 127 |
-
return output_path, msg
|
| 128 |
-
finally:
|
| 129 |
-
self.video_processor.cleanup_temp(temp_dir)
|
| 130 |
|
| 131 |
@staticmethod
|
| 132 |
def _p(cb, v, t):
|
|
|
|
| 2 |
Enhancement pipeline.
|
| 3 |
|
| 4 |
Image: decode -> Real-ESRGAN upscale -> encode.
|
| 5 |
+
Video (CHUNKED so the whole clip is processed across multiple GPU calls):
|
| 6 |
+
prepare_video() β CPU: (trim to total cap) + extract all frames
|
| 7 |
+
upscale_frames() β GPU: upscale ONE batch of frames (called per chunk)
|
| 8 |
+
finalize_video() β CPU: assemble all upscaled frames + original audio
|
| 9 |
"""
|
| 10 |
import os
|
| 11 |
import cv2
|
| 12 |
import time
|
| 13 |
import numpy as np
|
| 14 |
from pathlib import Path
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from typing import Optional, Callable, Tuple, List
|
| 17 |
|
| 18 |
from config import AppConfig, EIGHT_K_W, EIGHT_K_H
|
| 19 |
from enhancer import RealESRGANEnhancer, compute_effective_scale
|
| 20 |
from utils.video_processor import VideoProcessor
|
| 21 |
|
| 22 |
|
| 23 |
+
@dataclass
|
| 24 |
+
class VideoPrep:
|
| 25 |
+
temp_dir: str
|
| 26 |
+
frames_dir: str
|
| 27 |
+
out_dir: str
|
| 28 |
+
files: List[str] # full paths to extracted source frames, in order
|
| 29 |
+
outscale: float
|
| 30 |
+
fps: float
|
| 31 |
+
width: int
|
| 32 |
+
height: int
|
| 33 |
+
ow: int
|
| 34 |
+
oh: int
|
| 35 |
+
trimmed_path: str # (possibly trimmed) source, used for audio
|
| 36 |
+
|
| 37 |
+
|
| 38 |
class EnhancePipeline:
|
| 39 |
def __init__(self, config: AppConfig):
|
| 40 |
self.config = config
|
|
|
|
| 62 |
f"{', face-enhanced' if face_enhance else ''})"
|
| 63 |
return out, info
|
| 64 |
|
| 65 |
+
# ---------- video: chunked ----------
|
| 66 |
+
def batch_size(self, face_enhance: bool) -> int:
|
| 67 |
+
return (getattr(self.config, "chunk_frames_face", 30) if face_enhance
|
| 68 |
+
else getattr(self.config, "chunk_frames_plain", 120))
|
| 69 |
+
|
| 70 |
+
def prepare_video(self, video_path: str, scale_mode: str = "4x",
|
| 71 |
+
face_enhance: bool = False,
|
| 72 |
+
progress: Optional[Callable] = None) -> VideoPrep:
|
| 73 |
+
"""CPU: trim to the overall cap and extract all frames."""
|
| 74 |
info = self.video_processor.get_video_info(video_path)
|
| 75 |
fps = info.fps or 30.0
|
| 76 |
|
| 77 |
+
max_total = getattr(self.config, "max_total_video_seconds", 20)
|
| 78 |
+
if face_enhance:
|
| 79 |
+
max_total = min(max_total, getattr(self.config, "max_total_video_seconds_face", 10))
|
| 80 |
+
if info.duration and info.duration > max_total:
|
|
|
|
|
|
|
|
|
|
| 81 |
self._p(progress, 0.03,
|
| 82 |
+
f"βοΈ Trimming to {max_total}s (max total for "
|
| 83 |
+
f"{'face-enhance' if face_enhance else 'this'} mode)...")
|
| 84 |
+
video_path = self.video_processor.trim(video_path, 0, max_total)
|
| 85 |
info = self.video_processor.get_video_info(video_path)
|
| 86 |
|
| 87 |
+
# Effective scale, capped to the 8K box AND the video resolution ceiling.
|
| 88 |
outscale = compute_effective_scale(info.width, info.height, scale_mode,
|
| 89 |
EIGHT_K_W, EIGHT_K_H)
|
| 90 |
max_le = getattr(self.config, "max_video_long_edge", 3840)
|
|
|
|
| 95 |
temp_dir = os.path.join(self.config.video.temp_dir, "processing")
|
| 96 |
frames_dir = os.path.join(temp_dir, "frames")
|
| 97 |
out_dir = os.path.join(temp_dir, "out")
|
| 98 |
+
self.video_processor.cleanup_temp(temp_dir)
|
| 99 |
os.makedirs(out_dir, exist_ok=True)
|
| 100 |
+
|
| 101 |
+
self._p(progress, 0.06, "ποΈ Extracting frames...")
|
| 102 |
+
self.video_processor.extract_frames_to_dir(video_path, frames_dir)
|
| 103 |
+
ext = self.config.video.frame_format
|
| 104 |
+
files = [os.path.join(frames_dir, f)
|
| 105 |
+
for f in sorted(os.listdir(frames_dir)) if f.endswith(ext)]
|
| 106 |
+
|
| 107 |
+
return VideoPrep(temp_dir, frames_dir, out_dir, files, outscale, info.fps,
|
| 108 |
+
info.width, info.height, ow, oh, video_path)
|
| 109 |
+
|
| 110 |
+
def upscale_frames(self, frame_paths: List[str], out_dir: str,
|
| 111 |
+
out_start_index: int, outscale: float, model_name: str,
|
| 112 |
+
denoise_strength: float, face_enhance: bool) -> int:
|
| 113 |
+
"""GPU: upscale one batch of frames into out_dir, numbered contiguously
|
| 114 |
+
from out_start_index. Runs the model β call inside a GPU context.
|
| 115 |
+
Returns the next output index."""
|
| 116 |
+
self.enhancer.ensure_loaded(model_name, denoise_strength)
|
| 117 |
ext = self.config.video.frame_format
|
| 118 |
+
idx = out_start_index
|
| 119 |
+
for fp in frame_paths:
|
| 120 |
+
frame = cv2.imread(fp, cv2.IMREAD_COLOR)
|
| 121 |
+
if frame is None:
|
| 122 |
+
continue
|
| 123 |
+
up = self.enhancer.enhance(frame, outscale, face_enhance=face_enhance)
|
| 124 |
+
idx += 1
|
| 125 |
+
cv2.imwrite(os.path.join(out_dir, f"{idx:06d}.{ext}"), up)
|
| 126 |
+
return idx
|
| 127 |
+
|
| 128 |
+
def finalize_video(self, prep: VideoPrep, output_path: str, processed: int,
|
| 129 |
+
progress: Optional[Callable] = None) -> Tuple[str, str]:
|
| 130 |
+
"""CPU: assemble all upscaled frames + original audio."""
|
| 131 |
+
self._p(progress, 0.94, "π¬ Assembling video (+audio)...")
|
| 132 |
+
self.video_processor.assemble_video(prep.out_dir, output_path, prep.fps,
|
| 133 |
+
original_video=prep.trimmed_path)
|
| 134 |
+
msg = (f"β
{prep.width}Γ{prep.height} β {prep.ow}Γ{prep.oh} "
|
| 135 |
+
f"(Γ{prep.outscale:.2f}) Β· {processed} frames")
|
| 136 |
+
self._p(progress, 1.0, msg)
|
| 137 |
+
return output_path, msg
|
| 138 |
+
|
| 139 |
+
def cleanup(self, prep: VideoPrep):
|
| 140 |
+
self.video_processor.cleanup_temp(prep.temp_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
@staticmethod
|
| 143 |
def _p(cb, v, t):
|