abangopera commited on
Commit
c1ef87c
·
verified ·
1 Parent(s): 4a0f977
Files changed (1) hide show
  1. app.py +201 -1
app.py CHANGED
@@ -3,12 +3,14 @@ import subprocess
3
  import os
4
  import uuid
5
  import tempfile
 
 
6
 
7
  app = Flask(__name__)
8
 
9
  # Folder untuk simpan hasil
10
  # OUTPUT_DIR = "downloads"
11
- OUTPUT_DIR = "/tmp/downloads"
12
  os.makedirs(OUTPUT_DIR, exist_ok=True)
13
 
14
  def run_cmd(args, shell=False):
@@ -95,6 +97,64 @@ def probe_media_duration_seconds(input_path):
95
  pass
96
  return None
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  @app.route('/image-to-video', methods=['POST'])
99
  def image_to_video():
100
  """Create a video from a still image and an audio file.
@@ -239,6 +299,146 @@ def image_to_video():
239
  pass
240
  return response
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  @app.route('/download-split', methods=['GET'])
243
  def download_and_split():
244
  """Download a full YouTube video and split it into fixed-duration parts.
 
3
  import os
4
  import uuid
5
  import tempfile
6
+ import urllib.request
7
+ import urllib.parse
8
 
9
  app = Flask(__name__)
10
 
11
  # Folder untuk simpan hasil
12
  # OUTPUT_DIR = "downloads"
13
+ OUTPUT_DIR = "/tmp/downloads"
14
  os.makedirs(OUTPUT_DIR, exist_ok=True)
15
 
16
  def run_cmd(args, shell=False):
 
97
  pass
98
  return None
99
 
100
+ def _guess_extension_from_url_or_ct(url, content_type, fallback_ext):
101
+ try:
102
+ # Try from URL path
103
+ path = urllib.parse.urlparse(url).path
104
+ ext = os.path.splitext(path)[1]
105
+ if ext:
106
+ return ext
107
+ except Exception:
108
+ pass
109
+ # Fallback from content-type
110
+ if content_type:
111
+ ct = content_type.lower().split(';')[0].strip()
112
+ mapping = {
113
+ 'image/jpeg': '.jpg',
114
+ 'image/jpg': '.jpg',
115
+ 'image/png': '.png',
116
+ 'image/webp': '.webp',
117
+ 'image/bmp': '.bmp',
118
+ 'image/gif': '.gif',
119
+ 'audio/mpeg': '.mp3',
120
+ 'audio/mp3': '.mp3',
121
+ 'audio/aac': '.aac',
122
+ 'audio/wav': '.wav',
123
+ 'audio/x-wav': '.wav',
124
+ 'audio/flac': '.flac',
125
+ 'audio/ogg': '.ogg',
126
+ 'audio/webm': '.webm',
127
+ }
128
+ if ct in mapping:
129
+ return mapping[ct]
130
+ return fallback_ext
131
+
132
+ def download_url_to_file(src_url, default_ext):
133
+ """Download a URL to a temp file under OUTPUT_DIR, return the file path.
134
+
135
+ This avoids extra deps; uses urllib with a sensible timeout.
136
+ """
137
+ try:
138
+ req = urllib.request.Request(src_url, headers={
139
+ 'User-Agent': 'Mozilla/5.0 (compatible; image-audio-fetcher)'
140
+ })
141
+ with urllib.request.urlopen(req, timeout=60) as resp:
142
+ content_type = resp.headers.get('Content-Type', '')
143
+ ext = _guess_extension_from_url_or_ct(src_url, content_type, default_ext)
144
+ temp_name = f"dl_{uuid.uuid4().hex}{ext}"
145
+ out_path = os.path.join(OUTPUT_DIR, temp_name)
146
+ with open(out_path, 'wb') as f:
147
+ while True:
148
+ chunk = resp.read(1024 * 64)
149
+ if not chunk:
150
+ break
151
+ f.write(chunk)
152
+ if not os.path.exists(out_path) or os.path.getsize(out_path) == 0:
153
+ raise RuntimeError('Downloaded file is empty')
154
+ return out_path
155
+ except Exception as e:
156
+ raise RuntimeError(str(e))
157
+
158
  @app.route('/image-to-video', methods=['POST'])
159
  def image_to_video():
