vclmax2 commited on
Commit
62e40b8
·
verified ·
1 Parent(s): 5eb71a0

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +14 -34
app.py CHANGED
@@ -63,8 +63,7 @@ def has_audio_stream(path: str) -> bool:
63
  return bool(result.stdout.strip())
64
 
65
 
66
- @spaces.GPU(duration=120)
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, GPU-resize, encode to H.264 with PyAV.
76
- Avoids writing hundreds of individual PNG files to disk.
 
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
- n_in = len(frame_bytes)
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] GPU: selected {len(selected)} frames", flush=True)
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
- png = frame_bytes[frame_idx]
116
- img = Image.open(io.BytesIO(png)).convert("RGB")
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] GPU: encoded {i}/{len(selected)} frames", flush=True)
135
 
136
- for pkt in stream.encode(): # flush encoder
137
  container.mux(pkt)
138
  container.close()
139
 
140
- print(f"[wr] GPU: done in {time.monotonic()-t0:.1f}s → {out_path}", flush=True)
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 via GPU (PyAV/libx264, no disk I/O)...")
470
- main_path = _gpu_encode_frames(frame_bytes, timestamps, float(duration), w, h)
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)...")