trydrama-neuralgarage commited on
Commit
facd56f
·
verified ·
1 Parent(s): 1ceed74

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -21
app.py CHANGED
@@ -17,7 +17,7 @@ import time
17
  import traceback
18
  import uuid
19
  from collections import defaultdict
20
- from datetime import date
21
 
22
  import gradio as gr
23
  import requests
@@ -30,8 +30,11 @@ BASE = os.environ.get(
30
  "https://studio.syncmonster.ai/neuralgarage/saas/api/v1/clients",
31
  ).rstrip("/")
32
  HEADERS = {"x-api-key": API_KEY}
33
- MAX_SECONDS = float(os.environ.get("MAX_SECONDS", 10))
34
- PER_USER_DAILY = int(os.environ.get("PER_USER_DAILY", 3))
 
 
 
35
  GLOBAL_DAILY = int(os.environ.get("GLOBAL_DAILY", 200))
36
  ADMIN_USERS = {u.strip().lower() for u in os.environ.get("ADMIN_USERS", "").split(",") if u.strip()}
37
  FLOW = os.environ.get("FLOW", "oneshot").strip().lower() # "oneshot" (default) or "twostep"
@@ -292,12 +295,15 @@ def video_seconds(path):
292
  f"ffprobe: {short(out.stderr, 300)}")
293
 
294
 
295
- def normalize_video(src, workdir):
296
  """Re-encode any upload (HEVC/ProRes .mov etc.) to H.264 + AAC MP4,
297
- which browsers can preview and the API can read. Keeps the audio track."""
 
298
  out = os.path.join(workdir, "input_h264.mp4")
 
 
