| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import os |
| import re |
| from typing import Dict, List, Optional |
|
|
| import torch |
| from PIL import Image, ImageOps |
| import gradio as gr |
| from transformers import ( |
| BlipForConditionalGeneration, |
| BlipProcessor, |
| AutoTokenizer, |
| GenerationConfig, |
| __version__ as TF_VER, |
| ) |
|
|
| |
| |
| |
|
|
| os.environ["TOKENIZERS_PARALLELISM"] = "false" |
|
|
| |
| |
| MODEL_ID = os.getenv("MODEL_ID_OR_PATH", "YaekobB/blip-caption-model") |
|
|
| |
| HF_TOKEN = os.getenv("HF_TOKEN", None) |
|
|
| device = torch.device("cpu") |
| |
| torch.set_num_threads(max(1, (os.cpu_count() or 2) - 1)) |
|
|
| print("🔧 torch:", torch.__version__, "| transformers:", TF_VER) |
| print("🔄 Loading model from:", MODEL_ID) |
|
|
| |
| |
| |
|
|
| model = BlipForConditionalGeneration.from_pretrained(MODEL_ID, token=HF_TOKEN) |
| try: |
| model.generation_config = GenerationConfig.from_pretrained(MODEL_ID, token=HF_TOKEN) |
| except Exception: |
| pass |
|
|
| |
| |
| try: |
| processor = BlipProcessor.from_pretrained(MODEL_ID, use_fast=False, token=HF_TOKEN) |
| print("ℹ️ Processor:", MODEL_ID) |
| except Exception as e: |
| |
| print("⚠️ Processor load from model repo failed; reason:", e) |
| processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base", use_fast=False) |
| print("ℹ️ Processor fallback:", "Salesforce/blip-image-captioning-base") |
|
|
| |
| processor.image_processor.size = {"height": 224, "width": 224} |
| if hasattr(processor.image_processor, "do_center_crop"): |
| processor.image_processor.do_center_crop = True |
| if hasattr(processor.image_processor, "crop_size"): |
| processor.image_processor.crop_size = {"height": 224, "width": 224} |
|
|
| |
| |
| |
|
|
| def _lm_head_rows(m: BlipForConditionalGeneration) -> int: |
| try: |
| return m.text_decoder.cls.predictions.decoder.weight.shape[0] |
| except Exception: |
| return m.get_input_embeddings().weight.shape[0] |
|
|
| lm_rows = _lm_head_rows(model) |
|
|
| def _load_tokenizer(model_head_rows: int): |
| |
| try: |
| tok = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN) |
| print("ℹ️ Tokenizer:", MODEL_ID) |
| except Exception as e: |
| print("⚠️ Tokenizer load from model repo failed; reason:", e) |
| tok = AutoTokenizer.from_pretrained("Salesforce/blip-image-captioning-base") |
|
|
| extra_tokens: List[str] = [] |
| if len(tok) < model_head_rows: |
| need = model_head_rows - len(tok) |
| extra_tokens = [f"<extra_tok_{i}>" for i in range(need)] |
| tok.add_tokens(extra_tokens) |
| tok.add_special_tokens({"additional_special_tokens": extra_tokens}) |
| print(f"ℹ️ Added {len(extra_tokens)} dummy *special* tokens to match LM head.") |
| return tok, extra_tokens |
|
|
| tokenizer, EXTRA_TOKENS = _load_tokenizer(lm_rows) |
|
|
| |
| if tokenizer.pad_token is None: |
| tokenizer.pad_token = tokenizer.eos_token |
| tokenizer.padding_side = "left" |
| model.config.pad_token_id = tokenizer.pad_token_id |
| model.generation_config.pad_token_id = tokenizer.pad_token_id |
| if tokenizer.eos_token_id is not None: |
| model.generation_config.eos_token_id = tokenizer.eos_token_id |
|
|
| processor.tokenizer = tokenizer |
|
|
| |
| BAD_WORDS_IDS: Optional[List[List[int]]] = None |
| if EXTRA_TOKENS: |
| bad = [tokenizer.convert_tokens_to_ids(t) for t in EXTRA_TOKENS] |
| BAD_WORDS_IDS = [[i] for i in bad if i is not None] |
|
|
| def _strip_extra_tokens(text: str) -> str: |
| if not EXTRA_TOKENS: |
| return text |
| return re.sub(r"\s*<extra_tok_\d+>\s*", " ", text).strip() |
|
|
| model.to(device).eval() |
| print("✅ Ready. Device:", device) |
| print("📏 Inference image size:", getattr(processor.image_processor, "size", None)) |
| print(f"🧪 vocab check -> lm_head rows: {lm_rows} | tokenizer size: {len(tokenizer)}") |
|
|
| |
| |
| |
|
|
| BASE_ARGS = dict( |
| min_length=5, |
| no_repeat_ngram_size=2, |
| early_stopping=True, |
| do_sample=False, |
| ) |
|
|
| PRESETS: Dict[str, Dict] = { |
| "Fast (CPU)": dict(num_beams=1, max_length=22, length_penalty=1.0, **BASE_ARGS), |
| "Balanced": dict(num_beams=2, max_length=28, length_penalty=1.0, **BASE_ARGS), |
| "Quality": dict(num_beams=4, max_length=32, length_penalty=1.05, **BASE_ARGS), |
| } |
| DEFAULT_PRESET = "Fast (CPU)" |
|
|
| |
| |
| |
|
|
| def _open_rgb_fast(path: str) -> Image.Image: |
| img = Image.open(path) |
| img = ImageOps.exif_transpose(img) |
| img.thumbnail((768, 768), Image.BILINEAR) |
| return img.convert("RGB") |
|
|
| |
| |
| |
|
|
| @torch.no_grad() |
| def caption_image(path: str, preset: str, beams: int, maxlen: int, lenpen: float, progress=gr.Progress(track_tqdm=True)) -> str: |
| |
| p = PRESETS[preset] |
| beams = int(beams or p["num_beams"]) |
| maxlen = int(maxlen or p["max_length"]) |
| lenpen = float(lenpen or p["length_penalty"]) |
|
|
| img = _open_rgb_fast(path) |
| batch = processor(images=img, return_tensors="pt").to(device) |
|
|
| gen_kwargs = dict( |
| num_beams=beams, |
| max_length=maxlen, |
| length_penalty=lenpen, |
| **BASE_ARGS, |
| ) |
| if BAD_WORDS_IDS is not None: |
| gen_kwargs["bad_words_ids"] = BAD_WORDS_IDS |
|
|
| ids = model.generate(pixel_values=batch["pixel_values"], **gen_kwargs) |
| text = tokenizer.decode(ids[0], skip_special_tokens=True) |
| return _strip_extra_tokens(text) |
|
|
| def run_batch(paths: List[str], preset: str, beams: int, maxlen: int, lenpen: float, progress=gr.Progress(track_tqdm=True)): |
| progress(0, desc="Preparing images") |
| results = [] |
| gallery = [] |
| n = len(paths or []) |
| for i, p in enumerate(paths or []): |
| cap = caption_image(p, preset, beams, maxlen, lenpen) |
| results.append((p, cap)) |
| gallery.append((p, cap)) |
| progress((i + 1) / max(1, n), desc=f"Captioned {i+1}/{n}") |
| return gallery |
|
|
| |
| |
| |
|
|
| from gradio.themes.base import Base |
| from gradio.themes.utils import colors |
|
|
| THEME = Base(primary_hue=colors.green, secondary_hue=colors.gray) |
|
|
| CUSTOM_CSS = """ |
| h1, h2, h3 { color: #228B22 !important; } /* forest green headings */ |
| #gallery .grid-wrap .label { |
| background: rgba(255,255,255,0.92); |
| border-radius: 10px; padding: 6px 10px; |
| font-size: 0.95rem; line-height: 1.25rem; |
| } |
| """ |
|
|
| with gr.Blocks(title="Multimodal Image Captioning with BLIP", theme=THEME, css=CUSTOM_CSS, fill_height=True) as demo: |
| gr.Markdown( |
| """ |
| # 🖼️ Multimodal Image Captioning with BLIP |
| Upload one or many images and get captions from a fine-tuned BLIP model. |
| """.strip() |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(scale=6): |
| uploader = gr.File( |
| label="Upload image(s)", |
| file_types=[".jpg", ".jpeg", ".png", ".bmp", ".webp"], |
| file_count="multiple", |
| type="filepath", |
| ) |
| gr.Markdown("_Supported: JPG, PNG, WebP, BMP. Inference at 224×224 (center-crop)._") |
| with gr.Column(scale=4): |
| preset = gr.Radio(choices=list(PRESETS.keys()), value=DEFAULT_PRESET, label="Preset") |
| with gr.Accordion("Advanced (optional)", open=False): |
| beams = gr.Slider(1, 6, value=PRESETS[DEFAULT_PRESET]["num_beams"], step=1, label="num_beams") |
| maxlen = gr.Slider(16, 48, value=PRESETS[DEFAULT_PRESET]["max_length"], step=1, label="max_length") |
| lenpen = gr.Slider(0.8, 1.3, value=PRESETS[DEFAULT_PRESET]["length_penalty"], step=0.05, label="length_penalty") |
| run = gr.Button("🚀 Generate Captions", variant="primary") |
|
|
| gallery = gr.Gallery(label="Results (image + caption below)", elem_id="gallery", columns=2, height="auto") |
|
|
| def _dispatch(files, preset, beams, maxlen, lenpen): |
| return run_batch(files, preset, beams, maxlen, lenpen) |
|
|
| run.click(_dispatch, inputs=[uploader, preset, beams, maxlen, lenpen], outputs=[gallery]) |
|
|
| |
| |
| demo.queue(default_concurrency_limit=1, max_size=16).launch() |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|