Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,178 +1,5 @@
|
|
| 1 |
import os, io, csv, time, json, hashlib, base64, zipfile, re
|
| 2 |
-
from typing import List,
|
| 3 |
-
|
| 4 |
-
import spaces
|
| 5 |
-
import gradio as gr
|
| 6 |
-
from PIL import Image
|
| 7 |
-
import torch
|
| 8 |
-
from transformers import LlavaForConditionalGeneration, AutoProcessor
|
| 9 |
-
|
| 10 |
-
# ────────────────────────────────────────────────────────
|
| 11 |
-
# Paths & caches
|
| 12 |
-
# ────────────────────────────────────────────────────────
|
| 13 |
-
os.environ.setdefault("HF_HOME", "/home/user/.cache/huggingface")
|
| 14 |
-
os.makedirs(os.environ["HF_HOME"], exist_ok=True)
|
| 15 |
-
|
| 16 |
-
APP_DIR = os.getcwd()
|
| 17 |
-
SESSION_FILE = "/tmp/session.json"
|
| 18 |
-
SETTINGS_FILE = "/tmp/cf_settings.json"
|
| 19 |
-
JOURNAL_FILE = "/tmp/cf_journal.json"
|
| 20 |
-
TXT_EXPORT_DIR = "/tmp/cf_txt"
|
| 21 |
-
THUMB_CACHE = os.path.expanduser("~/.cache/captionforge/thumbs")
|
| 22 |
-
os.makedirs(THUMB_CACHE, exist_ok=True)
|
| 23 |
-
os.makedirs(TXT_EXPORT_DIR, exist_ok=True)
|
| 24 |
-
|
| 25 |
-
# ────────────────────────────────────────────────────────
|
| 26 |
-
# Model setup
|
| 27 |
-
# ────────────────────────────────────────────────────────
|
| 28 |
-
MODEL_PATH = "fancyfeast/llama-joycaption-beta-one-hf-llava"
|
| 29 |
-
|
| 30 |
-
def _detect_gpu():
|
| 31 |
-
if torch.cuda.is_available():
|
| 32 |
-
p = torch.cuda.get_device_properties(0)
|
| 33 |
-
return "cuda", int(p.total_memory/(1024**3)), p.name
|
| 34 |
-
return "cpu", 0, "CPU"
|
| 35 |
-
|
| 36 |
-
BACKEND, VRAM_GB, GPU_NAME = _detect_gpu()
|
| 37 |
-
DTYPE = torch.bfloat16 if BACKEND == "cuda" else torch.float32
|
| 38 |
-
MAX_SIDE_CAP = 1024 if BACKEND == "cuda" else 640
|
| 39 |
-
DEVICE = "cuda" if BACKEND == "cuda" else "cpu"
|
| 40 |
-
|
| 41 |
-
processor = AutoProcessor.from_pretrained(MODEL_PATH)
|
| 42 |
-
model = LlavaForConditionalGeneration.from_pretrained(
|
| 43 |
-
MODEL_PATH,
|
| 44 |
-
torch_dtype=DTYPE,
|
| 45 |
-
low_cpu_mem_usage=True,
|
| 46 |
-
device_map=0 if BACKEND == "cuda" else "cpu",
|
| 47 |
-
)
|
| 48 |
-
model.eval()
|
| 49 |
-
|
| 50 |
-
print(f"[CaptionForge] Backend={BACKEND} GPU={GPU_NAME} VRAM={VRAM_GB}GB dtype={DTYPE}")
|
| 51 |
-
print(f"[CaptionForge] Gradio version: {gr.__version__}")
|
| 52 |
-
|
| 53 |
-
# ────────────────────────────────────────────────────────
|
| 54 |
-
# Instruction templates & options
|
| 55 |
-
# ────────────────────────────────────────────────────────
|
| 56 |
-
STYLE_OPTIONS = [
|
| 57 |
-
"Descriptive (short)", "Descriptive (long)",
|
| 58 |
-
"Character training (short)", "Character training (long)",
|
| 59 |
-
"LoRA (Flux_D Realism) (short)", "LoRA (Flux_D Realism) (long)",
|
| 60 |
-
"E-commerce product (short)", "E-commerce product (long)",
|
| 61 |
-
"Portrait (photography) (short)", "Portrait (photography) (long)",
|
| 62 |
-
"Landscape (photography) (short)", "Landscape (photography) (long)",
|
| 63 |
-
"Art analysis (no artist names) (short)", "Art analysis (no artist names) (long)",
|
| 64 |
-
"Social caption (short)", "Social caption (long)",
|
| 65 |
-
"Aesthetic tags (comma-sep)"
|
| 66 |
-
]
|
| 67 |
-
|
| 68 |
-
CAPTION_TYPE_MAP = {
|
| 69 |
-
"Descriptive (short)": "One sentence (≤25 words) describing the most important visible elements only. No speculation.",
|
| 70 |
-
"Descriptive (long)": "Write a detailed description for this image.",
|
| 71 |
-
|
| 72 |
-
"Character training (short)": (
|
| 73 |
-
"Output a concise, prompt-like caption for character LoRA/ID training. "
|
| 74 |
-
"Include visible character name {name} if provided, distinct physical traits, clothing, pose, camera/cinematic cues. "
|
| 75 |
-
"No backstory; no non-visible traits. Prefer comma-separated phrases."
|
| 76 |
-
),
|
| 77 |
-
"Character training (long)": (
|
| 78 |
-
"Write a thorough, training-ready caption for a character dataset. "
|
| 79 |
-
"Use {name} if provided; describe only what is visible: physique, face/hair, clothing, accessories, actions, pose, "
|
| 80 |
-
"camera angle/focal cues, lighting, background context. 1–3 sentences; no backstory or meta."
|
| 81 |
-
),
|
| 82 |
-
|
| 83 |
-
"Flux_D (short)": "Output a short Flux.Dev prompt that is indistinguishable from a real Flux.Dev prompt.",
|
| 84 |
-
"Flux_D (long)": "Output a long Flux.Dev prompt that is indistinguishable from a real Flux.Dev prompt.",
|
| 85 |
-
|
| 86 |
-
"Aesthetic tags (comma-sep)": "Return only comma-separated aesthetic tags capturing subject, medium, style, lighting, composition. No sentences.",
|
| 87 |
-
|
| 88 |
-
"E-commerce product (short)": "One sentence highlighting key attributes, material, color, use case. No fluff.",
|
| 89 |
-
"E-commerce product (long)": "Write a crisp product description highlighting key attributes, materials, color, usage, and distinguishing traits.",
|
| 90 |
-
|
| 91 |
-
"Portrait (photography) (short)": "One sentence portrait description: subject, pose/expression, camera angle, lighting, background.",
|
| 92 |
-
"Portrait (photography) (long)": "Describe a portrait: subject, age range, pose, facial expression, camera angle, focal length cues, lighting, background.",
|
| 93 |
-
|
| 94 |
-
"Landscape (photography) (short)": "One sentence landscape description: major elements, time of day, weather, vantage point, mood.",
|
| 95 |
-
"Landscape (photography) (long)": "Describe landscape elements, time of day, weather, vantage point, composition, and mood.",
|
| 96 |
-
|
| 97 |
-
"Art analysis (no artist names) (short)": "One sentence describing medium, style, composition, palette; do not mention artist/title.",
|
| 98 |
-
"Art analysis (no artist names) (long)": "Analyze the artwork's visible elements, medium, style, composition, palette. Do not mention artist names or titles.",
|
| 99 |
-
|
| 100 |
-
"Social caption (short)": "Write a short, catchy caption (max 25 words) describing the visible content. No hashtags.",
|
| 101 |
-
"Social caption (long)": "Write a slightly longer, engaging caption (≤50 words) describing the visible content. No hashtags."
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
EXTRA_CHOICES = [
|
| 105 |
-
"Do NOT include information about people/characters that cannot be changed (like ethnicity, gender, etc), but do still include changeable attributes (like hair style).",
|
| 106 |
-
"Do NOT include information about whether there is a watermark or not.",
|
| 107 |
-
"Do NOT use any ambiguous language.",
|
| 108 |
-
"ONLY describe the most important elements of the image.",
|
| 109 |
-
"Include information about the ages of any people/characters when applicable.",
|
| 110 |
-
"Explicitly specify the vantage height (eye-level, low-angle worm’s-eye, bird’s-eye, drone, rooftop, etc.).",
|
| 111 |
-
"Focus captions only on clothing/fashion details.",
|
| 112 |
-
"Focus on setting, scenery, and context; ignore subject details.",
|
| 113 |
-
"ONLY describe the subject’s pose, movement, or action. Do NOT mention appearance, clothing, or setting.",
|
| 114 |
-
"Do NOT include anything sexual; keep it PG.",
|
| 115 |
-
"Include synonyms/alternate phrasing to diversify training set.",
|
| 116 |
-
"ALWAYS arrange caption elements in the order → Subject, Clothing/Accessories, Action/Pose, Setting/Environment, Lighting/Camera/Style.",
|
| 117 |
-
"Do NOT mention the image's resolution.",
|
| 118 |
-
"Include information about depth, lighting, and camera angle.",
|
| 119 |
-
"Include information on composition (rule of thirds, symmetry, leading lines, etc).",
|
| 120 |
-
"Specify the depth of field and whether the background is in focus or blurred.",
|
| 121 |
-
"If applicable, mention the likely use of artificial or natural lighting sources.",
|
| 122 |
-
"If it is a work of art, do not include the artist's name or the title of the work.",
|
| 123 |
-
"Identify the image orientation (portrait, landscape, or square) if obvious.",
|
| 124 |
-
]
|
| 125 |
-
NAME_OPTION = "If there is a person/character in the image you must refer to them as {name}."
|
| 126 |
-
|
| 127 |
-
# ────────────────────────────────────────────────────────
|
| 128 |
-
# Helpers (hashing, thumbs, resize)
|
| 129 |
-
# ────────────────────────────────────────────────────────
|
| 130 |
-
def sha1_file(path: str) -> str:
|
| 131 |
-
h = hashlib.sha1()
|
| 132 |
-
with open(path, "rb") as f:
|
| 133 |
-
for chunk in iter(lambda: f.read(8192), b""):
|
| 134 |
-
h.update(chunk)
|
| 135 |
-
return h.hexdigest()
|
| 136 |
-
|
| 137 |
-
def thumb_path(sha1: str, max_side=96) -> str:
|
| 138 |
-
return os.path.join(THUMB_CACHE, f"{sha1}_{max_side}.jpg")
|
| 139 |
-
|
| 140 |
-
def ensure_thumb(path: str, sha1: str, max_side=96) -> str:
|
| 141 |
-
out = thumb_path(sha1, max_side)
|
| 142 |
-
if os.path.exists(out):
|
| 143 |
-
return out
|
| 144 |
-
try:
|
| 145 |
-
im = Image.open(path).convert("RGB")
|
| 146 |
-
except Exception:
|
| 147 |
-
return ""
|
| 148 |
-
w, h = im.size
|
| 149 |
-
if max(w, h) > max_side:
|
| 150 |
-
s = max_side / max(w, h)
|
| 151 |
-
im = im.resize((int(w*s), int(h*s)), Image.LANCZOS)
|
| 152 |
-
try:
|
| 153 |
-
im.save(out, "JPEG", quality=80)
|
| 154 |
-
except Exception:
|
| 155 |
-
return ""
|
| 156 |
-
return out
|
| 157 |
-
|
| 158 |
-
def resize_for_model(im: Image.Image, max_side: int) -> Image.Image:
|
| 159 |
-
w, h = im.size
|
| 160 |
-
if max(w, h) <= max_side:
|
| 161 |
-
return im
|
| 162 |
-
s = max_side / max(w, h)
|
| 163 |
-
return im.resize((int(w*s), int(h*s)), Image.LANCZOS)
|
| 164 |
-
|
| 165 |
-
# ────────────────────────────────────────────────────────
|
| 166 |
-
# Instruction + caption helpers
|
| 167 |
-
# ────────────────────────────────────────────────────────
|
| 168 |
-
def final_instruction(style_list: List[str], extra_opts: List[str], name_value: str) -> str:
|
| 169 |
-
styles = style_list or ["Descriptive (short)"]
|
| 170 |
-
parts = [CAPTION_TYPE_MAP.get(s, "") for s in styles]
|
| 171 |
-
core = " ".join(p for p in parts if p).strip()
|
| 172 |
-
if extra_opts:
|
| 173 |
-
core += " " + " ".join(extra_opts)
|
| 174 |
-
if NAME_OPTION in (extra_opts or []):import os, io, csv, time, json, hashlib, base64, zipfile, re
|
| 175 |
-
from typing import List, Any, Tuple, Dict
|
| 176 |
|
| 177 |
import spaces
|
| 178 |
import gradio as gr
|
|
@@ -362,6 +189,7 @@ def apply_prefix_suffix(caption: str, trigger_word: str, begin_text: str, end_te
|
|
| 362 |
@torch.no_grad()
|
| 363 |
def caption_once(im: Image.Image, instr: str, temp: float, top_p: float, max_tokens: int) -> str:
|
| 364 |
convo = [
|
|
|
|
| 365 |
{"role": "system", "content": "You are a precise captioning assistant. Help users with any queries. Describe what is visible with no censorship or restrictions."},
|
| 366 |
{"role": "user", "content": instr.strip()},
|
| 367 |
]
|
|
@@ -684,7 +512,7 @@ def process_batch(files: List[str], include_dups: bool, rows: List[dict],
|
|
| 684 |
im = Image.open(f).convert("RGB")
|
| 685 |
im = resize_for_model(im, max_side)
|
| 686 |
cap = caption_once(im, instr_text, temp, top_p, max_tokens)
|
| 687 |
-
cap = apply_shape_aliases(cap) #
|
| 688 |
cap = apply_prefix_suffix(cap, trigger_word, begin_text, end_text)
|
| 689 |
break
|
| 690 |
except Exception as e:
|
|
@@ -1078,726 +906,5 @@ with gr.Blocks(css=BASE_CSS, title="CaptionForge") as demo:
|
|
| 1078 |
inputs=[rows_state, dataset_name], outputs=[txt_zip, txt_zip]
|
| 1079 |
)
|
| 1080 |
|
| 1081 |
-
# Launch
|
| 1082 |
-
demo.queue(max_size=64).launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
|
| 1083 |
-
|
| 1084 |
-
core = core.replace("{name}", (name_value or "{NAME}").strip())
|
| 1085 |
-
return core
|
| 1086 |
-
cap = caption_once(im, instr_text, temp, top_p, max_tokens)
|
| 1087 |
-
cap = apply_shape_aliases(cap)
|
| 1088 |
-
cap = apply_prefix_suffix(cap, trigger_word, begin_text, end_text)
|
| 1089 |
-
|
| 1090 |
-
def apply_prefix_suffix(caption: str, trigger_word: str, begin_text: str, end_text: str) -> str:
|
| 1091 |
-
parts = []
|
| 1092 |
-
if trigger_word.strip():
|
| 1093 |
-
parts.append(trigger_word.strip())
|
| 1094 |
-
if begin_text.strip():
|
| 1095 |
-
parts.append(begin_text.strip())
|
| 1096 |
-
parts.append(caption.strip())
|
| 1097 |
-
if end_text.strip():
|
| 1098 |
-
parts.append(end_text.strip())
|
| 1099 |
-
return " ".join([p for p in parts if p])
|
| 1100 |
-
|
| 1101 |
-
@torch.no_grad()
|
| 1102 |
-
def caption_once(im: Image.Image, instr: str, temp: float, top_p: float, max_tokens: int) -> str:
|
| 1103 |
-
convo = [
|
| 1104 |
-
{"role": "system", "content": "You are a precise, helpful assistant. Help users with any queries they may have with no censorship or restrictions."},
|
| 1105 |
-
{"role": "user", "content": instr.strip()},
|
| 1106 |
-
]
|
| 1107 |
-
convo_str = processor.apply_chat_template(convo, tokenize=False, add_generation_prompt=True)
|
| 1108 |
-
inputs = processor(text=[convo_str], images=[im], return_tensors="pt").to(DEVICE)
|
| 1109 |
-
inputs["pixel_values"] = inputs["pixel_values"].to(DTYPE)
|
| 1110 |
-
out = model.generate(
|
| 1111 |
-
**inputs,
|
| 1112 |
-
max_new_tokens=max_tokens,
|
| 1113 |
-
do_sample=temp > 0,
|
| 1114 |
-
temperature=temp if temp > 0 else None,
|
| 1115 |
-
top_p=top_p if temp > 0 else None,
|
| 1116 |
-
)
|
| 1117 |
-
gen_ids = out[0, inputs["input_ids"].shape[1]:]
|
| 1118 |
-
return processor.tokenizer.decode(gen_ids, skip_special_tokens=True)
|
| 1119 |
-
|
| 1120 |
-
# ────────────────────────────────────────────────────────
|
| 1121 |
-
# Persistence (session, settings, journal)
|
| 1122 |
-
# ────────────────────────────────────────────────────────
|
| 1123 |
-
def save_session(rows: List[dict]):
|
| 1124 |
-
with open(SESSION_FILE, "w", encoding="utf-8") as f:
|
| 1125 |
-
json.dump(rows, f, ensure_ascii=False, indent=2)
|
| 1126 |
-
|
| 1127 |
-
def load_session() -> List[dict]:
|
| 1128 |
-
if os.path.exists(SESSION_FILE):
|
| 1129 |
-
with open(SESSION_FILE, "r", encoding="utf-8") as f:
|
| 1130 |
-
return json.load(f)
|
| 1131 |
-
return []
|
| 1132 |
-
|
| 1133 |
-
def save_settings(cfg: dict):
|
| 1134 |
-
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
|
| 1135 |
-
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
| 1136 |
-
|
| 1137 |
-
def load_settings() -> dict:
|
| 1138 |
-
if os.path.exists(SETTINGS_FILE):
|
| 1139 |
-
with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
|
| 1140 |
-
cfg = json.load(f)
|
| 1141 |
-
else:
|
| 1142 |
-
cfg = {}
|
| 1143 |
-
defaults = {
|
| 1144 |
-
"dataset_name": "captionforge",
|
| 1145 |
-
"temperature": 0.6,
|
| 1146 |
-
"top_p": 0.9,
|
| 1147 |
-
"max_tokens": 256,
|
| 1148 |
-
"chunk_mode": "Manual (step)",
|
| 1149 |
-
"chunk_size": 10,
|
| 1150 |
-
"max_side": min(896, MAX_SIDE_CAP),
|
| 1151 |
-
"styles": ["Character training (short)"],
|
| 1152 |
-
"extras": [],
|
| 1153 |
-
"name": "",
|
| 1154 |
-
"trigger": "",
|
| 1155 |
-
"begin": "",
|
| 1156 |
-
"end": "",
|
| 1157 |
-
}
|
| 1158 |
-
for k, v in defaults.items():
|
| 1159 |
-
cfg.setdefault(k, v)
|
| 1160 |
-
|
| 1161 |
-
# migrate legacy names
|
| 1162 |
-
legacy_map = {
|
| 1163 |
-
"Descriptive": "Descriptive (short)",
|
| 1164 |
-
"LoRA (Flux_D Realism)": "LoRA (Flux_D Realism) (short)",
|
| 1165 |
-
"Portrait (photography)": "Portrait (photography) (short)",
|
| 1166 |
-
"Landscape (photography)": "Landscape (photography) (short)",
|
| 1167 |
-
"Art analysis (no artist names)": "Art analysis (no artist names) (short)",
|
| 1168 |
-
"E-commerce product": "E-commerce product (short)",
|
| 1169 |
-
}
|
| 1170 |
-
styles = cfg.get("styles") or []
|
| 1171 |
-
migrated = []
|
| 1172 |
-
for s in styles if isinstance(styles, list) else [styles]:
|
| 1173 |
-
migrated.append(legacy_map.get(s, s))
|
| 1174 |
-
migrated = [s for s in migrated if s in STYLE_OPTIONS]
|
| 1175 |
-
if not migrated:
|
| 1176 |
-
migrated = ["Descriptive (short)"]
|
| 1177 |
-
cfg["styles"] = migrated
|
| 1178 |
-
return cfg
|
| 1179 |
-
|
| 1180 |
-
def save_journal(data: dict):
|
| 1181 |
-
with open(JOURNAL_FILE, "w", encoding="utf-8") as f:
|
| 1182 |
-
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 1183 |
-
|
| 1184 |
-
def load_journal() -> dict:
|
| 1185 |
-
if os.path.exists(JOURNAL_FILE):
|
| 1186 |
-
with open(JOURNAL_FILE, "r", encoding="utf-8") as f:
|
| 1187 |
-
return json.load(f)
|
| 1188 |
-
return {}
|
| 1189 |
-
|
| 1190 |
-
# ────────────────────────────────────────────────────────
|
| 1191 |
-
# Logo loader
|
| 1192 |
-
# ────────────────────────────────────────────────────────
|
| 1193 |
-
def logo_b64_img() -> str:
|
| 1194 |
-
candidates = [
|
| 1195 |
-
os.path.join(APP_DIR, "captionforge-logo.png"),
|
| 1196 |
-
"/home/user/app/captionforge-logo.png",
|
| 1197 |
-
"captionforge-logo.png",
|
| 1198 |
-
]
|
| 1199 |
-
for p in candidates:
|
| 1200 |
-
if os.path.exists(p):
|
| 1201 |
-
with open(p, "rb") as f:
|
| 1202 |
-
b64 = base64.b64encode(f.read()).decode("ascii")
|
| 1203 |
-
return f"<img src='data:image/png;base64,{b64}' alt='CaptionForge' class='cf-logo'>"
|
| 1204 |
-
return ""
|
| 1205 |
-
|
| 1206 |
-
# ────────────────────────────────────────────────────────
|
| 1207 |
-
# Import / Export helpers
|
| 1208 |
-
# ────────────────────────────────────────────────────────
|
| 1209 |
-
def sanitize_basename(s: str) -> str:
|
| 1210 |
-
s = (s or "").strip() or "captionforge"
|
| 1211 |
-
return re.sub(r"[^A-Za-z0-9._-]+", "_", s)[:120]
|
| 1212 |
-
|
| 1213 |
-
def ts_stamp() -> str:
|
| 1214 |
-
return time.strftime("%Y%m%d_%H%M%S")
|
| 1215 |
-
|
| 1216 |
-
def export_csv(rows: List[dict], basename: str) -> str:
|
| 1217 |
-
base = sanitize_basename(basename)
|
| 1218 |
-
out = f"/tmp/{base}_{ts_stamp()}.csv"
|
| 1219 |
-
with open(out, "w", newline="", encoding="utf-8") as f:
|
| 1220 |
-
w = csv.writer(f)
|
| 1221 |
-
w.writerow(["sha1", "filename", "caption"])
|
| 1222 |
-
for r in rows:
|
| 1223 |
-
w.writerow([r.get("sha1",""), r.get("filename",""), r.get("caption","")])
|
| 1224 |
-
return out
|
| 1225 |
-
|
| 1226 |
-
def export_excel(rows: List[dict], basename: str) -> str:
|
| 1227 |
-
try:
|
| 1228 |
-
from openpyxl import Workbook
|
| 1229 |
-
from openpyxl.drawing.image import Image as XLImage
|
| 1230 |
-
except Exception as e:
|
| 1231 |
-
raise RuntimeError("Excel export requires 'openpyxl' in requirements.txt.") from e
|
| 1232 |
-
base = sanitize_basename(basename)
|
| 1233 |
-
out = f"/tmp/{base}_{ts_stamp()}.xlsx"
|
| 1234 |
-
wb = Workbook(); ws = wb.active; ws.title = "CaptionForge"
|
| 1235 |
-
ws.append(["image", "filename", "caption"])
|
| 1236 |
-
ws.column_dimensions["A"].width = 24
|
| 1237 |
-
ws.column_dimensions["B"].width = 42
|
| 1238 |
-
ws.column_dimensions["C"].width = 100
|
| 1239 |
-
r_i = 2
|
| 1240 |
-
for r in rows:
|
| 1241 |
-
fn = r.get("filename",""); cap = r.get("caption","")
|
| 1242 |
-
ws.cell(row=r_i, column=2, value=fn)
|
| 1243 |
-
ws.cell(row=r_i, column=3, value=cap)
|
| 1244 |
-
img_path = r.get("thumb_path") or r.get("path")
|
| 1245 |
-
if img_path and os.path.exists(img_path):
|
| 1246 |
-
try:
|
| 1247 |
-
ximg = XLImage(img_path); ws.add_image(ximg, f"A{r_i}"); ws.row_dimensions[r_i].height = 110
|
| 1248 |
-
except Exception:
|
| 1249 |
-
pass
|
| 1250 |
-
r_i += 1
|
| 1251 |
-
wb.save(out); return out
|
| 1252 |
-
|
| 1253 |
-
def export_txt_zip(rows: List[dict], basename: str) -> str:
|
| 1254 |
-
base = sanitize_basename(basename)
|
| 1255 |
-
for name in os.listdir(TXT_EXPORT_DIR):
|
| 1256 |
-
try: os.remove(os.path.join(TXT_EXPORT_DIR, name))
|
| 1257 |
-
except Exception: pass
|
| 1258 |
-
used: Dict[str,int] = {}
|
| 1259 |
-
for r in rows:
|
| 1260 |
-
orig = r.get("filename") or r.get("sha1") or "item"
|
| 1261 |
-
stem = re.sub(r"\.[A-Za-z0-9]+$", "", orig)
|
| 1262 |
-
stem = sanitize_basename(stem)
|
| 1263 |
-
if stem in used:
|
| 1264 |
-
used[stem] += 1
|
| 1265 |
-
stem = f"{stem}_{used[stem]}"
|
| 1266 |
-
else:
|
| 1267 |
-
used[stem] = 0
|
| 1268 |
-
txt_path = os.path.join(TXT_EXPORT_DIR, f"{stem}.txt")
|
| 1269 |
-
with open(txt_path, "w", encoding="utf-8") as f:
|
| 1270 |
-
f.write(r.get("caption", ""))
|
| 1271 |
-
zpath = f"/tmp/{base}_{ts_stamp()}_txt.zip"
|
| 1272 |
-
with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED) as z:
|
| 1273 |
-
for name in os.listdir(TXT_EXPORT_DIR):
|
| 1274 |
-
if name.endswith(".txt"):
|
| 1275 |
-
z.write(os.path.join(TXT_EXPORT_DIR, name), arcname=name)
|
| 1276 |
-
return zpath
|
| 1277 |
-
|
| 1278 |
-
def import_csv_jsonl(path: str, rows: List[dict]) -> List[dict]:
|
| 1279 |
-
"""Merge captions from CSV/JSONL into current rows by sha1→filename, updating only 'caption'."""
|
| 1280 |
-
if not path or not os.path.exists(path):
|
| 1281 |
-
return rows or []
|
| 1282 |
-
rows = rows or []
|
| 1283 |
-
by_sha = {r.get("sha1"): r for r in rows if r.get("sha1")}
|
| 1284 |
-
by_fn = {r.get("filename"): r for r in rows if r.get("filename")}
|
| 1285 |
-
|
| 1286 |
-
def _apply(item: dict):
|
| 1287 |
-
if not isinstance(item, dict):
|
| 1288 |
-
return
|
| 1289 |
-
cap = (item.get("caption") or "").strip()
|
| 1290 |
-
if not cap:
|
| 1291 |
-
return
|
| 1292 |
-
sha = (item.get("sha1") or "").strip()
|
| 1293 |
-
fn = (item.get("filename") or "").strip()
|
| 1294 |
-
tgt = by_sha.get(sha) if sha else None
|
| 1295 |
-
if not tgt and fn:
|
| 1296 |
-
tgt = by_fn.get(fn)
|
| 1297 |
-
if tgt is not None:
|
| 1298 |
-
prev = tgt.get("caption", "")
|
| 1299 |
-
if cap != prev:
|
| 1300 |
-
tgt.setdefault("history", []).append(prev)
|
| 1301 |
-
tgt["caption"] = cap
|
| 1302 |
-
|
| 1303 |
-
ext = os.path.splitext(path)[1].lower()
|
| 1304 |
-
try:
|
| 1305 |
-
if ext == ".csv":
|
| 1306 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 1307 |
-
for row in csv.DictReader(f):
|
| 1308 |
-
_apply(row)
|
| 1309 |
-
elif ext in (".jsonl", ".ndjson"):
|
| 1310 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 1311 |
-
for line in f:
|
| 1312 |
-
line = line.strip()
|
| 1313 |
-
if not line:
|
| 1314 |
-
continue
|
| 1315 |
-
try:
|
| 1316 |
-
obj = json.loads(line)
|
| 1317 |
-
except Exception:
|
| 1318 |
-
continue
|
| 1319 |
-
_apply(obj)
|
| 1320 |
-
else:
|
| 1321 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 1322 |
-
data = json.load(f)
|
| 1323 |
-
if isinstance(data, list):
|
| 1324 |
-
for obj in data:
|
| 1325 |
-
_apply(obj)
|
| 1326 |
-
except Exception as e:
|
| 1327 |
-
print("[CaptionForge] Import merge failed:", e)
|
| 1328 |
-
save_session(rows)
|
| 1329 |
-
return rows
|
| 1330 |
-
|
| 1331 |
-
# ────────────────────────────────────────────────────────
|
| 1332 |
-
# Batching / processing
|
| 1333 |
-
# ────────────────────────────────────────────────────────
|
| 1334 |
-
def adaptive_defaults(vram_gb: int) -> Tuple[int, int]:
|
| 1335 |
-
if vram_gb >= 40: return 24, min(1024, MAX_SIDE_CAP)
|
| 1336 |
-
if vram_gb >= 24: return 16, min(1024, MAX_SIDE_CAP)
|
| 1337 |
-
if vram_gb >= 12: return 10, 896
|
| 1338 |
-
if vram_gb >= 8: return 6, 768
|
| 1339 |
-
return 4, 640
|
| 1340 |
-
|
| 1341 |
-
def warmup_if_needed(temp: float, top_p: float, max_tokens: int, max_side: int):
|
| 1342 |
-
try:
|
| 1343 |
-
im = Image.new("RGB", (min(128, max_side), min(128, max_side)), (127,127,127))
|
| 1344 |
-
_ = caption_once(im, "Warm up.", temp=0.0, top_p=top_p, max_tokens=min(16, max_tokens))
|
| 1345 |
-
except Exception as e:
|
| 1346 |
-
print("[CaptionForge] Warm-up skipped:", e)
|
| 1347 |
-
|
| 1348 |
-
@spaces.GPU()
|
| 1349 |
-
@torch.no_grad()
|
| 1350 |
-
def process_batch(files: List[str], include_dups: bool, rows: List[dict],
|
| 1351 |
-
instr_text: str, temp: float, top_p: float, max_tokens: int,
|
| 1352 |
-
trigger_word: str, begin_text: str, end_text: str,
|
| 1353 |
-
mode: str, chunk_size: int, max_side: int, step_once: bool,
|
| 1354 |
-
progress=gr.Progress(track_tqdm=True)) -> Tuple[List[dict], List[str], List[str]]:
|
| 1355 |
-
if torch.cuda.is_available():
|
| 1356 |
-
torch.cuda.empty_cache()
|
| 1357 |
-
files = files or []
|
| 1358 |
-
if not files: return rows, [], []
|
| 1359 |
-
|
| 1360 |
-
auto_mode = mode == "Auto"
|
| 1361 |
-
if auto_mode:
|
| 1362 |
-
chunk_size, max_side = adaptive_defaults(VRAM_GB)
|
| 1363 |
-
|
| 1364 |
-
sha_seen = {r.get("sha1") for r in rows}
|
| 1365 |
-
f_infos = []
|
| 1366 |
-
for f in files:
|
| 1367 |
-
try:
|
| 1368 |
-
s = sha1_file(f)
|
| 1369 |
-
except Exception:
|
| 1370 |
-
continue
|
| 1371 |
-
if (not include_dups) and (s in sha_seen):
|
| 1372 |
-
continue
|
| 1373 |
-
f_infos.append((f, os.path.basename(f), s))
|
| 1374 |
-
if not f_infos: return rows, [], []
|
| 1375 |
-
|
| 1376 |
-
warmup_if_needed(temp, top_p, max_tokens, max_side)
|
| 1377 |
-
|
| 1378 |
-
chunks = [f_infos[i:i+chunk_size] for i in range(0, len(f_infos), chunk_size)]
|
| 1379 |
-
if step_once:
|
| 1380 |
-
chunks = chunks[:1]
|
| 1381 |
-
|
| 1382 |
-
error_log: List[str] = []
|
| 1383 |
-
remaining: List[str] = []
|
| 1384 |
-
|
| 1385 |
-
for ci, chunk in enumerate(chunks):
|
| 1386 |
-
for (f, fname, sha) in progress.tqdm(chunk, desc=f"Chunk {ci+1}/{len(chunks)}"):
|
| 1387 |
-
thumb = ensure_thumb(f, sha, 96)
|
| 1388 |
-
attempt = 0
|
| 1389 |
-
cap = None
|
| 1390 |
-
while attempt < 2:
|
| 1391 |
-
attempt += 1
|
| 1392 |
-
try:
|
| 1393 |
-
im = Image.open(f).convert("RGB")
|
| 1394 |
-
im = resize_for_model(im, max_side)
|
| 1395 |
-
cap = caption_once(im, instr_text, temp, top_p, max_tokens)
|
| 1396 |
-
cap = apply_prefix_suffix(cap, trigger_word, begin_text, end_text)
|
| 1397 |
-
break
|
| 1398 |
-
except Exception as e:
|
| 1399 |
-
error_log.append(f"{fname} ({sha[:8]}): {type(e).__name__}: {e} [attempt {attempt}]")
|
| 1400 |
-
rows.append({
|
| 1401 |
-
"sha1": sha, "filename": fname, "path": f, "thumb_path": thumb,
|
| 1402 |
-
"caption": cap or "", "history": []
|
| 1403 |
-
})
|
| 1404 |
-
save_session(rows)
|
| 1405 |
-
|
| 1406 |
-
if step_once and len(f_infos) > len(chunks[0]):
|
| 1407 |
-
remaining = [fi[0] for fi in f_infos[len(chunks[0]):]]
|
| 1408 |
-
save_journal({"remaining_files": remaining})
|
| 1409 |
-
return rows, remaining, error_log
|
| 1410 |
-
|
| 1411 |
-
# ────────────────────────────────────────────────────────
|
| 1412 |
-
# UI
|
| 1413 |
-
# ────────────────────────────────────────────────────────
|
| 1414 |
-
BASE_CSS = """
|
| 1415 |
-
:root{--galleryW:50%;--tableW:50%;}
|
| 1416 |
-
.gradio-container{max-width:100%!important}
|
| 1417 |
-
.cf-hero{
|
| 1418 |
-
display:flex; align-items:center; justify-content:center; gap:16px;
|
| 1419 |
-
margin:4px 0 12px; text-align:center;
|
| 1420 |
-
}
|
| 1421 |
-
.cf-hero > div { text-align:center; }
|
| 1422 |
-
.cf-logo{height:calc(3.25rem + 3 * 1.1rem + 18px);width:auto;object-fit:contain}
|
| 1423 |
-
.cf-title{margin:0;font-size:3.25rem;line-height:1;letter-spacing:.2px}
|
| 1424 |
-
.cf-sub{margin:6px 0 0;font-size:1.1rem;color:#cfd3da}
|
| 1425 |
-
.cf-row{display:flex;gap:12px}
|
| 1426 |
-
.cf-col-gallery{flex:0 0 var(--galleryW)}
|
| 1427 |
-
.cf-col-table{flex:0 0 var(--tableW)}
|
| 1428 |
-
/* Shared scroll look */
|
| 1429 |
-
.cf-scroll{max-height:70vh; overflow-y:auto; border:1px solid #e6e6e6; border-radius:10px; padding:8px}
|
| 1430 |
-
/* Uniform sizes */
|
| 1431 |
-
#cfGal .grid > div { height: 96px; }
|
| 1432 |
-
/* Hide SHA1 in table (first column) */
|
| 1433 |
-
#cfTable table thead tr th:first-child { display:none; }
|
| 1434 |
-
#cfTable table tbody tr td:first-child { display:none; }
|
| 1435 |
-
"""
|
| 1436 |
-
|
| 1437 |
-
|
| 1438 |
-
with gr.Blocks(css=BASE_CSS, title="CaptionForge") as demo:
|
| 1439 |
-
settings = load_settings()
|
| 1440 |
-
settings["styles"] = [s for s in settings.get("styles", []) if s in STYLE_OPTIONS] or ["Character training (short)"]
|
| 1441 |
-
|
| 1442 |
-
header = gr.HTML(value="""
|
| 1443 |
-
<div class="cf-hero">
|
| 1444 |
-
%s
|
| 1445 |
-
<div>
|
| 1446 |
-
<h1 class="cf-title">CaptionForge</h1>
|
| 1447 |
-
<div class="cf-sub">Batch captioning</div>
|
| 1448 |
-
<div class="cf-sub">Scrollable editor & autosave</div>
|
| 1449 |
-
<div class="cf-sub">CSV / Excel / TXT export</div>
|
| 1450 |
-
</div>
|
| 1451 |
-
</div>
|
| 1452 |
-
<hr>
|
| 1453 |
-
""" % logo_b64_img())
|
| 1454 |
-
|
| 1455 |
-
with gr.Tabs():
|
| 1456 |
-
with gr.Tab("Captioner"):
|
| 1457 |
-
# (keep all your existing main controls here)
|
| 1458 |
-
...
|
| 1459 |
-
with gr.Tab("Shape Aliases"):
|
| 1460 |
-
enable_aliases = gr.Checkbox(label="Enable shape alias replacements", value=True)
|
| 1461 |
-
alias_table = gr.Dataframe(
|
| 1462 |
-
headers=["shape (literal token)", "name to insert"],
|
| 1463 |
-
datatype=["str","str"],
|
| 1464 |
-
row_count=(1, "dynamic"),
|
| 1465 |
-
wrap=True,
|
| 1466 |
-
interactive=True
|
| 1467 |
-
)
|
| 1468 |
-
save_aliases_btn = gr.Button("Save aliases")
|
| 1469 |
-
save_status = gr.Markdown()
|
| 1470 |
-
|
| 1471 |
-
init_rows, init_enabled = get_shape_alias_rows()
|
| 1472 |
-
enable_aliases.value = init_enabled
|
| 1473 |
-
alias_table.value = init_rows
|
| 1474 |
-
|
| 1475 |
-
save_aliases_btn.click(
|
| 1476 |
-
set_shape_alias_rows,
|
| 1477 |
-
inputs=[enable_aliases, alias_table],
|
| 1478 |
-
outputs=[save_status]
|
| 1479 |
-
)
|
| 1480 |
-
|
| 1481 |
-
# ===== Controls (top)
|
| 1482 |
-
with gr.Group():
|
| 1483 |
-
with gr.Row():
|
| 1484 |
-
with gr.Column(scale=2):
|
| 1485 |
-
style_checks = gr.CheckboxGroup(
|
| 1486 |
-
choices=STYLE_OPTIONS,
|
| 1487 |
-
value=settings.get("styles", ["Character training (short)"]),
|
| 1488 |
-
label="Caption style (choose one or combine)"
|
| 1489 |
-
)
|
| 1490 |
-
with gr.Accordion("Extra options", open=True):
|
| 1491 |
-
extra_opts = gr.CheckboxGroup(
|
| 1492 |
-
choices=[NAME_OPTION] + EXTRA_CHOICES,
|
| 1493 |
-
value=settings.get("extras", []),
|
| 1494 |
-
label=None
|
| 1495 |
-
)
|
| 1496 |
-
with gr.Accordion("Name & Prefix/Suffix", open=True):
|
| 1497 |
-
name_input = gr.Textbox(label="Person / Character Name", value=settings.get("name", ""))
|
| 1498 |
-
trig = gr.Textbox(label="Trigger word")
|
| 1499 |
-
add_start = gr.Textbox(label="Add text to start")
|
| 1500 |
-
add_end = gr.Textbox(label="Add text to end")
|
| 1501 |
-
|
| 1502 |
-
with gr.Column(scale=1):
|
| 1503 |
-
instruction_preview = gr.Textbox(label="Model Instructions", lines=14)
|
| 1504 |
-
dataset_name = gr.Textbox(label="Dataset name (used for export file titles)", value=settings.get("dataset_name", "captionforge"))
|
| 1505 |
-
|
| 1506 |
-
with gr.Row():
|
| 1507 |
-
chunk_mode = gr.Radio(
|
| 1508 |
-
choices=["Auto", "Manual (all at once)", "Manual (step)"],
|
| 1509 |
-
value=settings.get("chunk_mode", "Manual (step)"),
|
| 1510 |
-
label="Batch mode"
|
| 1511 |
-
)
|
| 1512 |
-
chunk_size = gr.Slider(1, 50, settings.get("chunk_size", 10), step=1, label="Chunk size")
|
| 1513 |
-
max_side = gr.Slider(256, MAX_SIDE_CAP, settings.get("max_side", min(896, MAX_SIDE_CAP)), step=32, label="Max side (resize)")
|
| 1514 |
-
|
| 1515 |
-
# Auto-refresh instruction & persist key controls
|
| 1516 |
-
def _refresh_instruction(styles, extra, name_value, trigv, begv, endv):
|
| 1517 |
-
instr = final_instruction(styles or ["Character training (short)"], extra or [], name_value)
|
| 1518 |
-
cfg = load_settings()
|
| 1519 |
-
cfg.update({
|
| 1520 |
-
"styles": styles or ["Character training (short)"],
|
| 1521 |
-
"extras": extra or [],
|
| 1522 |
-
"name": name_value,
|
| 1523 |
-
"trigger": trigv, "begin": begv, "end": endv
|
| 1524 |
-
})
|
| 1525 |
-
save_settings(cfg)
|
| 1526 |
-
return instr
|
| 1527 |
-
|
| 1528 |
-
for comp in [style_checks, extra_opts, name_input, trig, add_start, add_end]:
|
| 1529 |
-
comp.change(_refresh_instruction, inputs=[style_checks, extra_opts, name_input, trig, add_start, add_end], outputs=[instruction_preview])
|
| 1530 |
-
|
| 1531 |
-
# ===== File inputs & actions
|
| 1532 |
-
with gr.Accordion("Uploaded images", open=True) as uploads_acc:
|
| 1533 |
-
input_files = gr.File(label="Drop images", file_types=["image"], file_count="multiple", type="filepath")
|
| 1534 |
-
import_file = gr.File(label="Import CSV or JSONL (merge captions)")
|
| 1535 |
-
|
| 1536 |
-
with gr.Row():
|
| 1537 |
-
import_btn = gr.Button("Import → Merge")
|
| 1538 |
-
resume_btn = gr.Button("Resume last run", variant="secondary")
|
| 1539 |
-
run_button = gr.Button("Caption batch", variant="primary")
|
| 1540 |
-
|
| 1541 |
-
# Step-chunk controls
|
| 1542 |
-
step_panel = gr.Group(visible=False)
|
| 1543 |
-
with step_panel:
|
| 1544 |
-
step_msg = gr.Markdown("")
|
| 1545 |
-
step_next = gr.Button("Process next chunk")
|
| 1546 |
-
step_finish = gr.Button("Finish")
|
| 1547 |
-
|
| 1548 |
-
# States
|
| 1549 |
-
rows_state = gr.State(load_session())
|
| 1550 |
-
visible_count_state = gr.State(100)
|
| 1551 |
-
pending_files = gr.State([])
|
| 1552 |
-
remaining_state = gr.State([])
|
| 1553 |
-
autosave_md = gr.Markdown("Ready.")
|
| 1554 |
-
|
| 1555 |
-
# Error log
|
| 1556 |
-
with gr.Accordion("Error log", open=False):
|
| 1557 |
-
log_md = gr.Markdown("")
|
| 1558 |
-
|
| 1559 |
-
# Side-by-side gallery + table with shared scroll
|
| 1560 |
-
with gr.Row():
|
| 1561 |
-
with gr.Column(scale=1):
|
| 1562 |
-
gallery = gr.Gallery(
|
| 1563 |
-
label="Results (images)",
|
| 1564 |
-
show_label=True,
|
| 1565 |
-
columns=10, # 10 per row as requested
|
| 1566 |
-
height=520,
|
| 1567 |
-
elem_id="cfGal",
|
| 1568 |
-
elem_classes=["cf-scroll"]
|
| 1569 |
-
)
|
| 1570 |
-
with gr.Column(scale=1):
|
| 1571 |
-
table = gr.Dataframe(
|
| 1572 |
-
label="Editable captions (whole session)",
|
| 1573 |
-
value=[], # filled by _render
|
| 1574 |
-
headers=["sha1", "filename", "caption"], # include sha1, but it's hidden by CSS
|
| 1575 |
-
interactive=True,
|
| 1576 |
-
elem_id="cfTable",
|
| 1577 |
-
elem_classes=["cf-scroll"]
|
| 1578 |
-
)
|
| 1579 |
-
|
| 1580 |
-
# Exports aligned under buttons
|
| 1581 |
-
with gr.Row():
|
| 1582 |
-
with gr.Column():
|
| 1583 |
-
export_csv_btn = gr.Button("Export CSV")
|
| 1584 |
-
csv_file = gr.File(label="CSV file", visible=False)
|
| 1585 |
-
with gr.Column():
|
| 1586 |
-
export_xlsx_btn = gr.Button("Export Excel (.xlsx)")
|
| 1587 |
-
xlsx_file = gr.File(label="Excel file", visible=False)
|
| 1588 |
-
with gr.Column():
|
| 1589 |
-
export_txt_btn = gr.Button("Export captions as .txt (zip)")
|
| 1590 |
-
txt_zip = gr.File(label="TXT zip", visible=False)
|
| 1591 |
-
|
| 1592 |
-
# Render helpers
|
| 1593 |
-
def _render(rows, vis_n):
|
| 1594 |
-
rows = rows or []
|
| 1595 |
-
n = max(0, int(vis_n) if vis_n else 0)
|
| 1596 |
-
win = rows[:n]
|
| 1597 |
-
|
| 1598 |
-
# Gallery expects list of paths (Gradio 5 ok)
|
| 1599 |
-
gal = []
|
| 1600 |
-
for r in win:
|
| 1601 |
-
p = r.get("thumb_path") or r.get("path")
|
| 1602 |
-
if isinstance(p, str) and os.path.exists(p):
|
| 1603 |
-
gal.append(p)
|
| 1604 |
-
|
| 1605 |
-
# Dataframe rows (sha1 kept for mapping; hidden via CSS)
|
| 1606 |
-
df = [[str(r.get("sha1","")), str(r.get("filename","")), str(r.get("caption",""))] for r in win]
|
| 1607 |
-
info = f"Showing {len(win)} of {len(rows)}"
|
| 1608 |
-
return gal, df, info
|
| 1609 |
-
|
| 1610 |
-
# Initial loads
|
| 1611 |
-
demo.load(lambda s,e,n: final_instruction(s or ["Character training (short)"], e or [], n),
|
| 1612 |
-
inputs=[style_checks, extra_opts, name_input], outputs=[instruction_preview])
|
| 1613 |
-
demo.load(_render, inputs=[rows_state, visible_count_state], outputs=[gallery, table, autosave_md])
|
| 1614 |
-
|
| 1615 |
-
# Scroll sync (Gallery + Table)
|
| 1616 |
-
gr.HTML("""
|
| 1617 |
-
<script>
|
| 1618 |
-
(function () {
|
| 1619 |
-
function findGalleryScrollRoot() {
|
| 1620 |
-
const host = document.querySelector("#cfGal");
|
| 1621 |
-
if (!host) return null;
|
| 1622 |
-
return host.querySelector(".grid") || host.querySelector("[data-testid='gallery']") || host;
|
| 1623 |
-
}
|
| 1624 |
-
function findTableScrollRoot() {
|
| 1625 |
-
const host = document.querySelector("#cfTable");
|
| 1626 |
-
if (!host) return null;
|
| 1627 |
-
return host.querySelector(".wrap") ||
|
| 1628 |
-
host.querySelector(".dataframe-wrap") ||
|
| 1629 |
-
(host.querySelector("table") ? host.querySelector("table").parentElement : null) ||
|
| 1630 |
-
host;
|
| 1631 |
-
}
|
| 1632 |
-
function syncScroll(a, b) {
|
| 1633 |
-
if (!a || !b) return;
|
| 1634 |
-
let lock = false;
|
| 1635 |
-
const onScrollA = () => { if (lock) return; lock = true; b.scrollTop = a.scrollTop; lock = false; };
|
| 1636 |
-
const onScrollB = () => { if (lock) return; lock = true; a.scrollTop = b.scrollTop; lock = false; };
|
| 1637 |
-
a.addEventListener("scroll", onScrollA, { passive: true });
|
| 1638 |
-
b.addEventListener("scroll", onScrollB, { passive: true });
|
| 1639 |
-
}
|
| 1640 |
-
let tries = 0;
|
| 1641 |
-
const timer = setInterval(() => {
|
| 1642 |
-
tries++;
|
| 1643 |
-
const gal = findGalleryScrollRoot();
|
| 1644 |
-
const tab = findTableScrollRoot();
|
| 1645 |
-
if (gal && tab) {
|
| 1646 |
-
const H = Math.min(gal.clientHeight || 520, tab.clientHeight || 520);
|
| 1647 |
-
gal.style.maxHeight = H + "px";
|
| 1648 |
-
gal.style.overflowY = "auto";
|
| 1649 |
-
tab.style.maxHeight = H + "px";
|
| 1650 |
-
tab.style.overflowY = "auto";
|
| 1651 |
-
syncScroll(gal, tab);
|
| 1652 |
-
clearInterval(timer);
|
| 1653 |
-
}
|
| 1654 |
-
if (tries > 20) clearInterval(timer);
|
| 1655 |
-
}, 100);
|
| 1656 |
-
})();
|
| 1657 |
-
</script>
|
| 1658 |
-
""")
|
| 1659 |
-
|
| 1660 |
-
# Auto refresh instruction persists (already wired above)
|
| 1661 |
-
# Table edits -> save + rerender (map by sha1, which is hidden in UI)
|
| 1662 |
-
def on_table_edit(df_rows, rows):
|
| 1663 |
-
by_sha = {r.get("sha1"): r for r in (rows or [])}
|
| 1664 |
-
for row in (df_rows or []):
|
| 1665 |
-
if not row:
|
| 1666 |
-
continue
|
| 1667 |
-
sha = str(row[0]) if len(row) > 0 else ""
|
| 1668 |
-
tgt = by_sha.get(sha)
|
| 1669 |
-
if not tgt:
|
| 1670 |
-
continue
|
| 1671 |
-
prev = tgt.get("caption", "")
|
| 1672 |
-
newc = row[2] if len(row) > 2 else prev
|
| 1673 |
-
if newc != prev:
|
| 1674 |
-
hist = tgt.setdefault("history", [])
|
| 1675 |
-
hist.append(prev)
|
| 1676 |
-
tgt["caption"] = newc
|
| 1677 |
-
save_session(rows or [])
|
| 1678 |
-
return rows, f"Saved • {time.strftime('%H:%M:%S')}"
|
| 1679 |
-
table.change(on_table_edit, inputs=[table, rows_state], outputs=[rows_state, autosave_md])\
|
| 1680 |
-
.then(_render, inputs=[rows_state, visible_count_state], outputs=[gallery, table, autosave_md])
|
| 1681 |
-
|
| 1682 |
-
def _compile_shape_aliases_from_file():
|
| 1683 |
-
s = load_settings()
|
| 1684 |
-
if not s.get("shape_aliases_enabled", True):
|
| 1685 |
-
return []
|
| 1686 |
-
compiled = []
|
| 1687 |
-
for item in s.get("shape_aliases", []):
|
| 1688 |
-
shape = (item.get("shape") or "").strip()
|
| 1689 |
-
name = (item.get("name") or "").strip()
|
| 1690 |
-
if not shape or not name:
|
| 1691 |
-
continue
|
| 1692 |
-
pat = rf"\b{re.escape(shape)}(?:-?shaped)?\b"
|
| 1693 |
-
compiled.append((re.compile(pat, flags=re.I), name))
|
| 1694 |
-
return compiled
|
| 1695 |
-
|
| 1696 |
-
SHAPE_ALIASES = _compile_shape_aliases_from_file()
|
| 1697 |
-
|
| 1698 |
-
def apply_shape_aliases(caption: str) -> str:
|
| 1699 |
-
for pat, name in SHAPE_ALIASES:
|
| 1700 |
-
caption = pat.sub(f"({name})", caption)
|
| 1701 |
-
return caption
|
| 1702 |
-
|
| 1703 |
-
|
| 1704 |
-
# Import merge
|
| 1705 |
-
def do_import(file, rows):
|
| 1706 |
-
if not file:
|
| 1707 |
-
return rows, gr.update(value="No file"), rows
|
| 1708 |
-
rows = import_csv_jsonl(file.name, rows or [])
|
| 1709 |
-
return rows, gr.update(value=f"Merged from {os.path.basename(file.name)} at {time.strftime('%H:%M:%S')}"), rows
|
| 1710 |
-
import_btn.click(do_import, inputs=[import_file, rows_state], outputs=[rows_state, autosave_md, rows_state])\
|
| 1711 |
-
.then(_render, inputs=[rows_state, visible_count_state], outputs=[gallery, table, autosave_md])
|
| 1712 |
-
|
| 1713 |
-
# Prepare run (hash dupes)
|
| 1714 |
-
def prepare_run(files, rows, pending):
|
| 1715 |
-
files = files or []
|
| 1716 |
-
rows = rows or []
|
| 1717 |
-
pending = pending or []
|
| 1718 |
-
if not files:
|
| 1719 |
-
return [], gr.update(open=True), []
|
| 1720 |
-
existing = {r.get("sha1") for r in rows if r.get("sha1")}
|
| 1721 |
-
pending_sha = set()
|
| 1722 |
-
for f in pending:
|
| 1723 |
-
try:
|
| 1724 |
-
pending_sha.add(sha1_file(f))
|
| 1725 |
-
except Exception:
|
| 1726 |
-
pass
|
| 1727 |
-
keep = []
|
| 1728 |
-
for f in files:
|
| 1729 |
-
try:
|
| 1730 |
-
s = sha1_file(f)
|
| 1731 |
-
except Exception:
|
| 1732 |
-
continue
|
| 1733 |
-
if s not in existing and s not in pending_sha:
|
| 1734 |
-
keep.append(f)
|
| 1735 |
-
return keep, gr.update(open=False), keep
|
| 1736 |
-
input_files.change(prepare_run, inputs=[input_files, rows_state, pending_files], outputs=[pending_files, uploads_acc, pending_files])
|
| 1737 |
-
|
| 1738 |
-
# Step visibility helper (dynamic message)
|
| 1739 |
-
def _step_visibility(remain, mode):
|
| 1740 |
-
if (mode == "Manual (step)") and remain:
|
| 1741 |
-
return gr.update(visible=True), gr.update(value=f"{len(remain)} files remain. Process next chunk?")
|
| 1742 |
-
return gr.update(visible=False), gr.update(value="")
|
| 1743 |
-
|
| 1744 |
-
# Run helpers (use settings for gen params; sliders removed from UI)
|
| 1745 |
-
def _run_once(pending, rows, instr, trigv, begv, endv, mode, csize, mside):
|
| 1746 |
-
cfg = load_settings()
|
| 1747 |
-
t = cfg.get("temperature", 0.6)
|
| 1748 |
-
p = cfg.get("top_p", 0.9)
|
| 1749 |
-
m = cfg.get("max_tokens", 256)
|
| 1750 |
-
step_once = (mode == "Manual (step)")
|
| 1751 |
-
new_rows, remaining, errors = process_batch(
|
| 1752 |
-
pending, False, rows or [], instr, t, p, m, trigv, begv, endv,
|
| 1753 |
-
mode, int(csize), int(mside), step_once
|
| 1754 |
-
)
|
| 1755 |
-
log = "\n".join(errors) if errors else "(no errors)"
|
| 1756 |
-
return new_rows, remaining, log, f"Saved • {time.strftime('%H:%M:%S')}"
|
| 1757 |
-
|
| 1758 |
-
run_button.click(
|
| 1759 |
-
_run_once,
|
| 1760 |
-
inputs=[pending_files, rows_state, instruction_preview, trig, add_start, add_end, chunk_mode, chunk_size, max_side],
|
| 1761 |
-
outputs=[rows_state, remaining_state, log_md, autosave_md]
|
| 1762 |
-
).then(
|
| 1763 |
-
_render, inputs=[rows_state, visible_count_state], outputs=[gallery, table, autosave_md]
|
| 1764 |
-
).then(
|
| 1765 |
-
_step_visibility, inputs=[remaining_state, chunk_mode], outputs=[step_panel, step_msg]
|
| 1766 |
-
)
|
| 1767 |
-
|
| 1768 |
-
def _resume(rows):
|
| 1769 |
-
j = load_journal()
|
| 1770 |
-
rem = j.get("remaining_files", [])
|
| 1771 |
-
return rem, ("Nothing to resume" if not rem else f"Loaded {len(rem)} remaining")
|
| 1772 |
-
resume_btn.click(_resume, inputs=[rows_state], outputs=[pending_files, autosave_md])
|
| 1773 |
-
|
| 1774 |
-
def _step_next(remain, rows, instr, trigv, begv, endv, mode, csize, mside):
|
| 1775 |
-
return _run_once(remain, rows, instr, trigv, begv, endv, mode, csize, mside)
|
| 1776 |
-
step_next.click(
|
| 1777 |
-
_step_next,
|
| 1778 |
-
inputs=[remaining_state, rows_state, instruction_preview, trig, add_start, add_end, chunk_mode, chunk_size, max_side],
|
| 1779 |
-
outputs=[rows_state, remaining_state, log_md, autosave_md]
|
| 1780 |
-
).then(
|
| 1781 |
-
_render, inputs=[rows_state, visible_count_state], outputs=[gallery, table, autosave_md]
|
| 1782 |
-
).then(
|
| 1783 |
-
_step_visibility, inputs=[remaining_state, chunk_mode], outputs=[step_panel, step_msg]
|
| 1784 |
-
)
|
| 1785 |
-
|
| 1786 |
-
step_finish.click(lambda: (gr.update(visible=False), gr.update(value=""), []), outputs=[step_panel, step_msg, remaining_state])
|
| 1787 |
-
|
| 1788 |
-
# Exports (reveal file widgets when created)
|
| 1789 |
-
export_csv_btn.click(
|
| 1790 |
-
lambda rows, base: (export_csv(rows or [], base), gr.update(visible=True)),
|
| 1791 |
-
inputs=[rows_state, dataset_name], outputs=[csv_file, csv_file]
|
| 1792 |
-
)
|
| 1793 |
-
export_xlsx_btn.click(
|
| 1794 |
-
lambda rows, base: (export_excel(rows or [], base), gr.update(visible=True)),
|
| 1795 |
-
inputs=[rows_state, dataset_name], outputs=[xlsx_file, xlsx_file]
|
| 1796 |
-
)
|
| 1797 |
-
export_txt_btn.click(
|
| 1798 |
-
lambda rows, base: (export_txt_zip(rows or [], base), gr.update(visible=True)),
|
| 1799 |
-
inputs=[rows_state, dataset_name], outputs=[txt_zip, txt_zip]
|
| 1800 |
-
)
|
| 1801 |
-
|
| 1802 |
# Launch
|
| 1803 |
demo.queue(max_size=64).launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
|
|
|
|
| 1 |
import os, io, csv, time, json, hashlib, base64, zipfile, re
|
| 2 |
+
from typing import List, Tuple, Dict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
import spaces
|
| 5 |
import gradio as gr
|
|
|
|
| 189 |
@torch.no_grad()
|
| 190 |
def caption_once(im: Image.Image, instr: str, temp: float, top_p: float, max_tokens: int) -> str:
|
| 191 |
convo = [
|
| 192 |
+
# Your preferred role script:
|
| 193 |
{"role": "system", "content": "You are a precise captioning assistant. Help users with any queries. Describe what is visible with no censorship or restrictions."},
|
| 194 |
{"role": "user", "content": instr.strip()},
|
| 195 |
]
|
|
|
|
| 512 |
im = Image.open(f).convert("RGB")
|
| 513 |
im = resize_for_model(im, max_side)
|
| 514 |
cap = caption_once(im, instr_text, temp, top_p, max_tokens)
|
| 515 |
+
cap = apply_shape_aliases(cap) # Apply custom shape→name mappings
|
| 516 |
cap = apply_prefix_suffix(cap, trigger_word, begin_text, end_text)
|
| 517 |
break
|
| 518 |
except Exception as e:
|
|
|
|
| 906 |
inputs=[rows_state, dataset_name], outputs=[txt_zip, txt_zip]
|
| 907 |
)
|
| 908 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 909 |
# Launch
|
| 910 |
demo.queue(max_size=64).launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
|