File size: 8,759 Bytes
3c11df8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | """Two-stage prompt rewriter for LingBot-Video (example-side helper).
rewrite_prompt turns a brief idea into the structured JSON caption the DiT expects:
stage 1 expands the idea into a natural-language caption (base model), stage 2 maps it
into structured JSON (base model + stage-2 LoRA). This is a separate VLM + LoRA adapter,
not the DiT, and is not downloaded with the pipeline -- hence it lives with the examples.
TransformersBackend loads the rewriter VLM locally; make_backend also accepts any object
exposing generate(text, image, use_lora).
from examples.lingbot_video.model_training.scripts.prompt_rewriter import rewrite_prompt
caption = rewrite_prompt("a puppy running across a meadow", mode="t2v", duration=5)
video = pipe(prompt=caption, ...)
"""
import io
import json
import os
import re
# Optional deps: only needed for the local rewriter backend / image loading.
try:
import requests
except ImportError:
requests = None
try:
from PIL import Image
except ImportError:
Image = None
try:
from json_repair import repair_json
except ImportError:
repair_json = None
try:
from .system_prompts import (
VIDEO_STEP1_EXPAND, VIDEO_STEP2_MAP, IMAGE_STEP1_EXPAND, IMAGE_STEP2_MAP,
)
except ImportError:
from system_prompts import (
VIDEO_STEP1_EXPAND, VIDEO_STEP2_MAP, IMAGE_STEP1_EXPAND, IMAGE_STEP2_MAP,
)
# mode -> (step1 system prompt, step2 system prompt, feed image?, add duration?)
MODES = {
"t2v": dict(s1=VIDEO_STEP1_EXPAND, s2=VIDEO_STEP2_MAP, image=False, duration=True),
"ti2v": dict(s1=VIDEO_STEP1_EXPAND, s2=VIDEO_STEP2_MAP, image=True, duration=True),
"t2i": dict(s1=IMAGE_STEP1_EXPAND, s2=IMAGE_STEP2_MAP, image=False, duration=False),
}
def _has_cjk(s: str) -> bool:
return any("一" <= c <= "鿿" for c in s)
def load_image(src):
"""Load a first-frame image from a local path / http(s) URL / PIL.Image -> RGB PIL."""
if Image is None:
raise ImportError("loading a first-frame image requires the Pillow package.")
if isinstance(src, Image.Image):
return src.convert("RGB")
if isinstance(src, str) and re.match(r"^https?://", src):
if requests is None:
raise ImportError("fetching an image URL requires the requests package.")
return Image.open(io.BytesIO(requests.get(src, timeout=30).content)).convert("RGB")
return Image.open(src).convert("RGB")
def _step1_text(mode, prompt, dur):
sys = MODES[mode]["s1"]
if mode == "t2i":
return sys + "\n\nUser image prompt:\n" + prompt
dur_line = f"\n\n视频时长:{dur} 秒" if _has_cjk(prompt) else f"\n\nVideo Duration: {dur} seconds"
return sys + "\n\n" + prompt + dur_line
def _step2_text(mode, detailed, dur):
sys = MODES[mode]["s2"]
if mode == "t2i":
return sys + "\n\nDETAILED CAPTION:\n" + detailed
return (sys + f"\n\nVideo Duration: {dur} seconds\n\nDETAILED CAPTION:\n"
+ detailed + "\n\nOutput the JSON now.")
def parse_json(raw):
"""Parse the stage-2 output into a dict, or None. Strips any code fence, tries the
stdlib parser, then falls back to json_repair (if installed) for messy output."""
s = (raw or "").strip()
m = re.search(r"```(?:json)?\s*(\{.*\})\s*```", s, re.DOTALL)
if m:
s = m.group(1)
try:
obj = json.loads(s)
if isinstance(obj, dict):
return obj
except Exception:
pass
if repair_json is not None:
try:
obj = repair_json(s, return_objects=True)
return obj if isinstance(obj, dict) else None
except Exception:
return None
return None
def save_caption(result, duration, path):
"""Save as {"caption": <caption>, "duration": <seconds|null>} -- the prompt.json the
pipeline consumes. duration is integer seconds for T2V/TI2V, None for T2I."""
dur = int(round(duration)) if MODES[result["mode"]]["duration"] else None
with open(path, "w", encoding="utf-8") as f:
json.dump({"caption": result["json"], "duration": dur}, f, ensure_ascii=False, indent=2)
return path
class TransformersBackend:
"""Local rewriter VLM + LoRA adapter (peft): stage1 = base (adapter disabled),
stage2 = base + LoRA. Set base/adapter (or REWRITER_BASE_MODEL/REWRITER_ADAPTER)."""
def __init__(self, base=None, adapter=None, device="auto", max_new_tokens=6144):
import contextlib
import torch
from peft import PeftModel
from transformers import AutoModelForImageTextToText, AutoProcessor
self._contextlib = contextlib
self._torch = torch
base = base or os.environ.get("REWRITER_BASE_MODEL", "")
adapter = adapter or os.environ.get("REWRITER_ADAPTER", "")
if not base or not adapter:
raise ValueError(
"Set the rewriter base and adapter paths via base=/adapter= or the "
"REWRITER_BASE_MODEL / REWRITER_ADAPTER environment variables."
)
self.processor = AutoProcessor.from_pretrained(base, trust_remote_code=True)
model = AutoModelForImageTextToText.from_pretrained(
base, torch_dtype=torch.bfloat16, device_map=device, trust_remote_code=True)
self.model = PeftModel.from_pretrained(model, adapter).eval()
self.max_new_tokens = max_new_tokens
def generate(self, text, image, use_lora):
torch = self._torch
content = ([{"type": "image", "image": image}] if image is not None else []) \
+ [{"type": "text", "text": text}]
messages = [{"role": "user", "content": content}]
chat = self.processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True, enable_thinking=False)
inputs = self.processor(
text=[chat], images=([image] if image is not None else None), return_tensors="pt"
).to(self.model.device)
# stage1 (expand): disable LoRA; stage2 (map): keep LoRA active.
adapter_ctx = self._contextlib.nullcontext() if use_lora else self.model.disable_adapter()
with torch.no_grad(), adapter_ctx:
out = self.model.generate(**inputs, max_new_tokens=self.max_new_tokens, do_sample=False)
gen = out[:, inputs["input_ids"].shape[1]:]
return self.processor.batch_decode(gen, skip_special_tokens=True)[0]
def make_backend(backend="transformers", base=None, adapter=None):
"""Build a rewriter backend: "transformers" for the local TransformersBackend, or any
object already exposing generate(text, image, use_lora) -> str (e.g. a hosted endpoint)."""
if backend == "transformers":
return TransformersBackend(base, adapter)
if hasattr(backend, "generate"):
return backend
raise ValueError(
f"unknown backend: {backend!r}; pass 'transformers' or an object exposing "
"generate(text, image, use_lora)."
)
class Rewriter:
"""Two-stage orchestrator. The backend implements ``generate(text, image, use_lora) -> str``."""
def __init__(self, backend):
self.backend = backend
def rewrite(self, prompt, mode="t2v", first_frame=None, duration=5):
if mode not in MODES:
raise ValueError(f"mode must be one of {list(MODES)}")
cfg = MODES[mode]
dur = int(round(duration))
img = None
if cfg["image"]:
if first_frame is None:
raise ValueError(f"{mode} requires first_frame (path / URL / PIL.Image)")
img = load_image(first_frame)
# stage 1: EXPAND -- base model (no LoRA)
detailed = self.backend.generate(_step1_text(mode, prompt, dur), img, use_lora=False).strip()
# stage 2: MAP -- base + LoRA
raw = self.backend.generate(_step2_text(mode, detailed, dur), img, use_lora=True).strip()
return {"mode": mode, "detailed": detailed, "json": parse_json(raw), "json_raw": raw}
def rewrite_prompt(prompt, mode="t2v", first_frame=None, duration=5,
backend="transformers", base=None, adapter=None, return_result=False):
"""Rewrite a brief idea into the structured caption string the pipeline expects.
Returns the compact-JSON caption string (ready to pass as ``pipe(prompt=...)``).
Pass ``return_result=True`` to also get the full stage-1/stage-2 dict.
"""
rw = Rewriter(make_backend(backend, base, adapter))
result = rw.rewrite(prompt, mode=mode, first_frame=first_frame, duration=duration)
if result["json"] is not None:
caption = json.dumps(result["json"], ensure_ascii=False, separators=(",", ":"))
else:
caption = result["json_raw"]
if return_result:
return caption, result
return caption
|