Spaces:
Runtime error
Runtime error
nsfwalex Claude Opus 4.8 (1M context) commited on
Commit Β·
bae8329
1
Parent(s): b3b69d0
feat: add UI-less prompt_to_video_assets endpoint (LLM->image->LLM)
Browse filesChains Qwen (first-frame image prompt) -> image model -> R2 upload ->
Qwen (video prompt grounded on the frame) in one streaming call. Exposed
via gr.api so it adds no Gradio UI. Returns video_prompt + first_frame_url
(presigned GET) plus r2 filekey/bucket and the intermediate frame prompt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/r2_uploader.cpython-312.pyc +0 -0
- app.py +173 -0
- r2_uploader.py +21 -0
__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (41.7 kB). View file
|
|
|
__pycache__/r2_uploader.cpython-312.pyc
ADDED
|
Binary file (9.71 kB). View file
|
|
|
app.py
CHANGED
|
@@ -676,6 +676,174 @@ def assistant_chat(
|
|
| 676 |
yield text, _progress("done", 1.0, label="Done")
|
| 677 |
|
| 678 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
# Recommended defaults per model: (steps, guidance, height, width)
|
| 680 |
MODEL_DEFAULTS = {
|
| 681 |
MODEL_ZIMAGE: dict(steps=9, guidance=0.0, height=1024, width=1024),
|
|
@@ -951,6 +1119,11 @@ with gr.Blocks(fill_height=True) as demo:
|
|
| 951 |
fn=assistant_chat, inputs=vlm_inputs, outputs=[vlm_output, vlm_progress],
|
| 952 |
)
|
| 953 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
if __name__ == "__main__":
|
| 955 |
demo.launch(
|
| 956 |
theme=custom_theme,
|
|
|
|
| 676 |
yield text, _progress("done", 1.0, label="Done")
|
| 677 |
|
| 678 |
|
| 679 |
+
# =============================================================================
|
| 680 |
+
# Combined endpoint: text -> first-frame image + video prompt (no UI)
|
| 681 |
+
# =============================================================================
|
| 682 |
+
# One call chains LLM + image model + LLM to turn a raw idea into the two assets
|
| 683 |
+
# an image-to-video pipeline needs:
|
| 684 |
+
# 1. LLM (Qwen) writes a first-frame *image* prompt from
|
| 685 |
+
# `image_instruction` + `original_input`.
|
| 686 |
+
# 2. The image model renders that first frame.
|
| 687 |
+
# 3. The frame is uploaded to R2 (same path as the Generate tab).
|
| 688 |
+
# 4. LLM (Qwen) writes a *video* prompt from `video_instruction` +
|
| 689 |
+
# `original_input`, grounded on the rendered first frame image.
|
| 690 |
+
# It returns the video prompt and the first-frame URL (+ R2 filekey/bucket and
|
| 691 |
+
# the intermediate image prompt). Exposed via `gr.api` so it has no Gradio UI.
|
| 692 |
+
#
|
| 693 |
+
# Like the other streaming endpoints it is a generator yielding one structured
|
| 694 |
+
# dict per frame (so progress crosses the /call SSE stream, see _progress); the
|
| 695 |
+
# same dict carries the results, filled in as each stage completes, and the final
|
| 696 |
+
# yield has `done=True`. Stage weights below sum to 1.0.
|
| 697 |
+
_P2V_W_FRAME_PROMPT = 0.30 # LLM writes the first-frame image prompt
|
| 698 |
+
_P2V_W_IMAGE = 0.30 # image model renders the first frame
|
| 699 |
+
_P2V_W_UPLOAD = 0.05 # R2 upload
|
| 700 |
+
_P2V_W_VIDEO_PROMPT = 0.35 # LLM writes the video prompt
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
def _compose_instruction(instruction, original_input):
|
| 704 |
+
"""Join an instruction with the raw idea into a single LLM message."""
|
| 705 |
+
instruction = (instruction or "").strip()
|
| 706 |
+
original_input = (original_input or "").strip()
|
| 707 |
+
if instruction and original_input:
|
| 708 |
+
return f"{instruction}\n\nInput:\n{original_input}"
|
| 709 |
+
return instruction or original_input
|
| 710 |
+
|
| 711 |
+
|
| 712 |
+
def prompt_to_video_assets(
|
| 713 |
+
original_input: str,
|
| 714 |
+
image_instruction: str,
|
| 715 |
+
video_instruction: str,
|
| 716 |
+
model_name: str = MODEL_ZIMAGE,
|
| 717 |
+
height: int = 1024,
|
| 718 |
+
width: int = 1024,
|
| 719 |
+
num_inference_steps: int = 9,
|
| 720 |
+
guidance_scale: float = 0.0,
|
| 721 |
+
seed: int = 42,
|
| 722 |
+
randomize_seed: bool = True,
|
| 723 |
+
reasoning: str = "Off",
|
| 724 |
+
max_new_tokens: int = 512,
|
| 725 |
+
request: gr.Request = None,
|
| 726 |
+
) -> dict:
|
| 727 |
+
"""Text -> first-frame image (uploaded to R2) + video generation prompt.
|
| 728 |
+
|
| 729 |
+
Streams progress and returns a dict with ``video_prompt``, ``first_frame_url``
|
| 730 |
+
(a presigned GET, usable directly), ``r2_filekey``/``r2_bucket`` (for callers
|
| 731 |
+
that resolve their own public URL), ``first_frame_prompt`` (the intermediate
|
| 732 |
+
image prompt) and ``seed``. Has no UI (registered via ``gr.api``).
|
| 733 |
+
"""
|
| 734 |
+
state = {
|
| 735 |
+
"stage": "frame_prompt", "p": 0.0, "step": 0, "total": 0, "label": "",
|
| 736 |
+
"first_frame_prompt": None,
|
| 737 |
+
"first_frame_url": None,
|
| 738 |
+
"r2_filekey": None,
|
| 739 |
+
"r2_bucket": None,
|
| 740 |
+
"video_prompt": None,
|
| 741 |
+
"seed": None,
|
| 742 |
+
"done": False,
|
| 743 |
+
"error": None,
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
def frame(stage, base, span, frac=1.0, step=0, total=0, label=""):
|
| 747 |
+
state.update(
|
| 748 |
+
stage=stage,
|
| 749 |
+
p=max(0.0, min(1.0, base + span * max(0.0, min(1.0, frac)))),
|
| 750 |
+
step=int(step), total=int(total), label=label,
|
| 751 |
+
)
|
| 752 |
+
return dict(state)
|
| 753 |
+
|
| 754 |
+
if not (original_input or "").strip():
|
| 755 |
+
state["error"] = "original_input is required"
|
| 756 |
+
state["done"] = True
|
| 757 |
+
yield dict(state)
|
| 758 |
+
return
|
| 759 |
+
|
| 760 |
+
# --- Stage 1: LLM writes the first-frame image prompt ---------------------
|
| 761 |
+
base = 0.0
|
| 762 |
+
frame_prompt = ""
|
| 763 |
+
for ev in vlm_chat(
|
| 764 |
+
_compose_instruction(image_instruction, original_input),
|
| 765 |
+
None, reasoning, max_new_tokens,
|
| 766 |
+
):
|
| 767 |
+
if ev[0] == "progress":
|
| 768 |
+
_, produced, budget, partial = ev
|
| 769 |
+
frame_prompt = partial
|
| 770 |
+
yield frame("frame_prompt", base, _P2V_W_FRAME_PROMPT,
|
| 771 |
+
frac=produced / max(budget, 1), step=produced, total=budget,
|
| 772 |
+
label="Writing first-frame prompt")
|
| 773 |
+
else:
|
| 774 |
+
frame_prompt = ev[1]
|
| 775 |
+
frame_prompt = (frame_prompt or "").strip()
|
| 776 |
+
state["first_frame_prompt"] = frame_prompt
|
| 777 |
+
yield frame("frame_prompt", base, _P2V_W_FRAME_PROMPT, label="First-frame prompt ready")
|
| 778 |
+
|
| 779 |
+
# --- Stage 2: image model renders the first frame -------------------------
|
| 780 |
+
base += _P2V_W_FRAME_PROMPT
|
| 781 |
+
image, used_seed = None, seed
|
| 782 |
+
use_negative_prompt = (model_name == MODEL_NOOBXL)
|
| 783 |
+
for ev in generate_image(
|
| 784 |
+
model_name, frame_prompt, NOOBXL_NEGATIVE, use_negative_prompt,
|
| 785 |
+
height, width, num_inference_steps, guidance_scale, seed, randomize_seed,
|
| 786 |
+
):
|
| 787 |
+
if ev[0] == "progress":
|
| 788 |
+
_, step, total = ev
|
| 789 |
+
yield frame("image", base, _P2V_W_IMAGE,
|
| 790 |
+
frac=step / max(total, 1), step=step, total=total,
|
| 791 |
+
label=f"Rendering first frame {step}/{total}")
|
| 792 |
+
else:
|
| 793 |
+
_, image, used_seed = ev
|
| 794 |
+
state["seed"] = int(used_seed)
|
| 795 |
+
|
| 796 |
+
# --- Stage 3: upload the first frame to R2 --------------------------------
|
| 797 |
+
base += _P2V_W_IMAGE
|
| 798 |
+
yield frame("upload", base, _P2V_W_UPLOAD, frac=0.1, label="Uploading first frame")
|
| 799 |
+
uid = r2_uploader.uid_from_request(request)
|
| 800 |
+
buf = io.BytesIO()
|
| 801 |
+
image.save(buf, format="PNG")
|
| 802 |
+
params = {
|
| 803 |
+
"model": model_name,
|
| 804 |
+
"prompt": frame_prompt,
|
| 805 |
+
"negative_prompt": (NOOBXL_NEGATIVE if use_negative_prompt else ""),
|
| 806 |
+
"height": int(height),
|
| 807 |
+
"width": int(width),
|
| 808 |
+
"num_inference_steps": int(num_inference_steps),
|
| 809 |
+
"guidance_scale": float(guidance_scale),
|
| 810 |
+
"seed": int(used_seed),
|
| 811 |
+
"uid": uid,
|
| 812 |
+
"source": "prompt_to_video_assets",
|
| 813 |
+
"original_input": original_input,
|
| 814 |
+
}
|
| 815 |
+
up = r2_uploader.upload_asset(
|
| 816 |
+
namespace=R2_NAMESPACE, prompt=frame_prompt, params=params,
|
| 817 |
+
data=buf.getvalue(), ext=".png", content_type="image/png", uid=uid,
|
| 818 |
+
)
|
| 819 |
+
if up.get("ok"):
|
| 820 |
+
state["r2_filekey"] = up["filekey"]
|
| 821 |
+
state["r2_bucket"] = up["bucket"]
|
| 822 |
+
state["first_frame_url"] = r2_uploader.presign_get_url(up["filekey"], up["bucket"])
|
| 823 |
+
else:
|
| 824 |
+
state["error"] = up.get("error", "R2 upload failed")
|
| 825 |
+
|
| 826 |
+
# --- Stage 4: LLM writes the video prompt (grounded on the frame) ---------
|
| 827 |
+
base += _P2V_W_UPLOAD
|
| 828 |
+
video_prompt = ""
|
| 829 |
+
for ev in vlm_chat(
|
| 830 |
+
_compose_instruction(video_instruction, original_input),
|
| 831 |
+
image, reasoning, max_new_tokens,
|
| 832 |
+
):
|
| 833 |
+
if ev[0] == "progress":
|
| 834 |
+
_, produced, budget, partial = ev
|
| 835 |
+
video_prompt = partial
|
| 836 |
+
yield frame("video_prompt", base, _P2V_W_VIDEO_PROMPT,
|
| 837 |
+
frac=produced / max(budget, 1), step=produced, total=budget,
|
| 838 |
+
label="Writing video prompt")
|
| 839 |
+
else:
|
| 840 |
+
video_prompt = ev[1]
|
| 841 |
+
state["video_prompt"] = (video_prompt or "").strip()
|
| 842 |
+
|
| 843 |
+
state["done"] = True
|
| 844 |
+
yield frame("done", 1.0, 0.0, label="Done")
|
| 845 |
+
|
| 846 |
+
|
| 847 |
# Recommended defaults per model: (steps, guidance, height, width)
|
| 848 |
MODEL_DEFAULTS = {
|
| 849 |
MODEL_ZIMAGE: dict(steps=9, guidance=0.0, height=1024, width=1024),
|
|
|
|
| 1119 |
fn=assistant_chat, inputs=vlm_inputs, outputs=[vlm_output, vlm_progress],
|
| 1120 |
)
|
| 1121 |
|
| 1122 |
+
# UI-less combined endpoint: text -> first-frame image (R2) + video prompt.
|
| 1123 |
+
# `gr.api` derives its schema from the function's type hints and registers no
|
| 1124 |
+
# components, so it adds an API route without touching the visible UI.
|
| 1125 |
+
gr.api(prompt_to_video_assets, api_name="prompt_to_video_assets")
|
| 1126 |
+
|
| 1127 |
if __name__ == "__main__":
|
| 1128 |
demo.launch(
|
| 1129 |
theme=custom_theme,
|
r2_uploader.py
CHANGED
|
@@ -126,6 +126,27 @@ def _ensure_bucket(s3, bucket: str) -> None:
|
|
| 126 |
_ensured_buckets.add(bucket)
|
| 127 |
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
def upload_asset(
|
| 130 |
*,
|
| 131 |
namespace: str,
|
|
|
|
| 126 |
_ensured_buckets.add(bucket)
|
| 127 |
|
| 128 |
|
| 129 |
+
def presign_get_url(filekey: str, bucket: str | None = None, expires: int = 604800) -> str | None:
|
| 130 |
+
"""Return a presigned GET URL for an uploaded object, or None on failure.
|
| 131 |
+
|
| 132 |
+
``expires`` defaults to 7 days (the SigV4 maximum). This lets callers hand
|
| 133 |
+
back a directly-usable URL even when no public R2 domain is bound; downstream
|
| 134 |
+
consumers that have a public base can still rebuild a clean URL from the
|
| 135 |
+
``filekey``/``bucket`` reported alongside it. Never raises.
|
| 136 |
+
"""
|
| 137 |
+
try:
|
| 138 |
+
cfg = _load_cfg()
|
| 139 |
+
bucket = bucket or cfg.get("bucket") or DEFAULT_BUCKET
|
| 140 |
+
s3 = _client(cfg)
|
| 141 |
+
return s3.generate_presigned_url(
|
| 142 |
+
"get_object",
|
| 143 |
+
Params={"Bucket": bucket, "Key": filekey},
|
| 144 |
+
ExpiresIn=int(expires),
|
| 145 |
+
)
|
| 146 |
+
except Exception: # noqa: BLE001 - URL is best-effort, never fatal
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
|
| 150 |
def upload_asset(
|
| 151 |
*,
|
| 152 |
namespace: str,
|