MaxonML commited on
Commit
bba6d0a
·
verified ·
1 Parent(s): 474490d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +312 -0
app.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import uuid
6
+ import zipfile
7
+
8
+ import cv2
9
+ import numpy as np
10
+ from flask import Flask, jsonify, render_template, request, send_file
11
+ from werkzeug.utils import secure_filename
12
+
13
+ app = Flask(__name__)
14
+
15
+ UPLOAD_FOLDER = "/tmp/uploads"
16
+ PROCESSED_FOLDER = "/tmp/processed"
17
+ app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
18
+ app.config["PROCESSED_FOLDER"] = PROCESSED_FOLDER
19
+
20
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
21
+ os.makedirs(PROCESSED_FOLDER, exist_ok=True)
22
+
23
+ # Try imageio_ffmpeg (local dev), fall back to system ffmpeg (HF Spaces / Docker)
24
+ try:
25
+ import imageio_ffmpeg
26
+
27
+ FFMPEG_EXE = imageio_ffmpeg.get_ffmpeg_exe()
28
+ except ImportError:
29
+ FFMPEG_EXE = shutil.which("ffmpeg") or "ffmpeg"
30
+
31
+
32
+ @app.route("/")
33
+ def index():
34
+ return render_template("index.html")
35
+
36
+
37
+ @app.route("/upload", methods=["POST"])
38
+ def upload_files():
39
+ if "videos" not in request.files:
40
+ return jsonify({"error": "No files"}), 400
41
+
42
+ files = request.files.getlist("videos")
43
+ saved_files = []
44
+ frame_url = None
45
+ orig_w, orig_h = 0, 0
46
+
47
+ for idx, file in enumerate(files):
48
+ if file.filename:
49
+ filename = secure_filename(file.filename)
50
+ unique_filename = f"{uuid.uuid4().hex}_{filename}"
51
+ filepath = os.path.join(app.config["UPLOAD_FOLDER"], unique_filename)
52
+ file.save(filepath)
53
+ saved_files.append(unique_filename)
54
+
55
+ if idx == 0:
56
+ cap = cv2.VideoCapture(filepath)
57
+ orig_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
58
+ orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
59
+ ret, frame = cap.read()
60
+ cap.release()
61
+
62
+ if ret:
63
+ frame_filename = f"{unique_filename}.jpg"
64
+ frame_path = os.path.join(
65
+ app.config["UPLOAD_FOLDER"], frame_filename
66
+ )
67
+ cv2.imwrite(frame_path, frame)
68
+ frame_url = f"/uploads/{frame_filename}"
69
+
70
+ return jsonify(
71
+ {
72
+ "filenames": saved_files,
73
+ "frame_url": frame_url,
74
+ "orig_w": orig_w,
75
+ "orig_h": orig_h,
76
+ }
77
+ )
78
+
79
+
80
+ @app.route("/uploads/<filename>")
81
+ def serve_upload(filename):
82
+ return send_file(os.path.join(app.config["UPLOAD_FOLDER"], filename))
83
+
84
+
85
+ @app.route("/process", methods=["POST"])
86
+ def process_videos():
87
+ data = request.json
88
+ filenames = data.get("filenames", [])
89
+ method = data.get("method", "blur_heavy")
90
+ tool = data.get("tool", "box")
91
+ upscale = data.get("upscale", "none")
92
+
93
+ orig_w, orig_h = int(data["orig_w"]), int(data["orig_h"])
94
+ processed_files = []
95
+
96
+ # Handle coordinates based on the tool used
97
+ if tool == "box":
98
+ px, py, pw, ph = (
99
+ float(data["px"]),
100
+ float(data["py"]),
101
+ float(data["pw"]),
102
+ float(data["ph"]),
103
+ )
104
+ x = max(0, min(int(px * orig_w), orig_w - 2))
105
+ y = max(0, min(int(py * orig_h), orig_h - 2))
106
+ w = max(1, min(int(pw * orig_w), orig_w - x))
107
+ h = max(1, min(int(ph * orig_h), orig_h - y))
108
+ mask_path = None
109
+
110
+ elif tool == "brush":
111
+ # Decode the painted canvas into an image
112
+ mask_data = data.get("mask_b64", "").split(",")[1]
113
+ img_bytes = base64.b64decode(mask_data)
114
+ nparr = np.frombuffer(img_bytes, np.uint8)
115
+ mask_img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED) # Reads RGBA
116
+
117
+ # Extract the alpha channel (where the user painted)
118
+ alpha_channel = mask_img[:, :, 3]
119
+ full_mask = cv2.resize(
120
+ alpha_channel, (orig_w, orig_h), interpolation=cv2.INTER_NEAREST
121
+ )
122
+
123
+ # Find the exact bounding box of their painting
124
+ coords = cv2.findNonZero(full_mask)
125
+ if coords is None:
126
+ return jsonify({"error": "No brush strokes detected."}), 400
127
+
128
+ x, y, w, h = cv2.boundingRect(coords)
129
+
130
+ # Crop the mask to the exact bounding box to map it to FFmpeg
131
+ mask_crop = full_mask[y : y + h, x : x + w]
132
+ mask_path = os.path.join(
133
+ app.config["UPLOAD_FOLDER"], f"mask_{uuid.uuid4().hex}.png"
134
+ )
135
+ cv2.imwrite(mask_path, mask_crop)
136
+
137
+ for filename in filenames:
138
+ input_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
139
+ # Extract original name by removing the uuid prefix (32 hex chars + underscore)
140
+ original_name = filename.split("_", 1)[1] if "_" in filename else filename
141
+ name_part, ext_part = os.path.splitext(original_name)
142
+ friendly_name = f"{name_part}_Cle{ext_part}"
143
+ output_filename = f"clean_{filename}"
144
+ output_path = os.path.join(app.config["PROCESSED_FOLDER"], output_filename)
145
+
146
+ # Build FFmpeg commands depending on the tool and method
147
+ if tool == "box":
148
+ if method == "delogo":
149
+ vf = f"delogo=x={x}:y={y}:w={w}:h={h}"
150
+ elif method == "blur_light":
151
+ vf = f"split[m][r];[r]crop={w}:{h}:{x}:{y},gblur=sigma=10[b];[m][b]overlay={x}:{y}"
152
+ elif method == "blur_heavy":
153
+ vf = f"split[m][r];[r]crop={w}:{h}:{x}:{y},gblur=sigma=35[b];[m][b]overlay={x}:{y}"
154
+ elif method == "pixelate":
155
+ pw_sq, ph_sq = max(1, w // 15), max(1, h // 15)
156
+ vf = f"split[m][r];[r]crop={w}:{h}:{x}:{y},scale={pw_sq}:{ph_sq},scale={w}:{h}:flags=neighbor[p];[m][p]overlay={x}:{y}"
157
+ elif method == "black_box":
158
+ vf = f"drawbox=x={x}:y={y}:w={w}:h={h}:color=black:t=fill"
159
+
160
+ command = [
161
+ FFMPEG_EXE,
162
+ "-y",
163
+ "-i",
164
+ input_path,
165
+ "-vf",
166
+ vf,
167
+ "-c:a",
168
+ "copy",
169
+ output_path,
170
+ ]
171
+
172
+ elif tool == "brush":
173
+ # Brush uses an advanced alpha-merge complex filter to shape the blur to the paint strokes
174
+ if method == "delogo":
175
+ # Delogo strictly requires a box, so we fall back to the bounding box of the paint
176
+ command = [
177
+ FFMPEG_EXE,
178
+ "-y",
179
+ "-i",
180
+ input_path,
181
+ "-vf",
182
+ f"delogo=x={x}:y={y}:w={w}:h={h}",
183
+ "-c:a",
184
+ "copy",
185
+ output_path,
186
+ ]
187
+ else:
188
+ if method == "blur_light":
189
+ effect = f"[0:v]crop={w}:{h}:{x}:{y},gblur=sigma=10[fx];"
190
+ elif method == "blur_heavy":
191
+ effect = f"[0:v]crop={w}:{h}:{x}:{y},gblur=sigma=35[fx];"
192
+ elif method == "pixelate":
193
+ pw_sq, ph_sq = max(1, w // 15), max(1, h // 15)
194
+ effect = f"[0:v]crop={w}:{h}:{x}:{y},scale={pw_sq}:{ph_sq},scale={w}:{h}:flags=neighbor[fx];"
195
+ elif method == "black_box":
196
+ effect = f"color=black:size={w}x{h}[fx];"
197
+
198
+ # Combine the cropped effect with the painted alpha mask
199
+ filter_chain = (
200
+ effect
201
+ + f"[fx][1:v]alphamerge[al];[0:v][al]overlay={x}:{y}:shortest=1"
202
+ )
203
+
204
+ command = [
205
+ FFMPEG_EXE,
206
+ "-y",
207
+ "-i",
208
+ input_path,
209
+ "-loop",
210
+ "1",
211
+ "-i",
212
+ mask_path, # Load the painted mask as an infinite video layer
213
+ "-filter_complex",
214
+ filter_chain,
215
+ "-c:a",
216
+ "copy",
217
+ output_path,
218
+ ]
219
+
220
+ try:
221
+ subprocess.run(
222
+ command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
223
+ )
224
+ processed_files.append((output_filename, friendly_name))
225
+ except subprocess.CalledProcessError as e:
226
+ print("FFMPEG ERROR:", e.stderr.decode())
227
+ return jsonify({"error": "FFmpeg processing failed"}), 500
228
+
229
+ # --- Optional Upscale Pass ---
230
+ if upscale != "none":
231
+ upscaled_files = []
232
+ for internal_name, friendly in processed_files:
233
+ src = os.path.join(app.config["PROCESSED_FOLDER"], internal_name)
234
+ up_filename = f"up_{internal_name}"
235
+ up_path = os.path.join(app.config["PROCESSED_FOLDER"], up_filename)
236
+
237
+ # Determine target resolution
238
+ if upscale == "1.5x":
239
+ scale_expr = "scale=iw*1.5:ih*1.5:flags=lanczos"
240
+ elif upscale == "2x":
241
+ scale_expr = "scale=iw*2:ih*2:flags=lanczos"
242
+ elif upscale == "4k":
243
+ scale_expr = "scale=3840:2160:force_original_aspect_ratio=decrease:flags=lanczos,pad=3840:2160:(ow-iw)/2:(oh-ih)/2"
244
+ else:
245
+ scale_expr = "scale=iw*2:ih*2:flags=lanczos"
246
+
247
+ # Lanczos upscale + light unsharp mask to sharpen
248
+ vf_upscale = f"{scale_expr},unsharp=5:5:0.5:5:5:0.0"
249
+
250
+ up_cmd = [
251
+ FFMPEG_EXE,
252
+ "-y",
253
+ "-i",
254
+ src,
255
+ "-vf",
256
+ vf_upscale,
257
+ "-c:v",
258
+ "libx264",
259
+ "-crf",
260
+ "18",
261
+ "-preset",
262
+ "slow",
263
+ "-c:a",
264
+ "copy",
265
+ up_path,
266
+ ]
267
+
268
+ try:
269
+ subprocess.run(
270
+ up_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
271
+ )
272
+ # Update friendly name to indicate upscale
273
+ fn_part, fn_ext = os.path.splitext(friendly)
274
+ upscaled_friendly = f"{fn_part}_HD{fn_ext}"
275
+ upscaled_files.append((up_filename, upscaled_friendly))
276
+ except subprocess.CalledProcessError as e:
277
+ print("UPSCALE ERROR:", e.stderr.decode())
278
+ return jsonify({"error": "Video upscaling failed"}), 500
279
+
280
+ processed_files = upscaled_files
281
+
282
+ if len(processed_files) == 1:
283
+ internal_name, friendly = processed_files[0]
284
+ return jsonify(
285
+ {"download_url": f"/download/{internal_name}", "download_name": friendly}
286
+ )
287
+ else:
288
+ zip_filename = f"bulk_processed_{uuid.uuid4().hex[:6]}.zip"
289
+ zip_path = os.path.join(app.config["PROCESSED_FOLDER"], zip_filename)
290
+ with zipfile.ZipFile(zip_path, "w") as zipf:
291
+ for internal_name, friendly in processed_files:
292
+ zipf.write(
293
+ os.path.join(app.config["PROCESSED_FOLDER"], internal_name),
294
+ friendly,
295
+ )
296
+ return jsonify(
297
+ {"download_url": f"/download/{zip_filename}", "download_name": zip_filename}
298
+ )
299
+
300
+
301
+ @app.route("/download/<filename>")
302
+ def download_file(filename):
303
+ download_name = request.args.get("name", filename)
304
+ return send_file(
305
+ os.path.join(app.config["PROCESSED_FOLDER"], filename),
306
+ as_attachment=True,
307
+ download_name=download_name,
308
+ )
309
+
310
+
311
+ if __name__ == "__main__":
312
+ app.run(host="0.0.0.0", port=7860, debug=False)