299
  result = subprocess.run(
300
- ["ffmpeg", "-y", "-i", src,
301
  "-map", "0:v:0", "-map", "0:a:0?",
302
  "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
303
  "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
@@ -348,20 +354,47 @@ def watermark(src, workdir, run):
348
  return out
349
 
350
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  def prepare_upload(video):
352
- """Runs right after upload so the preview plays and the API gets a clean file."""
 
 
 
353
  if not video:
354
- return None
355
  try:
356
  before = probe(video)
357
  seconds = video_seconds(video)
358
- if seconds > MAX_SECONDS + 0.5:
359
- log.info(f"[upload] rejected (too long, {seconds:.1f}s): {before}")
360
- raise DemoError("TOO_LONG", f"Please use a clip of {int(MAX_SECONDS)} seconds or less.")
361
- out = normalize_video(video, tempfile.mkdtemp())
362
  log.info(f"[upload] original: {before}")
363
- log.info(f"[upload] converted: {probe(out)}")
364
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  except DemoError as e:
366
  raise gr.Error(e.user_message, print_exception=False)
367
  except Exception:
@@ -369,6 +402,81 @@ def prepare_upload(video):
369
  raise gr.Error("Couldn't process this upload. Try another file.", print_exception=False)
370
 
371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  # ---------- usage limits ----------
373
 
374
  _usage = defaultdict(int)
@@ -571,7 +679,7 @@ def settings_chips(drama):
571
 
572
  # ---------- main handler ----------
573
 
574
- def run(video, mood, excited, angry, profile: gr.OAuthProfile | None):
575
  def stop(message):
576
  raise gr.Error(message, print_exception=False)
577
 
@@ -588,6 +696,7 @@ def run(video, mood, excited, angry, profile: gr.OAuthProfile | None):
588
  ctx = Run(profile.username)
589
  ctx.log(f"new run: user={ctx.user} admin={is_admin(ctx.user)} drama={drama} flow={FLOW}")
590
  counted = False
 
591
  try:
592
  take_quota(ctx.user)
593
  counted = True
@@ -595,13 +704,15 @@ def run(video, mood, excited, angry, profile: gr.OAuthProfile | None):
595
  ctx.stage = "prepare"
596
  yield None, status_html("working", "Preparing your clip…", 1)
597
  if video_seconds(video) > MAX_SECONDS + 0.5:
598
- raise DemoError("TOO_LONG", f"Please use a clip of {int(MAX_SECONDS)} seconds or less.")
 
599
  workdir = tempfile.mkdtemp()
600
  if not os.path.basename(video).startswith("input_h264"):
601
  video = normalize_video(video, workdir) # sample clips skip the upload step
602
  audio, audio_source = extract_audio(video, workdir)
603
  v, a = probe(video), probe(audio)
604
  ctx.facts["video"] = f"{v.get('video')} {v.get('duration_s')}s {v.get('size_kb')}KB"
 
605
  ctx.facts["audio"] = f"{a.get('audio')} {a.get('duration_s')}s ({audio_source})"
606
  ctx.log(f"video: {v}")
607
  ctx.log(f"audio: {a} ({audio_source})")
@@ -628,6 +739,7 @@ def run(video, mood, excited, angry, profile: gr.OAuthProfile | None):
628
  ctx.stage = "watermark"
629
  result = watermark(raw, workdir, ctx)
630
  report(ctx, "SUCCESS")
 
631
  yield result, status_html("done", f"Done in {ctx.elapsed()}", 5,
632
  settings_chips(drama))
633
 
@@ -636,8 +748,10 @@ def run(video, mood, excited, angry, profile: gr.OAuthProfile | None):
636
  refund_quota(ctx.user)
637
  if e.category == "LIMIT":
638
  ctx.log(f"blocked by usage limit: {e.user_message}")
 
639
  else:
640
  report(ctx, "FAILED", e.category, e.detail)
 
641
  yield None, status_html("error", e.user_message, note=f"Reference: {ctx.id}")
642
  stop(f"{e.user_message} (ref: {ctx.id})")
643
  except gr.Error:
@@ -647,6 +761,7 @@ def run(video, mood, excited, angry, profile: gr.OAuthProfile | None):
647
  refund_quota(ctx.user)
648
  ctx.log(f"UNEXPECTED {type(e).__name__}: {e}\n{traceback.format_exc()}", logging.ERROR)
649
  report(ctx, "FAILED", "UNEXPECTED", f"{type(e).__name__}: {e}")
 
650
  yield None, status_html("error", "Something unexpected went wrong.", note=f"Reference: {ctx.id}")
651
  stop(f"Something unexpected went wrong. (ref: {ctx.id})")
652
 
@@ -881,6 +996,13 @@ CSS = """
881
  #dr-go { height: 50px; font-size: 16px !important; font-weight: 600 !important; border-radius: 10px !important;
882
  box-shadow: none !important; transition: background .15s ease; }
883
  .gradio-container .dr-fine { font-size: 12px !important; color: var(--dr-faint) !important; text-align: center !important; margin: 0 !important; }
 
 
 
 
 
 
 
884
 
885
  /* status */
886
  .gradio-container .dr-status { border-radius: 10px; padding: 14px 16px; border: 1px solid var(--dr-line); background: #0e0e11; }
@@ -1021,8 +1143,14 @@ with gr.Blocks(title=PAGE_TITLE) as demo:
1021
  with gr.Row(elem_id="dr-main", equal_height=True):
1022
  with gr.Column(scale=1, elem_classes="dr-card"):
1023
  gr.HTML('<div class="dr-label"><b>01</b>Upload a clip</div>'
1024
- '<div class="dr-hint">One person, face clearly visible, front-on. .mp4 or .mov.</div>')
 
1025
  video_in = gr.Video(show_label=False, sources=["upload"], height=320, elem_classes="dr-media")
 
 
 
 
 
1026
  samples = [[p] for p in sorted(glob.glob("examples/*.mp4"))]
1027
  if samples:
1028
  gr.Examples(samples, inputs=[video_in], label="No clip handy? Try one of these")
@@ -1036,9 +1164,12 @@ with gr.Blocks(title=PAGE_TITLE) as demo:
1036
  info="−1 very sad · 0 neutral · +1 big smile")
1037
  excited = gr.Slider(0, 1, value=d_exc, step=0.05, label="Excitement")
1038
  angry = gr.Slider(0, 1, value=d_ang, step=0.05, label="Anger")
 
 
1039
  go = gr.Button("Change expression", variant="primary", elem_id="dr-go")
1040
  gr.HTML('<div class="dr-fine">Only upload footage you have the right to use. '
1041
- 'Demo results are watermarked.</div>')
 
1042
 
1043
  with gr.Column(scale=1, elem_classes="dr-card"):
1044
  gr.HTML('<div class="dr-label"><b>03</b>Your result</div>'
@@ -1099,11 +1230,16 @@ with gr.Blocks(title=PAGE_TITLE) as demo:
1099
  for slider in (mood, excited, angry):
1100
  slider.release(lambda: "Custom", outputs=[preset],
1101
  api_visibility="private", show_progress="hidden")
1102
- video_in.upload(prepare_upload, inputs=[video_in], outputs=[video_in], api_visibility="private")
1103
- video_in.clear(lambda: (None, IDLE), outputs=[video_out, status_out], api_visibility="private")
 
 
 
 
 
1104
  go.click(
1105
  run,
1106
- inputs=[video_in, mood, excited, angry],
1107
  outputs=[video_out, status_out],
1108
  concurrency_limit=2,
1109
  api_visibility="private",
 
17
  import traceback
18
  import uuid
19
  from collections import defaultdict
20
+ from datetime import date, datetime, timezone
21
 
22
  import gradio as gr
23
  import requests
 
30
  "https://studio.syncmonster.ai/neuralgarage/saas/api/v1/clients",
31
  ).rstrip("/")
32
  HEADERS = {"x-api-key": API_KEY}
33
+ MAX_SECONDS = float(os.environ.get("MAX_SECONDS", 15))
34
+ PER_USER_DAILY = int(os.environ.get("PER_USER_DAILY", 5))
35
+ MAX_UPLOAD_SECONDS = float(os.environ.get("MAX_UPLOAD_SECONDS", 600)) # longest upload we'll trim from
36
+ LEADS_DATASET = os.environ.get("LEADS_DATASET", "").strip() # e.g. trydrama/demo-users (private dataset)
37
+ HF_TOKEN = os.environ.get("HF_TOKEN", "").strip() # Space *secret* with write access
38
  GLOBAL_DAILY = int(os.environ.get("GLOBAL_DAILY", 200))
39
  ADMIN_USERS = {u.strip().lower() for u in os.environ.get("ADMIN_USERS", "").split(",") if u.strip()}
40
  FLOW = os.environ.get("FLOW", "oneshot").strip().lower() # "oneshot" (default) or "twostep"
 
295
  f"ffprobe: {short(out.stderr, 300)}")
296
 
297
 
298
+ def normalize_video(src, workdir, start=0.0, length=None):
299
  """Re-encode any upload (HEVC/ProRes .mov etc.) to H.264 + AAC MP4,
300
+ which browsers can preview and the API can read. Keeps the audio track.
301
+ With start/length, cuts that part of the clip at the same time."""
302
  out = os.path.join(workdir, "input_h264.mp4")
303
+ cut = (["-ss", f"{start:.2f}"] if start > 0 else [])
304
+ limit = (["-t", f"{length:.2f}"] if length else [])
305
  result = subprocess.run(
306
+ ["ffmpeg", "-y", *cut, "-i", src, *limit,
307
  "-map", "0:v:0", "-map", "0:a:0?",
308
  "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
309
  "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
 
354
  return out
355
 
356
 
357
+ def clock(seconds):
358
+ seconds = int(round(seconds))
359
+ return f"{seconds // 60}:{seconds % 60:02d}"
360
+
361
+
362
+ def trim_note(start, total):
363
+ end = min(start + MAX_SECONDS, total)
364
+ return (f'<div class="dr-trim-note">Your clip is <b>{clock(total)}</b> long. The demo uses '
365
+ f'{int(MAX_SECONDS)} seconds: <b>{clock(start)} to {clock(end)}</b>. '
366
+ f'Drag the slider to choose a different part.</div>')
367
+
368
+
369
+ NO_TRIM = (gr.update(visible=False), gr.update(value=0), "")
370
+
371
+
372
  def prepare_upload(video):
373
+ """Runs right after upload so the preview plays and the API gets a clean file.
374
+ Clips longer than MAX_SECONDS get cut to their first MAX_SECONDS, and the trim
375
+ panel appears so the person can pick a different part.
376
+ Returns (video_in, original_path_state, trim_panel, trim_slider, trim_note)."""
377
  if not video:
378
+ return None, None, *NO_TRIM
379
  try:
380
  before = probe(video)
381
  seconds = video_seconds(video)
 
 
 
 
382
  log.info(f"[upload] original: {before}")
383
+ if seconds > MAX_UPLOAD_SECONDS:
384
+ raise DemoError("TOO_LONG", f"This video is over {int(MAX_UPLOAD_SECONDS // 60)} minutes. "
385
+ "Upload a shorter clip; the demo uses up to "
386
+ f"{int(MAX_SECONDS)} seconds of it.")
387
+ if seconds <= MAX_SECONDS + 0.5:
388
+ out = normalize_video(video, tempfile.mkdtemp())
389
+ log.info(f"[upload] converted: {probe(out)}")
390
+ return out, None, *NO_TRIM
391
+ out = normalize_video(video, tempfile.mkdtemp(), 0.0, MAX_SECONDS)
392
+ latest = max(0.0, seconds - MAX_SECONDS)
393
+ log.info(f"[upload] long clip ({seconds:.1f}s), trimmed to 0-{MAX_SECONDS:g}s: {probe(out)}")
394
+ return (out, video,
395
+ gr.update(visible=True),
396
+ gr.update(minimum=0, maximum=round(latest, 1), value=0, step=0.5),
397
+ trim_note(0, seconds))
398
  except DemoError as e:
399
  raise gr.Error(e.user_message, print_exception=False)
400
  except Exception:
 
402
  raise gr.Error("Couldn't process this upload. Try another file.", print_exception=False)
403
 
404
 
405
+ def retrim(original, start):
406
+ """Cuts a new MAX_SECONDS window from the original upload when the slider moves."""
407
+ if not original or not os.path.exists(original):
408
+ raise gr.Error("Upload the video again to choose a different part.", print_exception=False)
409
+ try:
410
+ total = video_seconds(original)
411
+ start = max(0.0, min(float(start or 0), total - MAX_SECONDS))
412
+ out = normalize_video(original, tempfile.mkdtemp(), start, MAX_SECONDS)
413
+ log.info(f"[trim] {start:.1f}-{start + MAX_SECONDS:.1f}s of {total:.1f}s")
414
+ return out, trim_note(start, total)
415
+ except DemoError as e:
416
+ raise gr.Error(e.user_message, print_exception=False)
417
+ except Exception:
418
+ log.error(f"[trim] unexpected error:\n{traceback.format_exc()}")
419
+ raise gr.Error("Couldn't cut this part of the video. Try another position.", print_exception=False)
420
+
421
+
422
+ # ---------- user log (who tried the demo) ----------
423
+ # One JSON line per event, pushed every 10 minutes to a private dataset repo.
424
+ # Off unless LEADS_DATASET and HF_TOKEN are set. Never blocks or breaks the demo.
425
+
426
+ _leads = None
427
+ _leads_file = None
428
+ _seen_today = set()
429
+
430
+ if LEADS_DATASET and HF_TOKEN:
431
+ try:
432
+ from huggingface_hub import CommitScheduler, create_repo
433
+ create_repo(LEADS_DATASET, repo_type="dataset", private=True, exist_ok=True, token=HF_TOKEN)
434
+ _leads_dir = os.path.join(tempfile.gettempdir(), "drama_users")
435
+ os.makedirs(_leads_dir, exist_ok=True)
436
+ _leads_file = os.path.join(_leads_dir, f"events-{uuid.uuid4().hex[:8]}.jsonl")
437
+ _leads = CommitScheduler(repo_id=LEADS_DATASET, repo_type="dataset", folder_path=_leads_dir,
438
+ path_in_repo="data", every=10, private=True, token=HF_TOKEN)
439
+ log.info(f"[users] logging sign-ins and runs to dataset {LEADS_DATASET}")
440
+ except Exception as e:
441
+ _leads = None
442
+ log.warning(f"[users] user log is off: {type(e).__name__}: {e}")
443
+
444
+
445
+ def track(event, profile, **extra):
446
+ if _leads is None or profile is None:
447
+ return
448
+ try:
449
+ orgs = profile.get("orgs") or []
450
+ row = {
451
+ "time": datetime.now(timezone.utc).isoformat(timespec="seconds"),
452
+ "event": event,
453
+ "username": profile.username,
454
+ "name": profile.name,
455
+ "email": profile.get("email"),
456
+ "email_verified": profile.get("email_verified"),
457
+ "profile_url": profile.profile,
458
+ "avatar": profile.picture,
459
+ "orgs": [o.get("preferred_username") or o.get("name") for o in orgs if isinstance(o, dict)],
460
+ "admin": is_admin(profile.username),
461
+ **extra,
462
+ }
463
+ with _leads.lock:
464
+ with open(_leads_file, "a") as f:
465
+ f.write(json.dumps(row, default=str) + "\n")
466
+ except Exception as e:
467
+ log.warning(f"[users] couldn't record {event}: {type(e).__name__}: {e}")
468
+
469
+
470
+ def on_visit(profile: gr.OAuthProfile | None):
471
+ """Records signed-in visitors once a day, including ones who never press the button."""
472
+ if profile is None:
473
+ return
474
+ key = (profile.username, date.today().isoformat())
475
+ if key not in _seen_today:
476
+ _seen_today.add(key)
477
+ track("visit", profile)
478
+
479
+
480
  # ---------- usage limits ----------
481
 
482
  _usage = defaultdict(int)
 
679
 
680
  # ---------- main handler ----------
681
 
682
+ def run(video, mood, excited, angry, updates, profile: gr.OAuthProfile | None):
683
  def stop(message):
684
  raise gr.Error(message, print_exception=False)
685
 
 
696
  ctx = Run(profile.username)
697
  ctx.log(f"new run: user={ctx.user} admin={is_admin(ctx.user)} drama={drama} flow={FLOW}")
698
  counted = False
699
+ clip = {"drama": drama, "marketing_opt_in": bool(updates)}
700
  try:
701
  take_quota(ctx.user)
702
  counted = True
 
704
  ctx.stage = "prepare"
705
  yield None, status_html("working", "Preparing your clip…", 1)
706
  if video_seconds(video) > MAX_SECONDS + 0.5:
707
+ raise DemoError("TOO_LONG", f"Please use a clip of {int(MAX_SECONDS)} seconds or less. "
708
+ "Upload it again and pick a part with the slider.")
709
  workdir = tempfile.mkdtemp()
710
  if not os.path.basename(video).startswith("input_h264"):
711
  video = normalize_video(video, workdir) # sample clips skip the upload step
712
  audio, audio_source = extract_audio(video, workdir)
713
  v, a = probe(video), probe(audio)
714
  ctx.facts["video"] = f"{v.get('video')} {v.get('duration_s')}s {v.get('size_kb')}KB"
715
+ clip["clip_seconds"] = v.get("duration_s")
716
  ctx.facts["audio"] = f"{a.get('audio')} {a.get('duration_s')}s ({audio_source})"
717
  ctx.log(f"video: {v}")
718
  ctx.log(f"audio: {a} ({audio_source})")
 
739
  ctx.stage = "watermark"
740
  result = watermark(raw, workdir, ctx)
741
  report(ctx, "SUCCESS")
742
+ track("run", profile, outcome="success", run_id=ctx.id, seconds=round(time.time() - ctx.started), **clip)
743
  yield result, status_html("done", f"Done in {ctx.elapsed()}", 5,
744
  settings_chips(drama))
745
 
 
748
  refund_quota(ctx.user)
749
  if e.category == "LIMIT":
750
  ctx.log(f"blocked by usage limit: {e.user_message}")
751
+ track("limit_hit", profile, run_id=ctx.id, **clip)
752
  else:
753
  report(ctx, "FAILED", e.category, e.detail)
754
+ track("run", profile, outcome="failed", error=e.category, run_id=ctx.id, **clip)
755
  yield None, status_html("error", e.user_message, note=f"Reference: {ctx.id}")
756
  stop(f"{e.user_message} (ref: {ctx.id})")
757
  except gr.Error:
 
761
  refund_quota(ctx.user)
762
  ctx.log(f"UNEXPECTED {type(e).__name__}: {e}\n{traceback.format_exc()}", logging.ERROR)
763
  report(ctx, "FAILED", "UNEXPECTED", f"{type(e).__name__}: {e}")
764
+ track("run", profile, outcome="failed", error="UNEXPECTED", run_id=ctx.id, **clip)
765
  yield None, status_html("error", "Something unexpected went wrong.", note=f"Reference: {ctx.id}")
766
  stop(f"Something unexpected went wrong. (ref: {ctx.id})")
767
 
 
996
  #dr-go { height: 50px; font-size: 16px !important; font-weight: 600 !important; border-radius: 10px !important;
997
  box-shadow: none !important; transition: background .15s ease; }
998
  .gradio-container .dr-fine { font-size: 12px !important; color: var(--dr-faint) !important; text-align: center !important; margin: 0 !important; }
999
+ .gradio-container .dr-fine a { color: var(--dr-faint) !important; text-decoration: underline !important; padding: 0 !important; margin: 0 !important; display: inline !important; }
1000
+ .gradio-container .dr-trim { border: 1px solid var(--dr-line) !important; border-radius: 10px !important; padding: 12px 14px !important; gap: 6px !important; }
1001
+ .gradio-container .dr-trim-note { font-size: 13px !important; color: var(--dr-muted) !important; line-height: 1.5 !important; }
1002
+ .gradio-container .dr-trim-note b { color: var(--dr-text) !important; font-weight: 600 !important; }
1003
+ .gradio-container #dr-optin { margin: 2px 0 !important; }
1004
+ .gradio-container #dr-trim-start .tab-like-container { display: none !important; }
1005
+ .gradio-container #dr-optin label span { font-size: 13px !important; color: var(--dr-muted) !important; }
1006
 
1007
  /* status */
1008
  .gradio-container .dr-status { border-radius: 10px; padding: 14px 16px; border: 1px solid var(--dr-line); background: #0e0e11; }
 
1143
  with gr.Row(elem_id="dr-main", equal_height=True):
1144
  with gr.Column(scale=1, elem_classes="dr-card"):
1145
  gr.HTML('<div class="dr-label"><b>01</b>Upload a clip</div>'
1146
+ '<div class="dr-hint">One person, face clearly visible, front-on. .mp4 or .mov. '
1147
+ f'Longer than {int(MAX_SECONDS)} seconds? You can pick which part to use.</div>')
1148
  video_in = gr.Video(show_label=False, sources=["upload"], height=320, elem_classes="dr-media")
1149
+ original = gr.State(None)
1150
+ with gr.Column(visible=False, elem_classes="dr-trim") as trim_box:
1151
+ trim_info = gr.HTML()
1152
+ trim_start = gr.Slider(0, 1, value=0, step=0.5, label="Choose the part to use",
1153
+ buttons=[], elem_id="dr-trim-start")
1154
  samples = [[p] for p in sorted(glob.glob("examples/*.mp4"))]
1155
  if samples:
1156
  gr.Examples(samples, inputs=[video_in], label="No clip handy? Try one of these")
 
1164
  info="−1 very sad · 0 neutral · +1 big smile")
1165
  excited = gr.Slider(0, 1, value=d_exc, step=0.05, label="Excitement")
1166
  angry = gr.Slider(0, 1, value=d_ang, step=0.05, label="Anger")
1167
+ updates = gr.Checkbox(label="Email me DRAMA product updates and offers", value=False,
1168
+ container=False, elem_id="dr-optin")
1169
  go = gr.Button("Change expression", variant="primary", elem_id="dr-go")
1170
  gr.HTML('<div class="dr-fine">Only upload footage you have the right to use. '
1171
+ 'Demo results are watermarked. We keep a record of who uses the demo; see our '
1172
+ '<a href="https://trydrama.io/privacypolicy/" target="_blank">Privacy Policy</a>.</div>')
1173
 
1174
  with gr.Column(scale=1, elem_classes="dr-card"):
1175
  gr.HTML('<div class="dr-label"><b>03</b>Your result</div>'
 
1230
  for slider in (mood, excited, angry):
1231
  slider.release(lambda: "Custom", outputs=[preset],
1232
  api_visibility="private", show_progress="hidden")
1233
+ video_in.upload(prepare_upload, inputs=[video_in],
1234
+ outputs=[video_in, original, trim_box, trim_start, trim_info], api_visibility="private")
1235
+ trim_start.release(retrim, inputs=[original, trim_start], outputs=[video_in, trim_info],
1236
+ api_visibility="private")
1237
+ video_in.clear(lambda: (None, IDLE, None, gr.update(visible=False)),
1238
+ outputs=[video_out, status_out, original, trim_box], api_visibility="private")
1239
+ demo.load(on_visit, inputs=None, outputs=None, api_visibility="private", show_progress="hidden")
1240
  go.click(
1241
  run,
1242
+ inputs=[video_in, mood, excited, angry, updates],
1243
  outputs=[video_out, status_out],
1244
  concurrency_limit=2,
1245
  api_visibility="private",