Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -570,19 +570,19 @@ class RadioAnimated(gr.HTML):
|
|
| 570 |
|
| 571 |
class PromptBox(gr.HTML):
|
| 572 |
"""
|
| 573 |
-
|
| 574 |
-
Outputs: the current text value (string)
|
| 575 |
"""
|
| 576 |
-
def __init__(self, value="", placeholder="Describe
|
| 577 |
uid = uuid.uuid4().hex[:8]
|
| 578 |
|
| 579 |
html_template = f"""
|
| 580 |
-
<div
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
|
|
|
| 586 |
</div>
|
| 587 |
"""
|
| 588 |
|
|
@@ -590,50 +590,65 @@ class PromptBox(gr.HTML):
|
|
| 590 |
(() => {
|
| 591 |
const textarea = element.querySelector(".ds-textarea");
|
| 592 |
if (!textarea) return;
|
| 593 |
-
|
| 594 |
-
// Auto-resize (optional, but nice)
|
| 595 |
const autosize = () => {
|
| 596 |
textarea.style.height = "0px";
|
| 597 |
textarea.style.height = Math.min(textarea.scrollHeight, 240) + "px";
|
| 598 |
};
|
| 599 |
-
|
| 600 |
-
// Set initial value from props.value
|
| 601 |
const setValue = (v, triggerChange=false) => {
|
| 602 |
const val = (v ?? "");
|
| 603 |
if (textarea.value !== val) textarea.value = val;
|
| 604 |
autosize();
|
| 605 |
-
|
| 606 |
props.value = textarea.value;
|
| 607 |
if (triggerChange) trigger("change", props.value);
|
| 608 |
};
|
| 609 |
-
|
| 610 |
setValue(props.value, false);
|
| 611 |
-
|
| 612 |
-
// Update Gradio value on input
|
| 613 |
textarea.addEventListener("input", () => {
|
| 614 |
autosize();
|
| 615 |
props.value = textarea.value;
|
| 616 |
trigger("change", props.value);
|
| 617 |
});
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
let last = props.value;
|
| 620 |
const syncFromProps = () => {
|
| 621 |
if (props.value !== last) {
|
| 622 |
last = props.value;
|
| 623 |
-
setValue(last, false);
|
| 624 |
}
|
| 625 |
requestAnimationFrame(syncFromProps);
|
| 626 |
};
|
| 627 |
requestAnimationFrame(syncFromProps);
|
| 628 |
})();
|
|
|
|
| 629 |
"""
|
| 630 |
|
| 631 |
-
super().__init__(
|
| 632 |
-
|
| 633 |
-
html_template=html_template,
|
| 634 |
-
js_on_load=js_on_load,
|
| 635 |
-
**kwargs
|
| 636 |
-
)
|
| 637 |
|
| 638 |
class CameraDropdown(gr.HTML):
|
| 639 |
"""
|
|
@@ -1010,7 +1025,7 @@ css = """
|
|
| 1010 |
|
| 1011 |
/* Make the row behave nicely */
|
| 1012 |
#controls-row {
|
| 1013 |
-
display:
|
| 1014 |
align-items: center;
|
| 1015 |
gap: 12px;
|
| 1016 |
flex-wrap: nowrap; /* or wrap if you prefer on small screens */
|
|
@@ -1023,14 +1038,6 @@ css = """
|
|
| 1023 |
min-width: 0 !important;
|
| 1024 |
}
|
| 1025 |
|
| 1026 |
-
|
| 1027 |
-
/* Same idea for your radio HTML blocks (optional but helps) */
|
| 1028 |
-
#radioanimated_duration,
|
| 1029 |
-
#radioanimated_duration > div,
|
| 1030 |
-
#radioanimated_resolution,
|
| 1031 |
-
#radioanimated_resolution > div {
|
| 1032 |
-
width: fit-content !important;
|
| 1033 |
-
}
|
| 1034 |
|
| 1035 |
#col-container {
|
| 1036 |
margin: 0 auto;
|
|
@@ -1210,35 +1217,88 @@ css += """
|
|
| 1210 |
.ds-textarea{
|
| 1211 |
width: 100%;
|
| 1212 |
box-sizing: border-box;
|
| 1213 |
-
|
| 1214 |
background: #2b2b2b;
|
| 1215 |
color: rgba(255,255,255,0.9);
|
| 1216 |
-
|
| 1217 |
border: 1px solid rgba(255,255,255,0.12);
|
| 1218 |
border-radius: 14px;
|
| 1219 |
-
|
| 1220 |
padding: 14px 16px;
|
| 1221 |
outline: none;
|
| 1222 |
-
|
| 1223 |
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
|
| 1224 |
font-size: 15px;
|
| 1225 |
line-height: 1.35;
|
| 1226 |
-
|
| 1227 |
resize: none;
|
| 1228 |
-
height:
|
| 1229 |
-
|
| 1230 |
-
max-height: 94px;
|
| 1231 |
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1232 |
}
|
| 1233 |
|
| 1234 |
-
|
| 1235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1236 |
}
|
| 1237 |
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1241 |
}
|
|
|
|
| 1242 |
"""
|
| 1243 |
|
| 1244 |
css += """
|
|
@@ -1418,6 +1478,172 @@ css += """
|
|
| 1418 |
.cd-label{
|
| 1419 |
flex: 1;
|
| 1420 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1421 |
"""
|
| 1422 |
|
| 1423 |
|
|
@@ -1454,11 +1680,45 @@ with gr.Blocks(title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 1454 |
with gr.Column(elem_id="step-column"):
|
| 1455 |
|
| 1456 |
input_image = gr.Image(
|
| 1457 |
-
label="
|
| 1458 |
-
type="filepath",
|
| 1459 |
-
height=
|
| 1460 |
)
|
| 1461 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1462 |
prompt_ui = PromptBox(
|
| 1463 |
value="Make this image come alive with cinematic motion, smooth animation",
|
| 1464 |
elem_id="prompt_ui",
|
|
@@ -1498,11 +1758,11 @@ with gr.Blocks(title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 1498 |
|
| 1499 |
with gr.Row(elem_id="controls-row"):
|
| 1500 |
|
| 1501 |
-
|
| 1502 |
choices=["3s", "5s", "10s", "15s"],
|
| 1503 |
value="5s",
|
| 1504 |
title="Clip Duration",
|
| 1505 |
-
elem_id="
|
| 1506 |
)
|
| 1507 |
|
| 1508 |
duration = gr.Slider(
|
|
@@ -1527,7 +1787,7 @@ with gr.Blocks(title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 1527 |
</svg>"""
|
| 1528 |
|
| 1529 |
|
| 1530 |
-
|
| 1531 |
choices=[
|
| 1532 |
{"label": "16:9", "value": "16:9", "icon": ICON_16_9},
|
| 1533 |
{"label": "1:1", "value": "1:1", "icon": ICON_1_1},
|
|
@@ -1535,18 +1795,18 @@ with gr.Blocks(title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 1535 |
],
|
| 1536 |
value="16:9",
|
| 1537 |
title="Resolution",
|
| 1538 |
-
elem_id="
|
| 1539 |
)
|
| 1540 |
|
| 1541 |
|
| 1542 |
width = gr.Number(label="Width", value=DEFAULT_1_STAGE_WIDTH, precision=0, visible=False)
|
| 1543 |
height = gr.Number(label="Height", value=DEFAULT_1_STAGE_HEIGHT, precision=0, visible=False)
|
| 1544 |
|
| 1545 |
-
|
| 1546 |
choices=[name for name, _ in RUNTIME_LORA_CHOICES],
|
| 1547 |
value="No LoRA",
|
| 1548 |
title="Camera LoRA",
|
| 1549 |
-
elem_id="
|
| 1550 |
)
|
| 1551 |
|
| 1552 |
# Hidden real dropdown (backend value)
|
|
@@ -1559,22 +1819,22 @@ with gr.Blocks(title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 1559 |
|
| 1560 |
generate_btn = gr.Button("🤩 Generate Video", variant="primary", elem_classes="button-gradient")
|
| 1561 |
|
| 1562 |
-
|
| 1563 |
fn=lambda x: x,
|
| 1564 |
-
inputs=
|
| 1565 |
outputs=camera_lora,
|
| 1566 |
api_visibility="private"
|
| 1567 |
)
|
| 1568 |
|
| 1569 |
-
|
| 1570 |
fn=apply_duration,
|
| 1571 |
-
inputs=
|
| 1572 |
outputs=[duration],
|
| 1573 |
api_visibility="private"
|
| 1574 |
)
|
| 1575 |
-
|
| 1576 |
fn=apply_resolution,
|
| 1577 |
-
inputs=
|
| 1578 |
outputs=[width, height],
|
| 1579 |
api_visibility="private"
|
| 1580 |
)
|
|
@@ -1645,7 +1905,7 @@ with gr.Blocks(title="LTX-2 Video Distilled 🎥🔈") as demo:
|
|
| 1645 |
|
| 1646 |
],
|
| 1647 |
fn=generate_video_example,
|
| 1648 |
-
inputs=[input_image, prompt_ui,
|
| 1649 |
outputs = [output_video],
|
| 1650 |
label="I2V Examples",
|
| 1651 |
cache_examples=True,
|
|
|
|
| 570 |
|
| 571 |
class PromptBox(gr.HTML):
|
| 572 |
"""
|
| 573 |
+
Prompt textarea with an internal footer slot (.ds-footer) where we can inject dropdowns.
|
|
|
|
| 574 |
"""
|
| 575 |
+
def __init__(self, value="", placeholder="Describe what you want...", **kwargs):
|
| 576 |
uid = uuid.uuid4().hex[:8]
|
| 577 |
|
| 578 |
html_template = f"""
|
| 579 |
+
<div class="ds-card" data-ds="{uid}">
|
| 580 |
+
<div class="ds-top">
|
| 581 |
+
<textarea class="ds-textarea" rows="3" placeholder="{placeholder}"></textarea>
|
| 582 |
+
|
| 583 |
+
<!-- footer slot -->
|
| 584 |
+
<div class="ds-footer" aria-label="prompt-footer"></div>
|
| 585 |
+
</div>
|
| 586 |
</div>
|
| 587 |
"""
|
| 588 |
|
|
|
|
| 590 |
(() => {
|
| 591 |
const textarea = element.querySelector(".ds-textarea");
|
| 592 |
if (!textarea) return;
|
| 593 |
+
|
|
|
|
| 594 |
const autosize = () => {
|
| 595 |
textarea.style.height = "0px";
|
| 596 |
textarea.style.height = Math.min(textarea.scrollHeight, 240) + "px";
|
| 597 |
};
|
| 598 |
+
|
|
|
|
| 599 |
const setValue = (v, triggerChange=false) => {
|
| 600 |
const val = (v ?? "");
|
| 601 |
if (textarea.value !== val) textarea.value = val;
|
| 602 |
autosize();
|
|
|
|
| 603 |
props.value = textarea.value;
|
| 604 |
if (triggerChange) trigger("change", props.value);
|
| 605 |
};
|
| 606 |
+
|
| 607 |
setValue(props.value, false);
|
| 608 |
+
|
|
|
|
| 609 |
textarea.addEventListener("input", () => {
|
| 610 |
autosize();
|
| 611 |
props.value = textarea.value;
|
| 612 |
trigger("change", props.value);
|
| 613 |
});
|
| 614 |
+
|
| 615 |
+
// ✅ Focus-on-load (robust)
|
| 616 |
+
const shouldAutoFocus = () => {
|
| 617 |
+
// don’t steal focus if user already clicked/typed somewhere
|
| 618 |
+
const ae = document.activeElement;
|
| 619 |
+
if (ae && ae !== document.body && ae !== document.documentElement) return false;
|
| 620 |
+
// don’t auto-focus on small screens (optional; avoids mobile keyboard pop)
|
| 621 |
+
if (window.matchMedia && window.matchMedia("(max-width: 768px)").matches) return false;
|
| 622 |
+
return true;
|
| 623 |
+
};
|
| 624 |
+
|
| 625 |
+
const focusWithRetry = (tries = 30) => {
|
| 626 |
+
if (!shouldAutoFocus()) return;
|
| 627 |
+
// only focus if still not focused
|
| 628 |
+
if (document.activeElement !== textarea) textarea.focus({ preventScroll: true });
|
| 629 |
+
if (document.activeElement === textarea) return;
|
| 630 |
+
if (tries > 0) requestAnimationFrame(() => focusWithRetry(tries - 1));
|
| 631 |
+
};
|
| 632 |
+
|
| 633 |
+
// wait a tick so Gradio/layout settles
|
| 634 |
+
requestAnimationFrame(() => focusWithRetry());
|
| 635 |
+
|
| 636 |
+
// keep your sync loop
|
| 637 |
let last = props.value;
|
| 638 |
const syncFromProps = () => {
|
| 639 |
if (props.value !== last) {
|
| 640 |
last = props.value;
|
| 641 |
+
setValue(last, false);
|
| 642 |
}
|
| 643 |
requestAnimationFrame(syncFromProps);
|
| 644 |
};
|
| 645 |
requestAnimationFrame(syncFromProps);
|
| 646 |
})();
|
| 647 |
+
|
| 648 |
"""
|
| 649 |
|
| 650 |
+
super().__init__(value=value, html_template=html_template, js_on_load=js_on_load, **kwargs)
|
| 651 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
|
| 653 |
class CameraDropdown(gr.HTML):
|
| 654 |
"""
|
|
|
|
| 1025 |
|
| 1026 |
/* Make the row behave nicely */
|
| 1027 |
#controls-row {
|
| 1028 |
+
display: none !important;
|
| 1029 |
align-items: center;
|
| 1030 |
gap: 12px;
|
| 1031 |
flex-wrap: nowrap; /* or wrap if you prefer on small screens */
|
|
|
|
| 1038 |
min-width: 0 !important;
|
| 1039 |
}
|
| 1040 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1041 |
|
| 1042 |
#col-container {
|
| 1043 |
margin: 0 auto;
|
|
|
|
| 1217 |
.ds-textarea{
|
| 1218 |
width: 100%;
|
| 1219 |
box-sizing: border-box;
|
|
|
|
| 1220 |
background: #2b2b2b;
|
| 1221 |
color: rgba(255,255,255,0.9);
|
|
|
|
| 1222 |
border: 1px solid rgba(255,255,255,0.12);
|
| 1223 |
border-radius: 14px;
|
|
|
|
| 1224 |
padding: 14px 16px;
|
| 1225 |
outline: none;
|
|
|
|
| 1226 |
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
|
| 1227 |
font-size: 15px;
|
| 1228 |
line-height: 1.35;
|
|
|
|
| 1229 |
resize: none;
|
| 1230 |
+
min-height: 210px;
|
| 1231 |
+
max-height: 210px;
|
|
|
|
| 1232 |
overflow-y: auto;
|
| 1233 |
+
|
| 1234 |
+
/* IMPORTANT: space for the footer controls */
|
| 1235 |
+
padding-bottom: 72px;
|
| 1236 |
+
}
|
| 1237 |
+
|
| 1238 |
+
|
| 1239 |
+
.ds-card{
|
| 1240 |
+
width: 100%;
|
| 1241 |
+
max-width: 720px;
|
| 1242 |
+
margin: 0 auto;
|
| 1243 |
+
}
|
| 1244 |
+
.ds-top{
|
| 1245 |
+
position: relative;
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
/* Make room for footer inside textarea */
|
| 1249 |
+
.ds-textarea{
|
| 1250 |
+
padding-bottom: 72px;
|
| 1251 |
}
|
| 1252 |
|
| 1253 |
+
/* Footer positioning */
|
| 1254 |
+
.ds-footer{
|
| 1255 |
+
position: absolute;
|
| 1256 |
+
right: 12px;
|
| 1257 |
+
bottom: 10px;
|
| 1258 |
+
display: flex;
|
| 1259 |
+
gap: 8px;
|
| 1260 |
+
align-items: center;
|
| 1261 |
+
justify-content: flex-end;
|
| 1262 |
+
z-index: 3;
|
| 1263 |
}
|
| 1264 |
|
| 1265 |
+
/* Smaller pill buttons inside footer */
|
| 1266 |
+
.ds-footer .cd-trigger{
|
| 1267 |
+
min-height: 32px;
|
| 1268 |
+
padding: 6px 10px;
|
| 1269 |
+
font-size: 12px;
|
| 1270 |
+
gap: 6px;
|
| 1271 |
+
border-radius: 9999px;
|
| 1272 |
+
}
|
| 1273 |
+
.ds-footer .cd-trigger-icon,
|
| 1274 |
+
.ds-footer .cd-icn{
|
| 1275 |
+
width: 14px;
|
| 1276 |
+
height: 14px;
|
| 1277 |
+
}
|
| 1278 |
+
.ds-footer .cd-trigger-icon svg,
|
| 1279 |
+
.ds-footer .cd-icn svg{
|
| 1280 |
+
width: 14px;
|
| 1281 |
+
height: 14px;
|
| 1282 |
+
}
|
| 1283 |
+
.ds-footer .cd-caret{
|
| 1284 |
+
font-size: 11px;
|
| 1285 |
+
}
|
| 1286 |
+
|
| 1287 |
+
/* Bottom safe area bar (optional but looks nicer) */
|
| 1288 |
+
.ds-top::after{
|
| 1289 |
+
content: "";
|
| 1290 |
+
position: absolute;
|
| 1291 |
+
left: 1px;
|
| 1292 |
+
right: 1px;
|
| 1293 |
+
bottom: 1px;
|
| 1294 |
+
height: 56px;
|
| 1295 |
+
background: #2b2b2b;
|
| 1296 |
+
border-bottom-left-radius: 13px;
|
| 1297 |
+
border-bottom-right-radius: 13px;
|
| 1298 |
+
pointer-events: none;
|
| 1299 |
+
z-index: 2;
|
| 1300 |
}
|
| 1301 |
+
|
| 1302 |
"""
|
| 1303 |
|
| 1304 |
css += """
|
|
|
|
| 1478 |
.cd-label{
|
| 1479 |
flex: 1;
|
| 1480 |
}
|
| 1481 |
+
|
| 1482 |
+
/* =========================
|
| 1483 |
+
FIX: prompt border + scrollbar bleed
|
| 1484 |
+
========================= */
|
| 1485 |
+
|
| 1486 |
+
/* Put the border + background on the wrapper, not the textarea */
|
| 1487 |
+
.ds-top{
|
| 1488 |
+
position: relative;
|
| 1489 |
+
background: #2b2b2b;
|
| 1490 |
+
border: 1px solid rgba(255,255,255,0.12);
|
| 1491 |
+
border-radius: 14px;
|
| 1492 |
+
overflow: hidden; /* ensures the footer bar is clipped to rounded corners */
|
| 1493 |
+
}
|
| 1494 |
+
|
| 1495 |
+
/* Make textarea "transparent" so wrapper owns the border/background */
|
| 1496 |
+
.ds-textarea{
|
| 1497 |
+
background: transparent !important;
|
| 1498 |
+
border: none !important;
|
| 1499 |
+
border-radius: 0 !important; /* wrapper handles radius */
|
| 1500 |
+
outline: none;
|
| 1501 |
+
|
| 1502 |
+
/* keep your spacing */
|
| 1503 |
+
padding: 14px 16px;
|
| 1504 |
+
padding-bottom: 72px; /* room for footer */
|
| 1505 |
+
width: 100%;
|
| 1506 |
+
box-sizing: border-box;
|
| 1507 |
+
|
| 1508 |
+
/* keep scroll behavior */
|
| 1509 |
+
overflow-y: auto;
|
| 1510 |
+
|
| 1511 |
+
/* prevent scrollbar bleed by hiding native scrollbar */
|
| 1512 |
+
scrollbar-width: none; /* Firefox */
|
| 1513 |
+
}
|
| 1514 |
+
.ds-textarea::-webkit-scrollbar{ /* Chrome/Safari */
|
| 1515 |
+
width: 0;
|
| 1516 |
+
height: 0;
|
| 1517 |
+
}
|
| 1518 |
+
|
| 1519 |
+
/* Safe-area bar: now it matches perfectly because it's inside the same bordered wrapper */
|
| 1520 |
+
.ds-top::after{
|
| 1521 |
+
content: "";
|
| 1522 |
+
position: absolute;
|
| 1523 |
+
left: 0;
|
| 1524 |
+
right: 0;
|
| 1525 |
+
bottom: 0;
|
| 1526 |
+
height: 56px;
|
| 1527 |
+
background: #2b2b2b;
|
| 1528 |
+
pointer-events: none;
|
| 1529 |
+
z-index: 2;
|
| 1530 |
+
}
|
| 1531 |
+
|
| 1532 |
+
/* Footer above the bar */
|
| 1533 |
+
.ds-footer{
|
| 1534 |
+
position: absolute;
|
| 1535 |
+
right: 12px;
|
| 1536 |
+
bottom: 10px;
|
| 1537 |
+
display: flex;
|
| 1538 |
+
gap: 8px;
|
| 1539 |
+
align-items: center;
|
| 1540 |
+
justify-content: flex-end;
|
| 1541 |
+
z-index: 3;
|
| 1542 |
+
}
|
| 1543 |
+
|
| 1544 |
+
/* Ensure textarea content sits below overlays */
|
| 1545 |
+
.ds-textarea{
|
| 1546 |
+
position: relative;
|
| 1547 |
+
z-index: 1;
|
| 1548 |
+
}
|
| 1549 |
+
|
| 1550 |
+
/* ===== FIX dropdown menu being clipped/behind ===== */
|
| 1551 |
+
|
| 1552 |
+
/* Let the dropdown menu escape the prompt wrapper */
|
| 1553 |
+
.ds-top{
|
| 1554 |
+
overflow: visible !important; /* IMPORTANT: do not clip the menu */
|
| 1555 |
+
}
|
| 1556 |
+
|
| 1557 |
+
/* Keep the rounded "safe area" look without clipping the menu */
|
| 1558 |
+
.ds-top::after{
|
| 1559 |
+
left: 0 !important;
|
| 1560 |
+
right: 0 !important;
|
| 1561 |
+
bottom: 0 !important;
|
| 1562 |
+
border-bottom-left-radius: 14px !important;
|
| 1563 |
+
border-bottom-right-radius: 14px !important;
|
| 1564 |
+
}
|
| 1565 |
+
|
| 1566 |
+
/* Ensure the footer stays above the safe-area bar */
|
| 1567 |
+
.ds-footer{
|
| 1568 |
+
z-index: 20 !important;
|
| 1569 |
+
}
|
| 1570 |
+
|
| 1571 |
+
/* Make sure the opened menu is above EVERYTHING */
|
| 1572 |
+
.ds-footer .cd-menu{
|
| 1573 |
+
z-index: 999999 !important;
|
| 1574 |
+
}
|
| 1575 |
+
|
| 1576 |
+
/* Sometimes Gradio/columns/cards create stacking contexts;
|
| 1577 |
+
force the whole prompt card above nearby panels */
|
| 1578 |
+
.ds-card{
|
| 1579 |
+
position: relative;
|
| 1580 |
+
z-index: 50;
|
| 1581 |
+
}
|
| 1582 |
+
|
| 1583 |
+
/* --- Fix focus highlight shape (make it match rounded container) --- */
|
| 1584 |
+
|
| 1585 |
+
/* Kill any theme focus ring on the textarea itself */
|
| 1586 |
+
.ds-textarea:focus,
|
| 1587 |
+
.ds-textarea:focus-visible{
|
| 1588 |
+
outline: none !important;
|
| 1589 |
+
box-shadow: none !important;
|
| 1590 |
+
}
|
| 1591 |
+
|
| 1592 |
+
/* Optional: if some themes apply it even when not focused */
|
| 1593 |
+
.ds-textarea{
|
| 1594 |
+
outline: none !important;
|
| 1595 |
+
}
|
| 1596 |
+
|
| 1597 |
+
/* Apply the focus ring to the rounded wrapper instead */
|
| 1598 |
+
.ds-top:focus-within{
|
| 1599 |
+
border-color: rgba(255,255,255,0.22) !important;
|
| 1600 |
+
box-shadow: 0 0 0 3px rgba(255,255,255,0.06) !important;
|
| 1601 |
+
border-radius: 14px !important;
|
| 1602 |
+
}
|
| 1603 |
+
|
| 1604 |
+
/* If you see any tiny square corners, ensure the wrapper clips its own shadow properly */
|
| 1605 |
+
.ds-top{
|
| 1606 |
+
border-radius: 14px !important;
|
| 1607 |
+
}
|
| 1608 |
+
|
| 1609 |
+
/* =========================
|
| 1610 |
+
CameraDropdown: force readable menu text in BOTH themes
|
| 1611 |
+
========================= */
|
| 1612 |
+
|
| 1613 |
+
/* Menu surface */
|
| 1614 |
+
.cd-menu{
|
| 1615 |
+
background: #2b2b2b !important;
|
| 1616 |
+
border: 1px solid rgba(255,255,255,0.14) !important;
|
| 1617 |
+
}
|
| 1618 |
+
|
| 1619 |
+
/* Title */
|
| 1620 |
+
.cd-title{
|
| 1621 |
+
color: rgba(255,255,255,0.55) !important;
|
| 1622 |
+
}
|
| 1623 |
+
|
| 1624 |
+
/* Items + all descendants (fixes spans / inherited theme colors) */
|
| 1625 |
+
.cd-item,
|
| 1626 |
+
.cd-item *{
|
| 1627 |
+
color: rgba(255,255,255,0.92) !important;
|
| 1628 |
+
}
|
| 1629 |
+
|
| 1630 |
+
/* Hover state */
|
| 1631 |
+
.cd-item:hover{
|
| 1632 |
+
background: rgba(255,255,255,0.10) !important;
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
/* Checkmark */
|
| 1636 |
+
.cd-item::after{
|
| 1637 |
+
color: rgba(255,255,255,0.92) !important;
|
| 1638 |
+
}
|
| 1639 |
+
|
| 1640 |
+
/* (Optional) make sure the trigger stays readable too */
|
| 1641 |
+
.cd-trigger,
|
| 1642 |
+
.cd-trigger *{
|
| 1643 |
+
color: rgba(255,255,255,0.75) !important;
|
| 1644 |
+
}
|
| 1645 |
+
|
| 1646 |
+
|
| 1647 |
"""
|
| 1648 |
|
| 1649 |
|
|
|
|
| 1680 |
with gr.Column(elem_id="step-column"):
|
| 1681 |
|
| 1682 |
input_image = gr.Image(
|
| 1683 |
+
label="First Frame (Optional)",
|
| 1684 |
+
type="filepath",
|
| 1685 |
+
height=256
|
| 1686 |
)
|
| 1687 |
|
| 1688 |
+
relocate = gr.HTML(
|
| 1689 |
+
value="",
|
| 1690 |
+
html_template="<div></div>",
|
| 1691 |
+
js_on_load=r"""
|
| 1692 |
+
(() => {
|
| 1693 |
+
function moveIntoFooter() {
|
| 1694 |
+
const promptRoot = document.querySelector("#prompt_ui");
|
| 1695 |
+
if (!promptRoot) return false;
|
| 1696 |
+
|
| 1697 |
+
const footer = promptRoot.querySelector(".ds-footer");
|
| 1698 |
+
if (!footer) return false;
|
| 1699 |
+
|
| 1700 |
+
const dur = document.querySelector("#duration_ui .cd-wrap");
|
| 1701 |
+
const res = document.querySelector("#resolution_ui .cd-wrap");
|
| 1702 |
+
const cam = document.querySelector("#camera_ui .cd-wrap");
|
| 1703 |
+
|
| 1704 |
+
if (!dur || !res || !cam) return false;
|
| 1705 |
+
|
| 1706 |
+
footer.appendChild(dur);
|
| 1707 |
+
footer.appendChild(res);
|
| 1708 |
+
footer.appendChild(cam);
|
| 1709 |
+
|
| 1710 |
+
return true;
|
| 1711 |
+
}
|
| 1712 |
+
|
| 1713 |
+
const tick = () => {
|
| 1714 |
+
if (!moveIntoFooter()) requestAnimationFrame(tick);
|
| 1715 |
+
};
|
| 1716 |
+
requestAnimationFrame(tick);
|
| 1717 |
+
})();
|
| 1718 |
+
"""
|
| 1719 |
+
)
|
| 1720 |
+
|
| 1721 |
+
|
| 1722 |
prompt_ui = PromptBox(
|
| 1723 |
value="Make this image come alive with cinematic motion, smooth animation",
|
| 1724 |
elem_id="prompt_ui",
|
|
|
|
| 1758 |
|
| 1759 |
with gr.Row(elem_id="controls-row"):
|
| 1760 |
|
| 1761 |
+
duration_ui = CameraDropdown(
|
| 1762 |
choices=["3s", "5s", "10s", "15s"],
|
| 1763 |
value="5s",
|
| 1764 |
title="Clip Duration",
|
| 1765 |
+
elem_id="duration_ui"
|
| 1766 |
)
|
| 1767 |
|
| 1768 |
duration = gr.Slider(
|
|
|
|
| 1787 |
</svg>"""
|
| 1788 |
|
| 1789 |
|
| 1790 |
+
resolution_ui = CameraDropdown(
|
| 1791 |
choices=[
|
| 1792 |
{"label": "16:9", "value": "16:9", "icon": ICON_16_9},
|
| 1793 |
{"label": "1:1", "value": "1:1", "icon": ICON_1_1},
|
|
|
|
| 1795 |
],
|
| 1796 |
value="16:9",
|
| 1797 |
title="Resolution",
|
| 1798 |
+
elem_id="resolution_ui"
|
| 1799 |
)
|
| 1800 |
|
| 1801 |
|
| 1802 |
width = gr.Number(label="Width", value=DEFAULT_1_STAGE_WIDTH, precision=0, visible=False)
|
| 1803 |
height = gr.Number(label="Height", value=DEFAULT_1_STAGE_HEIGHT, precision=0, visible=False)
|
| 1804 |
|
| 1805 |
+
camera_ui = CameraDropdown(
|
| 1806 |
choices=[name for name, _ in RUNTIME_LORA_CHOICES],
|
| 1807 |
value="No LoRA",
|
| 1808 |
title="Camera LoRA",
|
| 1809 |
+
elem_id="camera_ui",
|
| 1810 |
)
|
| 1811 |
|
| 1812 |
# Hidden real dropdown (backend value)
|
|
|
|
| 1819 |
|
| 1820 |
generate_btn = gr.Button("🤩 Generate Video", variant="primary", elem_classes="button-gradient")
|
| 1821 |
|
| 1822 |
+
camera_ui.change(
|
| 1823 |
fn=lambda x: x,
|
| 1824 |
+
inputs=camera_ui,
|
| 1825 |
outputs=camera_lora,
|
| 1826 |
api_visibility="private"
|
| 1827 |
)
|
| 1828 |
|
| 1829 |
+
duration_ui.change(
|
| 1830 |
fn=apply_duration,
|
| 1831 |
+
inputs=duration_ui,
|
| 1832 |
outputs=[duration],
|
| 1833 |
api_visibility="private"
|
| 1834 |
)
|
| 1835 |
+
resolution_ui.change(
|
| 1836 |
fn=apply_resolution,
|
| 1837 |
+
inputs=resolution_ui,
|
| 1838 |
outputs=[width, height],
|
| 1839 |
api_visibility="private"
|
| 1840 |
)
|
|
|
|
| 1905 |
|
| 1906 |
],
|
| 1907 |
fn=generate_video_example,
|
| 1908 |
+
inputs=[input_image, prompt_ui, camera_ui, resolution_ui, audio_input],
|
| 1909 |
outputs = [output_video],
|
| 1910 |
label="I2V Examples",
|
| 1911 |
cache_examples=True,
|