Spaces:
Running on Zero
Running on Zero
Commit ·
c0e7e4d
1
Parent(s): c3c6897
Add showcase cases, auto-captioning, and drop no-translation pose
Browse files- Reorder examples: iStock rice-field motorbike first; add two OmniWorld
character scenes (misty forest, horse trail) in a separate "with characters"
group, per the release curation.
- Move the examples above the Advanced settings so they are visible and
clickable without scrolling.
- Auto-generate a scene prompt with Qwen2-VL-2B when the description is left
blank, instead of erroring, so an image-only request still gets a caption.
The model is held on CPU and only lifted onto the GPU while captioning.
- Remove the pan_right preset (pure in-place rotation, zero translation), the
only preset with no visible camera translation.
Co-authored-by: Cursor <cursoragent@cursor.com>
- .gitattributes +3 -0
- README.md +11 -7
- app.py +131 -16
- trajectories/pan_right.npy → examples/istock-motorbike-rice-field.jpg +2 -2
- examples/omni-horse-trail.jpg +3 -0
- examples/omni-misty-forest.jpg +3 -0
.gitattributes
CHANGED
|
@@ -38,3 +38,6 @@ diffsynth/tokenizer_configs/kolors/tokenizer/vocab.txt filter=lfs diff=lfs merge
|
|
| 38 |
examples/ai-airmountains.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
examples/ai-middleages.jpg filter=lfs diff=lfs merge=lfs -text
|
| 40 |
examples/ai-valley.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
examples/ai-airmountains.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
examples/ai-middleages.jpg filter=lfs diff=lfs merge=lfs -text
|
| 40 |
examples/ai-valley.jpg filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
examples/istock-motorbike-rice-field.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
examples/omni-misty-forest.jpg filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
examples/omni-horse-trail.jpg filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -25,8 +25,9 @@ tags:
|
|
| 25 |
[TencentARC/SCoPE](https://huggingface.co/TencentARC/SCoPE) retrofits **Wan2.2-I2V-A14B** with
|
| 26 |
*Sightline-Coordinate Positional Encoding*: Plücker camera rays are normalised, gated and injected
|
| 27 |
directly into the DiT's self-attention queries and keys, so an explicit 3D camera path steers the
|
| 28 |
-
generated shot. Give it a first frame
|
| 29 |
-
832×480, 16 fps clip.
|
|
|
|
| 30 |
|
| 31 |
* Paper / project page: <https://visual-ai.github.io/scope/>
|
| 32 |
* Reference code: <https://github.com/TencentARC/SCoPE>
|
|
@@ -40,9 +41,10 @@ height=480, width=832, num_frames=81, sigma_shift=5.0, switch_DiT_boundary=0.9,
|
|
| 40 |
camera_cfg_scale=1.0, lock_first_frame=False, tiled=False)`.
|
| 41 |
|
| 42 |
Camera paths are OpenCV **camera-to-world** matrices of shape `[81, 3, 4]`, expressed relative to
|
| 43 |
-
the first frame, which is the format SCoPE was trained on. The
|
| 44 |
taken from the release's own `examples/` trajectory set (re-expressed relative to frame 0), and the
|
| 45 |
-
"camera motion scale" slider rescales only their translation component.
|
|
|
|
| 46 |
|
| 47 |
## Deviations from the reference configuration
|
| 48 |
|
|
@@ -63,6 +65,8 @@ control itself is architectural and unaffected by the shorter schedule.
|
|
| 63 |
|
| 64 |
## Credits
|
| 65 |
|
| 66 |
-
Model, code and example images © the SCoPE authors (Apache-2.0). The
|
| 67 |
-
`examples/` are the AI-generated
|
| 68 |
-
|
|
|
|
|
|
|
|
|
| 25 |
[TencentARC/SCoPE](https://huggingface.co/TencentARC/SCoPE) retrofits **Wan2.2-I2V-A14B** with
|
| 26 |
*Sightline-Coordinate Positional Encoding*: Plücker camera rays are normalised, gated and injected
|
| 27 |
directly into the DiT's self-attention queries and keys, so an explicit 3D camera path steers the
|
| 28 |
+
generated shot. Give it a first frame and a camera move, and it returns an 81-frame,
|
| 29 |
+
832×480, 16 fps clip. A caption is used for the scene content; if you leave it blank, a small
|
| 30 |
+
vision-language model (`Qwen/Qwen2-VL-2B-Instruct`) writes one from your first frame automatically.
|
| 31 |
|
| 32 |
* Paper / project page: <https://visual-ai.github.io/scope/>
|
| 33 |
* Reference code: <https://github.com/TencentARC/SCoPE>
|
|
|
|
| 41 |
camera_cfg_scale=1.0, lock_first_frame=False, tiled=False)`.
|
| 42 |
|
| 43 |
Camera paths are OpenCV **camera-to-world** matrices of shape `[81, 3, 4]`, expressed relative to
|
| 44 |
+
the first frame, which is the format SCoPE was trained on. The 16 presets in `trajectories/` are
|
| 45 |
taken from the release's own `examples/` trajectory set (re-expressed relative to frame 0), and the
|
| 46 |
+
"camera motion scale" slider rescales only their translation component. Every preset carries a real
|
| 47 |
+
translation; pure in-place rotations were dropped because their effect is hard to see.
|
| 48 |
|
| 49 |
## Deviations from the reference configuration
|
| 50 |
|
|
|
|
| 65 |
|
| 66 |
## Credits
|
| 67 |
|
| 68 |
+
Model, code and example images © the SCoPE authors (Apache-2.0). The showcase images in
|
| 69 |
+
`examples/` are drawn from the SCoPE release: three AI-generated scenes, one iStock aerial clip
|
| 70 |
+
(rice-field motorbike), and two OmniWorld character scenes (kept in their own "with characters"
|
| 71 |
+
group). Automatic prompting uses [`Qwen/Qwen2-VL-2B-Instruct`](https://huggingface.co/Qwen/Qwen2-VL-2B-Instruct)
|
| 72 |
+
(Apache-2.0).
|
app.py
CHANGED
|
@@ -43,6 +43,7 @@ from torchao.quantization import ( # noqa: E402
|
|
| 43 |
Int8WeightOnlyConfig,
|
| 44 |
quantize_,
|
| 45 |
)
|
|
|
|
| 46 |
|
| 47 |
from diffsynth.data.video import save_video # noqa: E402
|
| 48 |
from diffsynth.models import ModelManager # noqa: E402
|
|
@@ -60,6 +61,14 @@ HERE = Path(__file__).resolve().parent
|
|
| 60 |
REPO_ID = "TencentARC/SCoPE"
|
| 61 |
LORA_REPO = "lightx2v/Wan2.2-Lightning"
|
| 62 |
LORA_SUBDIR = "Wan2.2-I2V-A14B-4steps-lora-rank64-Seko-V1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
WORK_DIR = Path(os.environ.get("SCOPE_WEIGHT_DIR", "/tmp/scope-weights"))
|
| 64 |
DEVICE = "cuda"
|
| 65 |
|
|
@@ -79,7 +88,6 @@ PRESETS: list[tuple[str, str]] = [
|
|
| 79 |
("Dolly out — pull straight back", "dolly_out"),
|
| 80 |
("Truck left — slide sideways to the left", "truck_left"),
|
| 81 |
("Truck right — slide sideways to the right", "truck_right"),
|
| 82 |
-
("Pan right — rotate in place, no translation", "pan_right"),
|
| 83 |
("Orbit left — arc around the subject", "orbit_left"),
|
| 84 |
("Crane up + forward — rise while pushing in", "crane_up_fwd"),
|
| 85 |
("Snake forward — weaving push-in", "snake_fwd"),
|
|
@@ -299,6 +307,57 @@ def build_pipeline() -> SCoPEPipeline:
|
|
| 299 |
PIPE = build_pipeline()
|
| 300 |
|
| 301 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
# --------------------------------------------------------------------------------------
|
| 303 |
# Inference
|
| 304 |
# --------------------------------------------------------------------------------------
|
|
@@ -338,7 +397,11 @@ def estimate_duration(
|
|
| 338 |
# Measured on ZeroGPU (fp8 experts, 832x480x81): 4 steps -> 67.5s, 8 steps -> 124s,
|
| 339 |
# i.e. ~14.1s per sampling step over ~11s of fixed text-encode/VAE cost. Keep a ~15%
|
| 340 |
# margin and nothing more, so a default 4-step run stays inside the free 120s quota.
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
|
| 344 |
@spaces.GPU(duration=estimate_duration)
|
|
@@ -355,8 +418,14 @@ def generate(
|
|
| 355 |
):
|
| 356 |
first_frame = prepare_image(image)
|
| 357 |
prompt = (prompt or "").strip()
|
|
|
|
| 358 |
if not prompt:
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
used_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
|
| 362 |
poses = load_trajectory(trajectory, motion_scale)
|
|
@@ -398,6 +467,8 @@ def generate(
|
|
| 398 |
f"{int(steps)} steps · seed {used_seed} · {PRESET_LABELS.get(trajectory, trajectory)} "
|
| 399 |
f"· motion x{motion_scale:g} · {elapsed:.0f}s"
|
| 400 |
)
|
|
|
|
|
|
|
| 401 |
return output.name, used_seed, status
|
| 402 |
|
| 403 |
|
|
@@ -458,6 +529,16 @@ def preview_path(trajectory: str, motion_scale: float) -> Image.Image:
|
|
| 458 |
# --------------------------------------------------------------------------------------
|
| 459 |
|
| 460 |
EXAMPLES = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
[
|
| 462 |
str(HERE / "examples" / "ai-airmountains.jpg"),
|
| 463 |
"A vast sky filled with multiple floating islands of different sizes, suspended above a "
|
|
@@ -490,6 +571,27 @@ EXAMPLES = [
|
|
| 490 |
],
|
| 491 |
]
|
| 492 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
CSS = """
|
| 494 |
#col-container { margin: 0 auto; max-width: 1180px; }
|
| 495 |
.dark .gradio-container { color: var(--body-text-color); }
|
|
@@ -505,8 +607,9 @@ with gr.Blocks() as demo:
|
|
| 505 |
rays are normalised, gated and injected straight into the DiT's self-attention
|
| 506 |
queries/keys, so a real 3D camera path drives the generated shot.
|
| 507 |
|
| 508 |
-
Drop in an image,
|
| 509 |
-
81-frame, 832x480, 16 fps clip that follows that trajectory.
|
|
|
|
| 510 |
"""
|
| 511 |
)
|
| 512 |
with gr.Row():
|
|
@@ -516,8 +619,9 @@ with gr.Blocks() as demo:
|
|
| 516 |
)
|
| 517 |
prompt_input = gr.Textbox(
|
| 518 |
label="Scene description",
|
| 519 |
-
placeholder="Describe what is in the image
|
| 520 |
lines=4,
|
|
|
|
| 521 |
)
|
| 522 |
trajectory_input = gr.Dropdown(
|
| 523 |
label="Camera move",
|
|
@@ -536,6 +640,27 @@ with gr.Blocks() as demo:
|
|
| 536 |
)
|
| 537 |
status_output = gr.Markdown()
|
| 538 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
with gr.Accordion("Advanced settings", open=False):
|
| 540 |
with gr.Row():
|
| 541 |
steps_input = gr.Slider(
|
|
@@ -571,16 +696,6 @@ with gr.Blocks() as demo:
|
|
| 571 |
)
|
| 572 |
randomize_input = gr.Checkbox(label="Randomize seed", value=True)
|
| 573 |
|
| 574 |
-
gr.Examples(
|
| 575 |
-
examples=EXAMPLES,
|
| 576 |
-
inputs=[image_input, prompt_input, trajectory_input],
|
| 577 |
-
outputs=[video_output, seed_input, status_output],
|
| 578 |
-
fn=generate,
|
| 579 |
-
cache_examples=True,
|
| 580 |
-
cache_mode="lazy",
|
| 581 |
-
label="Examples (AI-generated showcase scenes from the SCoPE release)",
|
| 582 |
-
)
|
| 583 |
-
|
| 584 |
gr.Markdown(
|
| 585 |
"""
|
| 586 |
**Notes** · Camera paths are OpenCV camera-to-world matrices `[81, 3, 4]` relative to
|
|
|
|
| 43 |
Int8WeightOnlyConfig,
|
| 44 |
quantize_,
|
| 45 |
)
|
| 46 |
+
from transformers import AutoProcessor, Qwen2VLForConditionalGeneration # noqa: E402
|
| 47 |
|
| 48 |
from diffsynth.data.video import save_video # noqa: E402
|
| 49 |
from diffsynth.models import ModelManager # noqa: E402
|
|
|
|
| 61 |
REPO_ID = "TencentARC/SCoPE"
|
| 62 |
LORA_REPO = "lightx2v/Wan2.2-Lightning"
|
| 63 |
LORA_SUBDIR = "Wan2.2-I2V-A14B-4steps-lora-rank64-Seko-V1"
|
| 64 |
+
# A small vision-language model used to auto-write a caption when the user leaves the
|
| 65 |
+
# scene description blank — SCoPE always needs a caption for the content it should draw.
|
| 66 |
+
CAPTIONER_REPO = "Qwen/Qwen2-VL-2B-Instruct"
|
| 67 |
+
CAPTION_INSTRUCTION = (
|
| 68 |
+
"Write one vivid paragraph describing this image for a video generation model. "
|
| 69 |
+
"Cover the main subjects, the setting, materials and colours, and the lighting. "
|
| 70 |
+
"Do not mention the camera, camera motion, shot type, or the word 'image'."
|
| 71 |
+
)
|
| 72 |
WORK_DIR = Path(os.environ.get("SCOPE_WEIGHT_DIR", "/tmp/scope-weights"))
|
| 73 |
DEVICE = "cuda"
|
| 74 |
|
|
|
|
| 88 |
("Dolly out — pull straight back", "dolly_out"),
|
| 89 |
("Truck left — slide sideways to the left", "truck_left"),
|
| 90 |
("Truck right — slide sideways to the right", "truck_right"),
|
|
|
|
| 91 |
("Orbit left — arc around the subject", "orbit_left"),
|
| 92 |
("Crane up + forward — rise while pushing in", "crane_up_fwd"),
|
| 93 |
("Snake forward — weaving push-in", "snake_fwd"),
|
|
|
|
| 307 |
PIPE = build_pipeline()
|
| 308 |
|
| 309 |
|
| 310 |
+
# --------------------------------------------------------------------------------------
|
| 311 |
+
# Optional prompt writer — Qwen2-VL captions the first frame when no prompt is given.
|
| 312 |
+
# Downloaded and held on CPU at build time; only lifted onto the GPU for the few seconds
|
| 313 |
+
# it is actually needed, then evicted so it never competes with the two 14B experts.
|
| 314 |
+
# --------------------------------------------------------------------------------------
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def build_captioner() -> tuple[Qwen2VLForConditionalGeneration, AutoProcessor]:
|
| 318 |
+
started = time.time()
|
| 319 |
+
processor = AutoProcessor.from_pretrained(
|
| 320 |
+
CAPTIONER_REPO, min_pixels=256 * 28 * 28, max_pixels=768 * 28 * 28
|
| 321 |
+
)
|
| 322 |
+
model = Qwen2VLForConditionalGeneration.from_pretrained(
|
| 323 |
+
CAPTIONER_REPO, torch_dtype=torch.bfloat16
|
| 324 |
+
)
|
| 325 |
+
model.requires_grad_(False)
|
| 326 |
+
model.eval()
|
| 327 |
+
print(f"[SCoPE] caption model ready in {time.time() - started:.0f}s", flush=True)
|
| 328 |
+
return model, processor
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
CAPTIONER, CAPTION_PROCESSOR = build_captioner()
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def autocaption(image: Image.Image) -> str:
|
| 335 |
+
"""Describe `image` with Qwen2-VL so a promptless request still has scene content."""
|
| 336 |
+
messages = [
|
| 337 |
+
{
|
| 338 |
+
"role": "user",
|
| 339 |
+
"content": [{"type": "image"}, {"type": "text", "text": CAPTION_INSTRUCTION}],
|
| 340 |
+
}
|
| 341 |
+
]
|
| 342 |
+
text = CAPTION_PROCESSOR.apply_chat_template(
|
| 343 |
+
messages, tokenize=False, add_generation_prompt=True
|
| 344 |
+
)
|
| 345 |
+
inputs = CAPTION_PROCESSOR(text=[text], images=[image], return_tensors="pt")
|
| 346 |
+
CAPTIONER.to(DEVICE)
|
| 347 |
+
try:
|
| 348 |
+
inputs = inputs.to(DEVICE)
|
| 349 |
+
with torch.inference_mode():
|
| 350 |
+
generated = CAPTIONER.generate(**inputs, max_new_tokens=220, do_sample=False)
|
| 351 |
+
trimmed = generated[:, inputs["input_ids"].shape[1] :]
|
| 352 |
+
caption = CAPTION_PROCESSOR.batch_decode(
|
| 353 |
+
trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=True
|
| 354 |
+
)[0].strip()
|
| 355 |
+
finally:
|
| 356 |
+
CAPTIONER.to("cpu")
|
| 357 |
+
torch.cuda.empty_cache()
|
| 358 |
+
return caption
|
| 359 |
+
|
| 360 |
+
|
| 361 |
# --------------------------------------------------------------------------------------
|
| 362 |
# Inference
|
| 363 |
# --------------------------------------------------------------------------------------
|
|
|
|
| 397 |
# Measured on ZeroGPU (fp8 experts, 832x480x81): 4 steps -> 67.5s, 8 steps -> 124s,
|
| 398 |
# i.e. ~14.1s per sampling step over ~11s of fixed text-encode/VAE cost. Keep a ~15%
|
| 399 |
# margin and nothing more, so a default 4-step run stays inside the free 120s quota.
|
| 400 |
+
budget = 11.0 + 14.1 * int(steps)
|
| 401 |
+
if not (prompt or "").strip():
|
| 402 |
+
# Lifting Qwen2-VL onto the GPU and captioning the frame adds a fixed overhead.
|
| 403 |
+
budget += 12.0
|
| 404 |
+
return int(math.ceil(1.15 * budget))
|
| 405 |
|
| 406 |
|
| 407 |
@spaces.GPU(duration=estimate_duration)
|
|
|
|
| 418 |
):
|
| 419 |
first_frame = prepare_image(image)
|
| 420 |
prompt = (prompt or "").strip()
|
| 421 |
+
autocaptioned = False
|
| 422 |
if not prompt:
|
| 423 |
+
# SCoPE needs a caption for the content, so write one from the frame instead of
|
| 424 |
+
# failing on an empty prompt.
|
| 425 |
+
prompt = autocaption(first_frame)
|
| 426 |
+
autocaptioned = True
|
| 427 |
+
if not prompt:
|
| 428 |
+
raise gr.Error("Could not caption the image automatically — please add a description.")
|
| 429 |
|
| 430 |
used_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
|
| 431 |
poses = load_trajectory(trajectory, motion_scale)
|
|
|
|
| 467 |
f"{int(steps)} steps · seed {used_seed} · {PRESET_LABELS.get(trajectory, trajectory)} "
|
| 468 |
f"· motion x{motion_scale:g} · {elapsed:.0f}s"
|
| 469 |
)
|
| 470 |
+
if autocaptioned:
|
| 471 |
+
status += f"\n\n*Auto-generated prompt (Qwen2-VL):* {prompt}"
|
| 472 |
return output.name, used_seed, status
|
| 473 |
|
| 474 |
|
|
|
|
| 529 |
# --------------------------------------------------------------------------------------
|
| 530 |
|
| 531 |
EXAMPLES = [
|
| 532 |
+
[
|
| 533 |
+
str(HERE / "examples" / "istock-motorbike-rice-field.jpg"),
|
| 534 |
+
"A serene rural landscape in soft early-morning light with long shadows. A dirt road "
|
| 535 |
+
"winds through a lush green area; on the left, dense clusters of tall trees including palms "
|
| 536 |
+
"and a few small houses with red roofs, and on the right a vast expanse of green rice "
|
| 537 |
+
"paddies neatly divided into sections. A motorcycle travels along the dirt road, flanked by "
|
| 538 |
+
"utility poles with wires running along them. The overall atmosphere is peaceful and "
|
| 539 |
+
"idyllic, with greenery and natural elements dominating the scene.",
|
| 540 |
+
"orbit_left",
|
| 541 |
+
],
|
| 542 |
[
|
| 543 |
str(HERE / "examples" / "ai-airmountains.jpg"),
|
| 544 |
"A vast sky filled with multiple floating islands of different sizes, suspended above a "
|
|
|
|
| 571 |
],
|
| 572 |
]
|
| 573 |
|
| 574 |
+
# Scenes that contain a person / character. Kept in their own group so the showcase above
|
| 575 |
+
# stays character-free, per the release curation.
|
| 576 |
+
CHARACTER_EXAMPLES = [
|
| 577 |
+
[
|
| 578 |
+
str(HERE / "examples" / "omni-misty-forest.jpg"),
|
| 579 |
+
"A character in red armor and a straw hat progresses along a forest path, their steps "
|
| 580 |
+
"deliberate as they navigate over stones and through patches of grass. The environment is "
|
| 581 |
+
"a misty forest with ancient stone structures on the left and moss-covered cliffs on the "
|
| 582 |
+
"right. The dense fog and surrounding greenery contribute to a mysterious ambiance.",
|
| 583 |
+
"orbit_left",
|
| 584 |
+
],
|
| 585 |
+
[
|
| 586 |
+
str(HERE / "examples" / "omni-horse-trail.jpg"),
|
| 587 |
+
"A character dressed in dark attire rides a white horse steadily along a rugged dirt path "
|
| 588 |
+
"that meanders through rocky terrain interspersed with patches of grass and shrubs. To the "
|
| 589 |
+
"left, a wooden fence lines the trail. In the distance, the landscape opens up to reveal "
|
| 590 |
+
"rolling hills covered in vegetation and distant mountains under a bright sky.",
|
| 591 |
+
"crane_up_fwd",
|
| 592 |
+
],
|
| 593 |
+
]
|
| 594 |
+
|
| 595 |
CSS = """
|
| 596 |
#col-container { margin: 0 auto; max-width: 1180px; }
|
| 597 |
.dark .gradio-container { color: var(--body-text-color); }
|
|
|
|
| 607 |
rays are normalised, gated and injected straight into the DiT's self-attention
|
| 608 |
queries/keys, so a real 3D camera path drives the generated shot.
|
| 609 |
|
| 610 |
+
Drop in an image, pick a camera move, and (optionally) describe the scene — you get
|
| 611 |
+
an 81-frame, 832x480, 16 fps clip that follows that trajectory. Leave the description
|
| 612 |
+
blank and a small vision-language model writes one from your image automatically.
|
| 613 |
"""
|
| 614 |
)
|
| 615 |
with gr.Row():
|
|
|
|
| 619 |
)
|
| 620 |
prompt_input = gr.Textbox(
|
| 621 |
label="Scene description",
|
| 622 |
+
placeholder="Describe what is in the image… (leave empty to auto-caption it)",
|
| 623 |
lines=4,
|
| 624 |
+
info="Optional — if left blank, Qwen2-VL writes a caption from your image.",
|
| 625 |
)
|
| 626 |
trajectory_input = gr.Dropdown(
|
| 627 |
label="Camera move",
|
|
|
|
| 640 |
)
|
| 641 |
status_output = gr.Markdown()
|
| 642 |
|
| 643 |
+
gr.Examples(
|
| 644 |
+
examples=EXAMPLES,
|
| 645 |
+
inputs=[image_input, prompt_input, trajectory_input],
|
| 646 |
+
outputs=[video_output, seed_input, status_output],
|
| 647 |
+
fn=generate,
|
| 648 |
+
cache_examples=True,
|
| 649 |
+
cache_mode="lazy",
|
| 650 |
+
label="Examples — click a row to load its image, prompt and camera move",
|
| 651 |
+
examples_per_page=8,
|
| 652 |
+
)
|
| 653 |
+
gr.Examples(
|
| 654 |
+
examples=CHARACTER_EXAMPLES,
|
| 655 |
+
inputs=[image_input, prompt_input, trajectory_input],
|
| 656 |
+
outputs=[video_output, seed_input, status_output],
|
| 657 |
+
fn=generate,
|
| 658 |
+
cache_examples=True,
|
| 659 |
+
cache_mode="lazy",
|
| 660 |
+
label="Examples with characters",
|
| 661 |
+
examples_per_page=8,
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
with gr.Accordion("Advanced settings", open=False):
|
| 665 |
with gr.Row():
|
| 666 |
steps_input = gr.Slider(
|
|
|
|
| 696 |
)
|
| 697 |
randomize_input = gr.Checkbox(label="Randomize seed", value=True)
|
| 698 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
gr.Markdown(
|
| 700 |
"""
|
| 701 |
**Notes** · Camera paths are OpenCV camera-to-world matrices `[81, 3, 4]` relative to
|
trajectories/pan_right.npy → examples/istock-motorbike-rice-field.jpg
RENAMED
|
File without changes
|
examples/omni-horse-trail.jpg
ADDED
|
Git LFS Details
|
examples/omni-misty-forest.jpg
ADDED
|
Git LFS Details
|