Update app.py
Browse files
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",
|
| 34 |
-
PER_USER_DAILY = int(os.environ.get("PER_USER_DAILY",
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
| 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.
|
|
|
|
| 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],
|
| 1103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|