160
  """Create a video from a still image and an audio file.
 
299
  pass
300
  return response
301
 
302
+ @app.route('/image-to-video-url', methods=['POST'])
303
+ def image_to_video_url():
304
+ """Create a video from image_url + audio_url (JSON body).
305
+
306
+ JSON fields:
307
+ - image_url (required)
308
+ - audio_url (required)
309
+ - duration (required unless audio_start/audio_end provided) seconds or HH:MM:SS
310
+ - audio_start (optional) seconds or HH:MM:SS
311
+ - audio_end (optional) seconds or HH:MM:SS
312
+ - resolution (optional) preset or WIDTHxHEIGHT
313
+ - fit (optional) 'pad' (default) or 'crop'
314
+ """
315
+ try:
316
+ data = request.get_json(silent=True) or {}
317
+ except Exception:
318
+ data = {}
319
+
320
+ # Support JSON body and/or query params as fallback
321
+ image_url = (data.get('image_url') or request.args.get('image_url') or '').strip()
322
+ audio_url = (data.get('audio_url') or request.args.get('audio_url') or '').strip()
323
+ duration_str = data.get('duration') if 'duration' in data else request.args.get('duration')
324
+ audio_start_str = data.get('audio_start') if 'audio_start' in data else request.args.get('audio_start')
325
+ audio_end_str = data.get('audio_end') if 'audio_end' in data else request.args.get('audio_end')
326
+ resolution = data.get('resolution') if 'resolution' in data else request.args.get('resolution')
327
+ fit = (data.get('fit') if 'fit' in data else request.args.get('fit') or 'pad').lower()
328
+
329
+ if not image_url or not audio_url:
330
+ return jsonify({"error": "Harap kirim image_url dan audio_url"}), 400
331
+
332
+ # Resolve timing parameters
333
+ audio_start_sec = None
334
+ audio_end_sec = None
335
+ duration_sec = None
336
+
337
+ if audio_start_str:
338
+ audio_start_sec = int(float(hms_to_seconds(str(audio_start_str))))
339
+ if audio_start_sec < 0:
340
+ audio_start_sec = 0
341
+ if audio_end_str:
342
+ audio_end_sec = int(float(hms_to_seconds(str(audio_end_str))))
343
+ if audio_end_sec < 0:
344
+ audio_end_sec = 0
345
+
346
+ if audio_start_sec is not None or audio_end_sec is not None:
347
+ if audio_start_sec is None:
348
+ audio_start_sec = 0
349
+ if audio_end_sec is not None and audio_end_sec > audio_start_sec:
350
+ duration_sec = audio_end_sec - audio_start_sec
351
+ elif duration_str:
352
+ tmp = int(float(hms_to_seconds(str(duration_str))))
353
+ duration_sec = tmp if tmp > 0 else None
354
+ if duration_sec is None or duration_sec <= 0:
355
+ return jsonify({"error": "Harap berikan rentang audio yang valid (audio_start < audio_end) atau sertakan duration"}), 400
356
+ else:
357
+ if not duration_str:
358
+ return jsonify({"error": "Parameter duration wajib diisi (detik atau HH:MM:SS)"}), 400
359
+ tmp = int(float(hms_to_seconds(str(duration_str))))
360
+ duration_sec = tmp if tmp > 0 else None
361
+ if duration_sec is None:
362
+ return jsonify({"error": "Duration tidak valid"}), 400
363
+
364
+ width, height = get_resolution_params(resolution)
365
+ if (fit in ['crop', 'pad']) and (resolution is not None) and (width is None or height is None):
366
+ return jsonify({"error": "Format resolution tidak valid. Gunakan preset (mis. portrait) atau WIDTHxHEIGHT"}), 400
367
+ width, height = normalize_even(width, height)
368
+
369
+ # Download inputs
370
+ try:
371
+ image_path = download_url_to_file(image_url, '.png')
372
+ audio_path = download_url_to_file(audio_url, '.mp3')
373
+ except Exception as e:
374
+ return jsonify({"error": f"Gagal download file dari URL: {str(e)}"}), 400
375
+
376
+ # Prepare output temp file
377
+ temp_out = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
378
+ temp_out.close()
379
+ output_path = temp_out.name
380
+
381
+ # Build FFmpeg command (same as upload variant)
382
+ ffmpeg_cmd = [
383
+ "ffmpeg", "-y",
384
+ "-loop", "1", "-i", image_path,
385
+ ]
386
+ if audio_start_sec is not None:
387
+ ffmpeg_cmd += ["-ss", str(audio_start_sec)]
388
+ ffmpeg_cmd += ["-i", audio_path]
389
+ ffmpeg_cmd += ["-r", "30"]
390
+ if width and height:
391
+ if fit == 'crop':
392
+ vf = f"scale={width}:{height}:force_original_aspect_ratio=increase,crop={width}:{height}"
393
+ else:
394
+ vf = f"scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2:black"
395
+ ffmpeg_cmd += ["-vf", vf]
396
+ ffmpeg_cmd += [
397
+ "-t", str(duration_sec),
398
+ "-c:v", "libx264", "-pix_fmt", "yuv420p",
399
+ "-c:a", "aac", "-b:a", "192k",
400
+ "-shortest",
401
+ "-movflags", "+faststart",
402
+ output_path
403
+ ]
404
+
405
+ result = run_cmd(ffmpeg_cmd)
406
+ # Cleanup downloads
407
+ try:
408
+ if os.path.exists(image_path):
409
+ os.remove(image_path)
410
+ if os.path.exists(audio_path):
411
+ os.remove(audio_path)
412
+ except Exception:
413
+ pass
414
+
415
+ if result.returncode != 0:
416
+ try:
417
+ os.remove(output_path)
418
+ except Exception:
419
+ pass
420
+ return jsonify({
421
+ "error": "Gagal membuat video dari image dan audio",
422
+ "details": result.stderr
423
+ }), 500
424
+
425
+ if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
426
+ try:
427
+ os.remove(output_path)
428
+ except Exception:
429
+ pass
430
+ return jsonify({"error": "File hasil kosong atau tidak ada"}), 500
431
+
432
+ from flask import send_file
433
+ response = send_file(output_path, as_attachment=True, download_name=f"image2video_{uuid.uuid4().hex}.mp4")
434
+ @response.call_on_close
435
+ def cleanup():
436
+ try:
437
+ os.remove(output_path)
438
+ except Exception:
439
+ pass
440
+ return response
441
+
442
  @app.route('/download-split', methods=['GET'])
443
  def download_and_split():
444
  """Download a full YouTube video and split it into fixed-duration parts.