Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -867,6 +867,36 @@ def _fit_keyframe(image_path, current_canvas):
|
|
| 867 |
return gr.update(value=image_path), gr.update(value=label)
|
| 868 |
|
| 869 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 870 |
load_models()
|
| 871 |
|
| 872 |
INTRO = """# PlagueKind · MiniMax-H3
|
|
@@ -885,6 +915,16 @@ CSS = """
|
|
| 885 |
.main.fillable {max-width: 1250px !important}
|
| 886 |
.dark .gradio-container { color: var(--body-text-color); }
|
| 887 |
.status p {font-size: 0.8rem; opacity: 0.65; text-align: center;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
"""
|
| 889 |
|
| 890 |
with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
|
|
@@ -979,7 +1019,12 @@ with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
|
|
| 979 |
)
|
| 980 |
|
| 981 |
with gr.Column():
|
| 982 |
-
video = gr.Video(label="Video + soundtrack")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
report = gr.Markdown()
|
| 984 |
with gr.Accordion("Distilled / Turbo LoRAs", open=False):
|
| 985 |
lora_1_strength = gr.Slider(
|
|
@@ -1060,6 +1105,18 @@ with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
|
|
| 1060 |
first_frame.upload(_fit_keyframe, [first_frame, canvas], [first_frame, canvas])
|
| 1061 |
last_frame.upload(_fit_keyframe, [last_frame, canvas], [last_frame, canvas])
|
| 1062 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
controls = [
|
| 1064 |
prompt,
|
| 1065 |
canvas,
|
|
|
|
| 867 |
return gr.update(value=image_path), gr.update(value=label)
|
| 868 |
|
| 869 |
|
| 870 |
+
# Client-side only (`fn=None`, no server round-trip): reads the real `<video>` element's current playback
|
| 871 |
+
# position, not a property of the file — so "grab this frame" means whatever's on screen when the button is
|
| 872 |
+
# pressed, paused or scrubbed to, not automatically the clip's last frame.
|
| 873 |
+
_FRAME_GRAB_JS = """
|
| 874 |
+
function() {
|
| 875 |
+
const video = document.querySelector('#h3-generated-video video');
|
| 876 |
+
return video ? video.currentTime : 0;
|
| 877 |
+
}
|
| 878 |
+
"""
|
| 879 |
+
|
| 880 |
+
|
| 881 |
+
def _extract_frame(video_path, timestamp):
|
| 882 |
+
"""The frame at `timestamp` seconds into `video_path`, as a numpy RGB array — Gradio converts it to a PIL
|
| 883 |
+
image for whichever `gr.Image` this is wired to. Runs on CPU; no GPU time, no interaction with `_generate`."""
|
| 884 |
+
if not video_path:
|
| 885 |
+
return None
|
| 886 |
+
import cv2
|
| 887 |
+
|
| 888 |
+
cap = cv2.VideoCapture(video_path)
|
| 889 |
+
if not cap.isOpened():
|
| 890 |
+
return None
|
| 891 |
+
fps = cap.get(cv2.CAP_PROP_FPS) or FPS
|
| 892 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 893 |
+
target_frame = min(int(float(timestamp) * fps), max(0, total_frames - 1))
|
| 894 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
|
| 895 |
+
ok, frame = cap.read()
|
| 896 |
+
cap.release()
|
| 897 |
+
return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if ok else None
|
| 898 |
+
|
| 899 |
+
|
| 900 |
load_models()
|
| 901 |
|
| 902 |
INTRO = """# PlagueKind · MiniMax-H3
|
|
|
|
| 915 |
.main.fillable {max-width: 1250px !important}
|
| 916 |
.dark .gradio-container { color: var(--body-text-color); }
|
| 917 |
.status p {font-size: 0.8rem; opacity: 0.65; text-align: center;}
|
| 918 |
+
.h3-hidden-timestamp {
|
| 919 |
+
opacity: 0;
|
| 920 |
+
height: 0px;
|
| 921 |
+
width: 0px;
|
| 922 |
+
margin: 0px;
|
| 923 |
+
padding: 0px;
|
| 924 |
+
overflow: hidden;
|
| 925 |
+
position: absolute;
|
| 926 |
+
pointer-events: none;
|
| 927 |
+
}
|
| 928 |
"""
|
| 929 |
|
| 930 |
with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
|
|
|
|
| 1019 |
)
|
| 1020 |
|
| 1021 |
with gr.Column():
|
| 1022 |
+
video = gr.Video(label="Video + soundtrack", elem_id="h3-generated-video")
|
| 1023 |
+
with gr.Row():
|
| 1024 |
+
grab_first_btn = gr.Button("📸 Use current frame as First frame", size="sm", variant="secondary")
|
| 1025 |
+
grab_last_btn = gr.Button("📸 Use current frame as Last frame", size="sm", variant="secondary")
|
| 1026 |
+
first_frame_timestamp = gr.Number(value=0, visible=True, elem_classes="h3-hidden-timestamp")
|
| 1027 |
+
last_frame_timestamp = gr.Number(value=0, visible=True, elem_classes="h3-hidden-timestamp")
|
| 1028 |
report = gr.Markdown()
|
| 1029 |
with gr.Accordion("Distilled / Turbo LoRAs", open=False):
|
| 1030 |
lora_1_strength = gr.Slider(
|
|
|
|
| 1105 |
first_frame.upload(_fit_keyframe, [first_frame, canvas], [first_frame, canvas])
|
| 1106 |
last_frame.upload(_fit_keyframe, [last_frame, canvas], [last_frame, canvas])
|
| 1107 |
|
| 1108 |
+
# Grabbing the currently-displayed frame: the button's own click runs only the JS above (`fn=None`, no
|
| 1109 |
+
# server round-trip) to read the real `<video>` element's playback position into a hidden number box; that
|
| 1110 |
+
# box's `.change()` is what actually decodes and writes the frame, server-side.
|
| 1111 |
+
grab_first_btn.click(fn=None, inputs=None, outputs=[first_frame_timestamp], js=_FRAME_GRAB_JS)
|
| 1112 |
+
first_frame_timestamp.change(
|
| 1113 |
+
_extract_frame, [video, first_frame_timestamp], first_frame, show_progress="hidden"
|
| 1114 |
+
)
|
| 1115 |
+
grab_last_btn.click(fn=None, inputs=None, outputs=[last_frame_timestamp], js=_FRAME_GRAB_JS)
|
| 1116 |
+
last_frame_timestamp.change(
|
| 1117 |
+
_extract_frame, [video, last_frame_timestamp], last_frame, show_progress="hidden"
|
| 1118 |
+
)
|
| 1119 |
+
|
| 1120 |
controls = [
|
| 1121 |
prompt,
|
| 1122 |
canvas,
|