Spaces:
Sleeping
Sleeping
Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -63,8 +63,7 @@ def has_audio_stream(path: str) -> bool:
|
|
| 63 |
return bool(result.stdout.strip())
|
| 64 |
|
| 65 |
|
| 66 |
-
|
| 67 |
-
def _gpu_encode_frames(
|
| 68 |
frame_bytes: list[bytes],
|
| 69 |
timestamps: list[float],
|
| 70 |
duration: float,
|
|
@@ -72,25 +71,22 @@ def _gpu_encode_frames(
|
|
| 72 |
h: int,
|
| 73 |
) -> str:
|
| 74 |
"""
|
| 75 |
-
Decode PNG frames in memory,
|
| 76 |
-
|
|
|
|
| 77 |
"""
|
| 78 |
import av
|
| 79 |
import numpy as np
|
| 80 |
-
import torch
|
| 81 |
-
import torchvision.transforms.functional as TF
|
| 82 |
from PIL import Image
|
| 83 |
|
| 84 |
t0 = time.monotonic()
|
| 85 |
-
|
| 86 |
-
print(f"[wr] GPU: {n_in} frames in memory, target {w}x{h} @ {TARGET_FPS}fps", flush=True)
|
| 87 |
|
| 88 |
-
# Select frames evenly at TARGET_FPS
|
| 89 |
n_out = max(1, int(duration * TARGET_FPS))
|
| 90 |
selected = []
|
| 91 |
for i in range(n_out):
|
| 92 |
t = i / TARGET_FPS
|
| 93 |
-
# Binary search for the closest timestamp
|
| 94 |
lo, hi = 0, len(timestamps) - 1
|
| 95 |
while lo < hi:
|
| 96 |
mid = (lo + hi) // 2
|
|
@@ -99,7 +95,7 @@ def _gpu_encode_frames(
|
|
| 99 |
else:
|
| 100 |
hi = mid
|
| 101 |
selected.append(lo)
|
| 102 |
-
print(f"[wr]
|
| 103 |
|
| 104 |
out_path = f"/tmp/main_{os.getpid()}_{int(time.time())}.mp4"
|
| 105 |
container = av.open(out_path, "w")
|
|
@@ -109,35 +105,19 @@ def _gpu_encode_frames(
|
|
| 109 |
stream.pix_fmt = "yuv420p"
|
| 110 |
stream.options = {"crf": "15", "preset": "fast", "profile": "high", "level": "4.2"}
|
| 111 |
|
| 112 |
-
needs_resize = None # determined on first frame
|
| 113 |
-
|
| 114 |
for i, frame_idx in enumerate(selected):
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
if needs_resize is None:
|
| 119 |
-
needs_resize = img.size != (w, h)
|
| 120 |
-
print(f"[wr] GPU: source frame size {img.size}, resize={needs_resize}", flush=True)
|
| 121 |
-
|
| 122 |
-
if needs_resize:
|
| 123 |
-
t_img = TF.to_tensor(img).cuda() # (C, H, W) float32 on GPU
|
| 124 |
-
t_img = TF.resize(t_img, [h, w], antialias=True) # GPU resize
|
| 125 |
-
np_frame = (t_img.permute(1, 2, 0).cpu().numpy() * 255).astype(np.uint8)
|
| 126 |
-
else:
|
| 127 |
-
np_frame = np.array(img)
|
| 128 |
-
|
| 129 |
-
av_frame = av.VideoFrame.from_ndarray(np_frame, format="rgb24")
|
| 130 |
for pkt in stream.encode(av_frame):
|
| 131 |
container.mux(pkt)
|
| 132 |
-
|
| 133 |
if i % 100 == 0:
|
| 134 |
-
print(f"[wr]
|
| 135 |
|
| 136 |
-
for pkt in stream.encode():
|
| 137 |
container.mux(pkt)
|
| 138 |
container.close()
|
| 139 |
|
| 140 |
-
print(f"[wr]
|
| 141 |
return out_path
|
| 142 |
|
| 143 |
|
|
@@ -466,8 +446,8 @@ async def _record_async(
|
|
| 466 |
timestamps = [f[0] for f in frames]
|
| 467 |
frame_bytes = [f[1] for f in frames]
|
| 468 |
|
| 469 |
-
log(f"encoding {len(frames)} frames
|
| 470 |
-
main_path =
|
| 471 |
log(f"main recording encoded: {main_path}")
|
| 472 |
|
| 473 |
log("assembling final video (audio + segments)...")
|
|
|
|
| 63 |
return bool(result.stdout.strip())
|
| 64 |
|
| 65 |
|
| 66 |
+
def _encode_frames(
|
|
|
|
| 67 |
frame_bytes: list[bytes],
|
| 68 |
timestamps: list[float],
|
| 69 |
duration: float,
|
|
|
|
| 71 |
h: int,
|
| 72 |
) -> str:
|
| 73 |
"""
|
| 74 |
+
Decode PNG frames in memory, encode to H.264 with PyAV (libx264).
|
| 75 |
+
No resize needed — Chrome always delivers frames at exactly w×h via
|
| 76 |
+
Emulation.setDeviceMetricsOverride + screencast maxWidth/maxHeight.
|
| 77 |
"""
|
| 78 |
import av
|
| 79 |
import numpy as np
|
|
|
|
|
|
|
| 80 |
from PIL import Image
|
| 81 |
|
| 82 |
t0 = time.monotonic()
|
| 83 |
+
print(f"[wr] encode: {len(frame_bytes)} frames → {w}x{h} @ {TARGET_FPS}fps", flush=True)
|
|
|
|
| 84 |
|
| 85 |
+
# Select frames evenly at TARGET_FPS via binary search on timestamps
|
| 86 |
n_out = max(1, int(duration * TARGET_FPS))
|
| 87 |
selected = []
|
| 88 |
for i in range(n_out):
|
| 89 |
t = i / TARGET_FPS
|
|
|
|
| 90 |
lo, hi = 0, len(timestamps) - 1
|
| 91 |
while lo < hi:
|
| 92 |
mid = (lo + hi) // 2
|
|
|
|
| 95 |
else:
|
| 96 |
hi = mid
|
| 97 |
selected.append(lo)
|
| 98 |
+
print(f"[wr] encode: selected {len(selected)} frames", flush=True)
|
| 99 |
|
| 100 |
out_path = f"/tmp/main_{os.getpid()}_{int(time.time())}.mp4"
|
| 101 |
container = av.open(out_path, "w")
|
|
|
|
| 105 |
stream.pix_fmt = "yuv420p"
|
| 106 |
stream.options = {"crf": "15", "preset": "fast", "profile": "high", "level": "4.2"}
|
| 107 |
|
|
|
|
|
|
|
| 108 |
for i, frame_idx in enumerate(selected):
|
| 109 |
+
img = Image.open(io.BytesIO(frame_bytes[frame_idx])).convert("RGB")
|
| 110 |
+
av_frame = av.VideoFrame.from_ndarray(np.array(img), format="rgb24")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
for pkt in stream.encode(av_frame):
|
| 112 |
container.mux(pkt)
|
|
|
|
| 113 |
if i % 100 == 0:
|
| 114 |
+
print(f"[wr] encode: {i}/{len(selected)} frames", flush=True)
|
| 115 |
|
| 116 |
+
for pkt in stream.encode():
|
| 117 |
container.mux(pkt)
|
| 118 |
container.close()
|
| 119 |
|
| 120 |
+
print(f"[wr] encode: done in {time.monotonic()-t0:.1f}s → {out_path}", flush=True)
|
| 121 |
return out_path
|
| 122 |
|
| 123 |
|
|
|
|
| 446 |
timestamps = [f[0] for f in frames]
|
| 447 |
frame_bytes = [f[1] for f in frames]
|
| 448 |
|
| 449 |
+
log(f"encoding {len(frames)} frames (PyAV/libx264, no disk I/O)...")
|
| 450 |
+
main_path = _encode_frames(frame_bytes, timestamps, float(duration), w, h)
|
| 451 |
log(f"main recording encoded: {main_path}")
|
| 452 |
|
| 453 |
log("assembling final video (audio + segments)...")
|