Ver código fonte

changes for the BLIP model to get the image recognization text

VISHAL BHANUSHALI 2 semanas atrás
pai
commit
4bb378fda6

+ 1 - 0
BLIP

@@ -0,0 +1 @@
+Subproject commit 3a29b7410476bf5f2ba0955827390eb6ea1f4f9d

+ 12 - 1
README.md

@@ -3,4 +3,15 @@
 content_quality_tool
 
 pip install cloudscraper
-pip install einops kornia timm
+pip install einops kornia timm
+
+bg-remover
+transformer==4.57.0
+
+for the blip
+torch==2.0.1
+torchvision==0.15.2
+transformers==4.26.1
+timm
+fairscale
+pillow

+ 102 - 0
bg_remover/blip_service.py

@@ -0,0 +1,102 @@
+# import torch
+# from PIL import Image
+# from torchvision import transforms
+
+# from BLIP.models.blip import blip_decoder
+# import os
+
+# class BLIPService:
+#     _model = None
+#     _device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+#     def __init__(self):
+#         if BLIPService._model is None:
+#             BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+#             config_path = os.path.join(BASE_DIR, "../BLIP/configs/med_config.json")
+#             checkpoint_path = os.path.join(BASE_DIR, "../BLIP/models/model_base_caption.pth")
+#             model_url = 'https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_retrieval_coco.pth'
+
+#             if not os.path.exists(config_path):
+#                 raise FileNotFoundError(f"BLIP config not found at {config_path}")
+#             # if not os.path.exists(checkpoint_path):
+#             #     raise FileNotFoundError(f"BLIP checkpoint not found at {checkpoint_path}")
+
+#             BLIPService._model = blip_decoder(
+#                 # pretrained=checkpoint_path,
+#                 med_config=config_path,
+#                 pretrained=model_url,
+#                 image_size=384,
+#                 vit="base"
+#             )
+#             BLIPService._model.eval()
+#             BLIPService._model.to(self._device)
+
+#         self.transform = transforms.Compose([
+#             transforms.Resize((384, 384)),
+#             transforms.ToTensor()
+#         ])
+
+#     def generate_caption(self, image: Image.Image):
+#         model = BLIPService._model
+#         model.eval()
+
+#         import torch
+#         from torchvision import transforms
+
+#         transform = transforms.Compose([
+#             transforms.Resize((384, 384)),
+#             transforms.ToTensor(),
+#             transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
+#         ])
+#         image_tensor = transform(image).unsqueeze(0)
+
+#         device = "cuda" if torch.cuda.is_available() else "cpu"
+#         image_tensor = image_tensor.to(device)
+#         model.to(device)
+
+#         with torch.no_grad():
+#             caption = model.generate(image_tensor, sample=False, num_beams=3, max_length=20, min_length=5)
+#         return caption[0]
+    
+#     # def generate_caption(self, image: Image.Image) -> str:
+#     #     image = image.convert("RGB")
+#     #     image = self.transform(image).unsqueeze(0).to(self._device)
+
+#     #     with torch.no_grad():
+#     #         caption = self._model.generate(
+#     #             image,
+#     #             sample=False,
+#     #             num_beams=3
+#     #         )
+
+#     #     return caption[0]
+
+
+# blip_service.py
+from PIL import Image
+import torch
+from transformers import BlipProcessor, BlipForConditionalGeneration
+
+class BLIPServiceHF:
+    _instance = None
+
+    def __new__(cls, *args, **kwargs):
+        # Singleton pattern to avoid reloading the model each time
+        if cls._instance is None:
+            cls._instance = super(BLIPServiceHF, cls).__new__(cls)
+            cls._instance._init_model()
+        return cls._instance
+
+    def _init_model(self):
+        self.processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
+        self.model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base")
+        self.model.eval()
+        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+        self.model.to(self.device)
+
+    def generate_caption(self, image: Image.Image) -> str:
+        inputs = self.processor(image, return_tensors="pt").to(self.device)
+        with torch.no_grad():
+            out = self.model.generate(**inputs)
+        caption = self.processor.decode(out[0], skip_special_tokens=True)
+        return caption

+ 36 - 0
bg_remover/blip_service_itm.py

@@ -0,0 +1,36 @@
+import torch
+from PIL import Image
+from torchvision import transforms
+from torchvision.transforms.functional import InterpolationMode
+from BLIP.models.blip import blip_decoder
+
+class BLIPDecoderService:
+    _instance = None
+    _model = None
+
+    def __new__(cls):
+        if cls._instance is None:
+            cls._instance = super(BLIPDecoderService, cls).__new__(cls)
+            cls.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
+            cls.image_size = 384
+            
+            # Load Model
+            model_url = 'https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_capfilt_large.pth'
+            cls._model = blip_decoder(pretrained=model_url, image_size=cls.image_size, vit='base')
+            cls._model.eval()
+            cls._model = cls._model.to(cls.device)
+            
+            # Preprocess
+            cls.transform = transforms.Compose([
+                transforms.Resize((cls.image_size, cls.image_size), interpolation=InterpolationMode.BICUBIC),
+                transforms.ToTensor(),
+                transforms.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711))
+            ])
+        return cls._instance
+
+    def generate_caption(self, pil_image):
+        image = self.transform(pil_image).unsqueeze(0).to(self.device)
+        with torch.no_grad():
+            # Beam search as per your requirement
+            caption = self._model.generate(image, sample=False, num_beams=3, max_length=20, min_length=5)
+            return caption[0]

+ 18 - 0
bg_remover/migrations/0002_backgroundtask_task_type.py

@@ -0,0 +1,18 @@
+# Generated by Django 5.2.7 on 2026-01-14 13:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('bg_remover', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='backgroundtask',
+            name='task_type',
+            field=models.CharField(choices=[('BG_REMOVE', 'Background Removal'), ('CAPTION', 'Image Captioning')], default='BG_REMOVE', max_length=20),
+        ),
+    ]

+ 15 - 1
bg_remover/models.py

@@ -10,11 +10,25 @@ class BackgroundTask(models.Model):
         ('COMPLETED', 'Completed'),
         ('FAILED', 'Failed'),
     ]
+
+    TASK_TYPE_CHOICES = [
+        ('BG_REMOVE', 'Background Removal'),
+        ('CAPTION', 'Image Captioning'),
+        # future:
+        # ('UPSCALE', 'Image Upscaling'),
+        # ('VQA', 'Visual Q&A'),
+    ]
+
     task_id = models.UUIDField(unique=True, editable=False)
+    task_type = models.CharField(
+        max_length=20,
+        choices=TASK_TYPE_CHOICES,
+        default='BG_REMOVE'
+    )
     status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
     zip_file = models.FileField(upload_to='bulk_results/', null=True, blank=True)
     created_at = models.DateTimeField(auto_now_add=True)
     error_message = models.TextField(null=True, blank=True)
 
     def __str__(self):
-        return f"Task {self.task_id} - {self.status}"
+        return f"Task {self.task_type} - {self.task_id} - {self.status}"

+ 80 - 2
bg_remover/templates/bg_remover_history.html

@@ -97,7 +97,7 @@
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
     <script src="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/browser/overlayscrollbars.browser.es6.min.js"></script>
     <script src="{% static './js/adminlte.js' %}"></script>
-    <script>
+    <!-- <script>
         async function fetchHistory() {
             try {
                 // Fetching from the API we built earlier
@@ -160,7 +160,85 @@
         // Auto-refresh every 5 seconds to update "Processing" tasks
         fetchHistory();
         // setInterval(fetchHistory, 5000);
-    </script>
+    </script> -->
+    <script>
+async function fetchHistory() {
+    try {
+        // Fetch tasks from your API (both BG and Caption)
+        const res = await fetch('{% url "bg_tasks_json" %}'); 
+        const data = await res.json();
+        const body = document.getElementById('history-table-body');
+        
+        if (data.length === 0) {
+            body.innerHTML = `
+                <tr>
+                    <td colspan="4" class="text-center py-20">
+                        <i class="bi bi-inbox text-4xl text-gray-200 mb-3 block"></i>
+                        <p class="text-gray-400 font-bold">No tasks found in your history.</p>
+                    </td>
+                </tr>`;
+            return;
+        }
+
+        body.innerHTML = data.map(t => {
+            let badgeStyles = "";
+            let statusLabel = t.status;
+            
+            // Set badge colors
+            if(t.status === 'COMPLETED') {
+                badgeStyles = "bg-green-100 text-green-700 border border-green-200";
+            } else if(t.status === 'PROCESSING') {
+                badgeStyles = "bg-blue-100 text-blue-700 border border-blue-200 animate-pulse";
+                statusLabel = "In Progress";
+            } else {
+                badgeStyles = "bg-red-100 text-red-700 border border-red-200";
+            }
+            console.log("t.task_type",t.task_type);
+            // Detect task type (Background or Caption)
+            const taskTypeLabel = t.task_type === "CAPTION" ? "AI Image Caption" : "Background Removal";
+
+            // If Caption task, show sample caption or note
+            const extraInfo = t.task_type === "CAPTION" && t.results?.length > 0 
+                ? `<div class="text-xs text-gray-600 mt-1">
+                        Example: "${t.results[0].caption || t.results[0].score}"
+                   </div>` 
+                : "";
+
+            return `
+            <tr class="transition-colors hover:bg-gray-50/50">
+                <td class="px-6 py-4">
+                    <div class="text-sm font-bold text-gray-800">${t.date}</div>
+                </td>
+                <td class="px-6 py-4">
+                    <div class="text-xs font-mono text-gray-400 tracking-tighter">${t.task_id}</div>
+                    <div class="text-xs font-bold text-gray-500 mt-1">${taskTypeLabel}</div>
+                </td>
+                <td class="px-6 py-4 text-center">
+                    <span class="status-badge ${badgeStyles}">
+                        ${statusLabel}
+                    </span>
+                    ${extraInfo}
+                </td>
+                <td class="px-6 py-4 text-end">
+                    ${t.url ? 
+                        `<a href="${t.url}" class="inline-flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-xl text-xs font-bold hover:bg-slate-800 transition-all">
+                            <i class="bi bi-download"></i> Download ZIP
+                        </a>` : 
+                        `<span class="text-[11px] font-black text-gray-300 uppercase italic">Working...</span>`
+                    }
+                </td>
+            </tr>`;
+        }).join('');
+    } catch (err) {
+        console.error("History fetch error:", err);
+    }
+}
+
+// Initial fetch and optional auto-refresh
+fetchHistory();
+// setInterval(fetchHistory, 5000);
+</script>
+
     <script>
         const SELECTOR_SIDEBAR_WRAPPER = ".sidebar-wrapper";
         const Default = {

+ 398 - 264
bg_remover/templates/bg_remover_index.html

@@ -9,331 +9,465 @@
 <script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3@5.0.12/index.css"
         integrity="sha256-tXJfXfp6Ewt1ilPzLDtQnJV4hclT9XuaZUKyUvmyr+Q=" crossorigin="anonymous">
-    <!--end::Fonts--><!--begin::Third Party Plugin(OverlayScrollbars)-->
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/styles/overlayscrollbars.min.css"
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/styles/overlayscrollbars.min.css"
         integrity="sha256-dSokZseQNT08wYEWiz5iLI8QPlKxG+TswNRD8k35cpg=" crossorigin="anonymous">
-    <!--end::Third Party Plugin(OverlayScrollbars)--><!--begin::Third Party Plugin(Bootstrap Icons)-->
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.min.css"
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.min.css"
         integrity="sha256-Qsx5lrStHZyR9REqhUF8iQt73X06c8LGIUPzpOhwRrI=" crossorigin="anonymous">
-    <!--end::Third Party Plugin(Bootstrap Icons)--><!--begin::Required Plugin(AdminLTE)-->
-    <link rel="stylesheet" href="{% static './css/adminlte.css' %}"><!--end::Required Plugin(AdminLTE)--><!-- apexcharts -->
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/apexcharts@3.37.1/dist/apexcharts.css"
+<link rel="stylesheet" href="{% static './css/adminlte.css' %}">
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/apexcharts@3.37.1/dist/apexcharts.css"
         integrity="sha256-4MX+61mt9NVvvuPjUWdUdyfZfxSB1/Rf9WtqRHgG5S0=" crossorigin="anonymous">
-    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
-    <link rel="stylesheet" href="{% static './css/select2-bootstrap4.min.css' %}">
-    <link rel="stylesheet" href="{% static './css/custom.css' %}">
-    <script src="https://cdn.tailwindcss.com"></script>
-    <!-- <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;800&display=swap" rel="stylesheet"> -->
-    <style>
-        .bg-remover-container .nav-pills .nav-link { color: #64748b; font-weight: 600; border-radius: 12px; padding: 10px 25px; transition: all 0.3s; border: none; background: none; }
-        .bg-remover-container .nav-pills .nav-link.active { background-color: #3b82f6 !important; color: white !important; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); }
-        .bg-remover-container .upload-card { transition: all 0.3s ease; border: 2px dashed #cbd5e1; background: #ffffff; }
-        .upload-card:hover { border-color: #3b82f6; background-color: #f8fafc; transform: translateY(-2px); }
-        .preview-box { background: #ffffff; border-radius: 12px; padding: 15px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
-        .checkerboard-bg {
-            background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(-45deg, #eee 25%, transparent 25%), 
-                              linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(-45deg, transparent 75%, #eee 75%);
-            background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
-        }
-    </style>
+<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
+<link rel="stylesheet" href="{% static './css/select2-bootstrap4.min.css' %}">
+<link rel="stylesheet" href="{% static './css/custom.css' %}">
+<script src="https://cdn.tailwindcss.com"></script>
+
+<style>
+    .bg-remover-container .nav-pills .nav-link { color: #64748b; font-weight: 600; border-radius: 12px; padding: 10px 25px; transition: all 0.3s; border: none; background: none; }
+    .bg-remover-container .nav-pills .nav-link.active { background-color: #3b82f6 !important; color: white !important; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); }
+    .bg-remover-container .upload-card { transition: all 0.3s ease; border: 2px dashed #cbd5e1; background: #ffffff; }
+    .upload-card:hover { border-color: #3b82f6; background-color: #f8fafc; transform: translateY(-2px); }
+    .preview-box { background: #ffffff; border-radius: 12px; padding: 15px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
+    .checkerboard-bg {
+        background-image: linear-gradient(45deg, #eee 25%, transparent 25%), linear-gradient(-45deg, #eee 25%, transparent 25%), 
+                          linear-gradient(45deg, transparent 75%, #eee 75%), linear-gradient(-45deg, transparent 75%, #eee 75%);
+        background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+    }
+</style>
 </head>
 
-<!-- <body class="layout-fixed sidebar-expand-lg bg-body-tertiary"> -->
 <body class="layout-fixed sidebar-expand-lg sidebar-mini app-loaded sidebar-collapse">
-    <div class="app-wrapper">
-        {% include 'header.html' %}
-        {% include 'sidebar.html' %}
-
-        <main class="app-main">
-            <div class="app-content-header"> <!--begin::Container-->
-                <div class="container-fluid"> <!--begin::Row-->
-                    <div class="row">
-                        <div class="col-sm-6">
-                            <h3 class="mb-0">🪄 Image Background Remover
-                                
-                            </h3> 
-                            
-                        </div>
-                        <div class="col-sm-6">
-                            <ol class="breadcrumb float-sm-end">
-                                <li class="breadcrumb-item"><a href="{% url 'file-upload' %}">Home</a></li>
-                                <li class="breadcrumb-item active" aria-current="page"><a href="{% url 'product-attributes' %}"></a>
-                                   🪄 Image Background Remover</a>
-                                </li>
-                            </ol>
-                        </div>
-                    </div> <!--end::Row-->
-                </div> <!--end::Container-->
+<div class="app-wrapper">
+    {% include 'header.html' %}
+    {% include 'sidebar.html' %}
+
+    <main class="app-main">
+        <div class="app-content-header">
+            <div class="container-fluid">
+                <div class="row">
+                    <div class="col-sm-6">
+                        <h3 class="mb-0">🪄 Image Background Remover</h3> 
+                    </div>
+                    <div class="col-sm-6">
+                        <ol class="breadcrumb float-sm-end">
+                            <li class="breadcrumb-item"><a href="{% url 'file-upload' %}">Home</a></li>
+                            <li class="breadcrumb-item active" aria-current="page"><a href="{% url 'product-attributes' %}"></a>
+                               🪄 Image Background Remover</a>
+                            </li>
+                        </ol>
+                    </div>
+                </div>
             </div>
-            <div class="app-content pt-10 bg-remover-container">
-                <div class=" mx-auto px-4">
-                    <div class="mb-10 flex flex-col md:flex-row md:items-end md:justify-between gap-4 border-b border-gray-100 pb-6">
-                        <div class="text-left">
-                            <h1 class="text-4xl font-black text-gray-900 tracking-tight mb-2">
-                                Image <span class="text-blue-600">Background</span> Remover
-                            </h1>
-                            <p class="text-gray-500 font-medium ">
-                                Professional-grade background removal. Process single images or 
-                                <span class="text-gray-800 font-semibold">batch process ZIP archives </span> instantly.
-                            </p>
-                        </div>
+        </div>
 
-                        <div class="flex items-center">
-                            <a href="/bg-remover/tasks/history/" 
-                            class="inline-flex items-center gap-2 px-5 py-2.5 bg-white border border-gray-200 text-gray-700 text-sm font-bold rounded-xl shadow-sm hover:shadow-md hover:border-blue-300 hover:text-blue-600 transition-all duration-200 group">
-                                <i class="bi bi-clock-history text-gray-400 group-hover:text-blue-500 transition-colors"></i>
-                                Task History
-                                <span class="flex h-2 w-2 rounded-full bg-blue-500 ml-1"></span>
-                            </a>
-                        </div>
+        <div class="app-content pt-10 bg-remover-container">
+            <div class="mx-auto px-4">
+                <div class="mb-10 flex flex-col md:flex-row md:items-end md:justify-between gap-4 border-b border-gray-100 pb-6">
+                    <div class="text-left">
+                        <h1 class="text-4xl font-black text-gray-900 tracking-tight mb-2">
+                            Image <span class="text-blue-600">Background</span> Remover
+                        </h1>
+                        <p class="text-gray-500 font-medium ">
+                            Professional-grade background removal. Process single images or 
+                            <span class="text-gray-800 font-semibold">batch process ZIP archives </span> instantly.
+                        </p>
                     </div>
-                    <!-- <div class="text-center mb-8">
-                        <h1 class="text-4xl font-extrabold text-gray-900 mb-2">AI Background Remover</h1>
-                        <p class="text-gray-500 font-medium">Remove image backgrounds instantly for single images, bulk uploads, or ZIP files.</p>
-                        <div class="flex justify-end mb-4">
-                            <a href="/bg-remover/tasks/history/" 
-                            class="flex items-center gap-2 px-6 py-2.5 bg-white border border-gray-200 text-gray-700 font-bold rounded-xl shadow-sm hover:bg-gray-50 hover:border-blue-400 transition-all">
-                                <i class="bi bi-clock-history text-blue-600"></i>
-                                View Task History
-                            </a>
-                        </div>
-                    </div> -->
-
-                    <ul class="nav nav-pills justify-content-center mb-8 gap-3" id="pills-tab" role="tablist">
-                        <li class="nav-item" role="presentation">
-                            <button class="nav-link active" id="pills-single-tab" data-bs-toggle="pill" data-bs-target="#pills-single" type="button" role="tab">
-                                <i class="bi bi-image mr-2"></i> Single Image
-                            </button>
-                        </li>
-                        <li class="nav-item" role="presentation">
-                            <button class="nav-link" id="pills-bulk-tab" data-bs-toggle="pill" data-bs-target="#pills-bulk" type="button" role="tab">
-                                <i class="bi bi-images mr-2"></i> Bulk & ZIP Process
-                            </button>
-                        </li>
-                    </ul>
-
-                    <div class="tab-content" id="pills-tabContent">
-                        <div class="tab-pane fade show active" id="pills-single" role="tabpanel">
-                            <div id="uploadContainer" class="bg-white p-8 rounded-3xl shadow-sm border border-gray-200 mb-8">
-                                <div id="drop-zone" class="upload-card flex flex-col items-center justify-center rounded-2xl p-12 cursor-pointer" onclick="document.getElementById('file-input').click()">
-                                    <input type="file" id="file-input" class="hidden" accept="image/*">
-                                    <div class="bg-blue-50 p-5 rounded-full mb-4 text-blue-600">
-                                        <i class="bi bi-cloud-arrow-up text-4xl"></i>
-                                    </div>
-                                    <p class="text-lg font-semibold text-gray-700">Click or Drag & Drop Image</p>
-                                    <p class="text-sm text-gray-400 mt-1">Supports PNG, JPG, WEBP</p>
+
+                    <div class="flex items-center">
+                        <a href="/bg-remover/tasks/history/" 
+                        class="inline-flex items-center gap-2 px-5 py-2.5 bg-white border border-gray-200 text-gray-700 text-sm font-bold rounded-xl shadow-sm hover:shadow-md hover:border-blue-300 hover:text-blue-600 transition-all duration-200 group">
+                            <i class="bi bi-clock-history text-gray-400 group-hover:text-blue-500 transition-colors"></i>
+                            Task History
+                            <span class="flex h-2 w-2 rounded-full bg-blue-500 ml-1"></span>
+                        </a>
+                    </div>
+                </div>
+
+                <ul class="nav nav-pills justify-content-center mb-8 gap-3" id="pills-tab" role="tablist">
+                    <li class="nav-item" role="presentation">
+                        <button class="nav-link active" id="pills-single-tab" data-bs-toggle="pill" data-bs-target="#pills-single" type="button" role="tab">
+                            <i class="bi bi-image mr-2"></i> Single Image
+                        </button>
+                    </li>
+                    <li class="nav-item" role="presentation">
+                        <button class="nav-link" id="pills-bulk-tab" data-bs-toggle="pill" data-bs-target="#pills-bulk" type="button" role="tab">
+                            <i class="bi bi-images mr-2"></i> Bulk & ZIP Process
+                        </button>
+                    </li>
+                </ul>
+
+                <div class="tab-content" id="pills-tabContent">
+                    <!-- NEW: Choose Action -->
+                            
+                    <!-- SINGLE IMAGE TAB -->
+                    <div class="tab-pane fade show active" id="pills-single" role="tabpanel">
+                        <div class="mt-6 flex justify-center gap-6">
+                                <label class="inline-flex items-center gap-2">
+                                    <input type="radio" name="action" value="remove_bg" checked>
+                                    <span>Remove Background</span>
+                                </label>
+                                <label class="inline-flex items-center gap-2">
+                                    <input type="radio" name="action" value="caption">
+                                    <span>Generate Caption</span>
+                                </label>
+                            </div>
+                        <div id="uploadContainer" class="bg-white p-8 rounded-3xl shadow-sm border border-gray-200 mb-8">
+                            <div id="drop-zone" class="upload-card flex flex-col items-center justify-center rounded-2xl p-12 cursor-pointer" onclick="document.getElementById('file-input').click()">
+                                <input type="file" id="file-input" class="hidden" accept="image/*">
+                                <div class="bg-blue-50 p-5 rounded-full mb-4 text-blue-600">
+                                    <i class="bi bi-cloud-arrow-up text-4xl"></i>
                                 </div>
+                                <p class="text-lg font-semibold text-gray-700">Click or Drag & Drop Image</p>
+                                <p class="text-sm text-gray-400 mt-1">Supports PNG, JPG, WEBP</p>
                             </div>
-                            <div id="result-container" class="hidden space-y-8 pb-20">
-                                <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
-                                    <div class="space-y-3">
-                                        <span class="text-xs font-bold uppercase text-gray-400 tracking-wider">Original</span>
-                                        <div class="preview-box"><img id="original-preview" class="rounded-lg w-full h-auto object-contain max-h-[400px]"></div>
+                        </div>
+                            
+
+                        <!-- RESULTS -->
+                        <div id="result-container" class="hidden space-y-8 pb-20">
+                            <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
+                                <div class="space-y-3">
+                                    <span class="text-xs font-bold uppercase text-gray-400 tracking-wider">Original</span>
+                                    <div class="preview-box"><img id="original-preview" class="rounded-lg w-full h-auto object-contain max-h-[400px]"></div>
+                                </div>
+                                <div class="space-y-3">
+                                    <div class="flex justify-between items-center px-1">
+                                        <span class="text-xs font-bold uppercase text-gray-400 tracking-wider">Processed</span>
+                                        <span id="processing-badge" class="hidden animate-pulse bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-[10px] font-bold">PROCESSING...</span>
                                     </div>
-                                    <div class="space-y-3">
-                                        <div class="flex justify-between items-center px-1">
-                                            <span class="text-xs font-bold uppercase text-gray-400 tracking-wider">Processed</span>
-                                            <span id="processing-badge" class="hidden animate-pulse bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-[10px] font-bold">PROCESSING...</span>
-                                        </div>
-                                        <div class="preview-box relative min-h-[200px] flex items-center justify-center checkerboard-bg rounded-lg">
-                                            <div id="result-loader" class="absolute inset-0 bg-white/60 backdrop-blur-sm flex flex-col items-center justify-center rounded-lg z-10 hidden">
-                                                <div class="w-10 h-10 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
-                                            </div>
-                                            <img id="processed-preview" class="rounded-lg w-full h-auto object-contain max-h-[400px]">
+                                    <div class="preview-box relative min-h-[200px] flex items-center justify-center checkerboard-bg rounded-lg p-4">
+                                        <div id="result-loader" class="absolute inset-0 bg-white/60 backdrop-blur-sm flex flex-col items-center justify-center rounded-lg z-10 hidden">
+                                            <div class="w-10 h-10 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
                                         </div>
+
+                                        <!-- Image element -->
+                                        <img id="processed-preview" class="rounded-lg w-full h-auto object-contain max-h-[400px]">
+
+                                        <!-- Caption element -->
+                                        <div id="caption-text" class="hidden text-center text-gray-800 font-semibold text-lg"></div>
                                     </div>
                                 </div>
-                                <div class="flex gap-3 justify-center">
-                                    <button onclick="location.reload()" class="px-6 py-2 bg-gray-100 rounded-xl font-bold hover:bg-gray-200">New Upload</button>
-                                    <a id="download-btn" class="hidden px-8 py-2 bg-green-600 text-white rounded-xl font-bold shadow-lg hover:bg-green-700">Download PNG</a>
-                                </div>
+
+                                <!-- <div class="space-y-3">
+                                    <div class="flex justify-between items-center px-1">
+                                        <span class="text-xs font-bold uppercase text-gray-400 tracking-wider">Processed</span>
+                                        <span id="processing-badge" class="hidden animate-pulse bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-[10px] font-bold">PROCESSING...</span>
+                                    </div>
+                                    <div class="preview-box relative min-h-[200px] flex items-center justify-center checkerboard-bg rounded-lg">
+                                        <div id="result-loader" class="absolute inset-0 bg-white/60 backdrop-blur-sm flex flex-col items-center justify-center rounded-lg z-10 hidden">
+                                            <div class="w-10 h-10 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
+                                        </div>
+                                        <img id="processed-preview" class="rounded-lg w-full h-auto object-contain max-h-[400px]">
+                                    </div>
+                                </div> -->
+                            </div>
+
+                            <!-- NEW CAPTION SECTION -->
+                            <div id="caption-container" class="hidden mt-4 text-center">
+                                <h3 class="text-lg font-semibold text-gray-700">AI Generated Caption:</h3>
+                                <p id="caption-text-below" class="text-blue-600 font-bold text-xl mt-1"></p>
+                            </div>
+
+                            <div class="flex gap-3 justify-center mt-4">
+                                <button onclick="location.reload()" class="px-6 py-2 bg-gray-100 rounded-xl font-bold hover:bg-gray-200">New Upload</button>
+                                <a id="download-btn" class="hidden px-8 py-2 bg-green-600 text-white rounded-xl font-bold shadow-lg hover:bg-green-700">Download PNG</a>
                             </div>
                         </div>
-                        <div class="tab-pane fade" id="pills-bulk" role="tabpanel">
-                             <div class="bg-white p-8 rounded-3xl shadow-sm border border-gray-200 mb-8">
-                                <div id="bulk-drop-zone" class="upload-card flex flex-col items-center justify-center rounded-2xl p-12 cursor-pointer transition-all border-indigo-200" onclick="document.getElementById('bulk-file-input').click()">
-                                    <input type="file" id="bulk-file-input" class="hidden" accept="image/*,.zip" multiple>
-                                    <div class="bg-indigo-50 p-5 rounded-full mb-4 text-indigo-600">
-                                        <i class="bi bi-file-earmark-zip text-4xl"></i>
-                                    </div>
-                                    <h3 class="text-xl font-bold text-gray-800">Bulk & ZIP Upload</h3>
-                                    <p class="text-gray-500 mt-1 text-center">Upload multiple images or a <b>single ZIP file</b></p>
+                    </div>
+
+                    <!-- BULK TAB -->
+                    <div class="tab-pane fade" id="pills-bulk" role="tabpanel">
+                        <div class="mb-6 flex justify-center gap-6">
+                            <label class="inline-flex items-center gap-2">
+                                <input type="radio" name="bulk-action" value="remove_bg" checked>
+                                <span class="font-semibold text-gray-700">Bulk Remove Background</span>
+                            </label>
+                            <label class="inline-flex items-center gap-2">
+                                <input type="radio" name="bulk-action" value="caption">
+                                <span class="font-semibold text-gray-700">Bulk Generate Caption</span>
+                            </label>
+                        </div>
+                        <div class="bg-white p-8 rounded-3xl shadow-sm border border-gray-200 mb-8">
+                            <div id="bulk-drop-zone" class="upload-card flex flex-col items-center justify-center rounded-2xl p-12 cursor-pointer transition-all border-indigo-200" onclick="document.getElementById('bulk-file-input').click()">
+                                <input type="file" id="bulk-file-input" class="hidden" accept="image/*,.zip" multiple>
+                                <div class="bg-indigo-50 p-5 rounded-full mb-4 text-indigo-600">
+                                    <i class="bi bi-file-earmark-zip text-4xl"></i>
                                 </div>
-                                <div id="bulk-list-container" class="hidden mt-8 border-t pt-8">
-                                    <div class="flex justify-between items-center mb-6">
-                                        <div>
-                                            <h3 class="font-extrabold text-gray-900 text-lg">Batch Queue</h3>
-                                            <p class="text-xs text-gray-400 uppercase font-bold tracking-widest"><span id="file-count">0</span> Items Ready</p>
-                                        </div>
-                                        <button id="start-bulk-btn" class="bg-indigo-600 text-white px-8 py-3 rounded-xl font-bold hover:bg-indigo-700 shadow-lg">Start Processing</button>
+                                <h3 class="text-xl font-bold text-gray-800">Bulk & ZIP Upload</h3>
+                                <p class="text-gray-500 mt-1 text-center">Upload multiple images or a <b>single ZIP file</b></p>
+                            </div>
+                            <div id="bulk-list-container" class="hidden mt-8 border-t pt-8">
+                                <div class="flex justify-between items-center mb-6">
+                                    <div>
+                                        <h3 class="font-extrabold text-gray-900 text-lg">Batch Queue</h3>
+                                        <p class="text-xs text-gray-400 uppercase font-bold tracking-widest"><span id="file-count">0</span> Items Ready</p>
                                     </div>
-                                    <div id="file-queue" class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-4"></div>
+                                    <button id="start-bulk-btn" class="bg-indigo-600 text-white px-8 py-3 rounded-xl font-bold hover:bg-indigo-700 shadow-lg">Start Processing</button>
                                 </div>
+                                <div id="file-queue" class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-4"></div>
                             </div>
                         </div>
                     </div>
                 </div>
             </div>
-            <div id="bulk-loader" class="hidden fixed inset-0 bg-black/60 backdrop-blur-md z-[9999] flex flex-col items-center justify-center text-white">
-                <div class="flex flex-col items-center p-8 bg-gray-900/80 rounded-3xl border border-white/10 shadow-2xl">
-                    <div class="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mb-4"></div>
-                    <h2 class="text-2xl font-bold">Processing Batch...</h2>
-                    <p class="text-gray-400 mt-2">AI is removing backgrounds and preparing your ZIP.</p>
-                    <p class="text-xs text-indigo-400 mt-4 uppercase tracking-widest font-bold">Please do not refresh the page</p>
-                </div>
+        </div>
+
+        <div id="bulk-loader" class="hidden fixed inset-0 bg-black/60 backdrop-blur-md z-[9999] flex flex-col items-center justify-center text-white">
+            <div class="flex flex-col items-center p-8 bg-gray-900/80 rounded-3xl border border-white/10 shadow-2xl">
+                <div class="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mb-4"></div>
+                <h2 class="text-2xl font-bold">Processing Batch...</h2>
+                <p class="text-gray-400 mt-2">AI is removing backgrounds and preparing your ZIP.</p>
+                <p class="text-xs text-indigo-400 mt-4 uppercase tracking-widest font-bold">Please do not refresh the page</p>
             </div>
-        </main>
-        {% include 'footer.html' %}
-    </div>
+        </div>
+    </main>
 
-    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/browser/overlayscrollbars.browser.es6.min.js"></script>
-    <script src="{% static './js/adminlte.js' %}"></script>
+    {% include 'footer.html' %}
+</div>
 
-    <script>
-        
+<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/browser/overlayscrollbars.browser.es6.min.js"></script>
+<script src="{% static './js/adminlte.js' %}"></script>
 
-        // --- PRESERVED SINGLE UPLOAD LOGIC ---
-        const fileInput = document.getElementById('file-input');
-        if (fileInput) {
-            fileInput.onchange = (e) => handleSingleUpload(e.target.files[0]);
-        }
+<script>
+    // SINGLE IMAGE UPLOAD + CAPTION
+    const fileInput = document.getElementById('file-input');
+    if (fileInput) {
+        fileInput.onchange = (e) => handleSingleUpload(e.target.files[0]);
+    }
+
+    // async function handleSingleUpload(file) {
+    //     if (!file) return;
+    //     document.getElementById('uploadContainer').classList.add('hidden');
+    //     document.getElementById('result-container').classList.remove('hidden');
+    //     document.getElementById('result-loader').classList.remove('hidden');
+    //     document.getElementById('original-preview').src = URL.createObjectURL(file);
+
+    //     const formData = new FormData();
+    //     formData.append('image', file);
+
+    //     try {
+    //         // --- REMOVE BG ---
+    //         const response = await fetch('/bg-remover/remove-bg/', {
+    //             method: 'POST',
+    //             body: formData,
+    //             headers: {'X-CSRFToken': '{{ csrf_token }}'}
+    //         });
+    //         if (!response.ok) throw new Error("Processing failed");
+    //         const blob = await response.blob();
+    //         const url = URL.createObjectURL(blob);
+    //         document.getElementById('processed-preview').src = url;
+    //         const btn = document.getElementById('download-btn');
+    //         btn.href = url;
+    //         btn.download = `cleared_${file.name.split('.')[0]}.png`;
+    //         btn.classList.remove('hidden');
+
+    //         // --- GET CAPTION ---
+    //         const captionResp = await fetch('/bg-remover/caption/', {
+    //             method: 'POST',
+    //             body: formData,
+    //             headers: {'X-CSRFToken': '{{ csrf_token }}'}
+    //         });
+    //         if (captionResp.ok) {
+    //             const data = await captionResp.json();
+    //             document.getElementById('caption-text').innerText = data.caption;
+    //             document.getElementById('caption-container').classList.remove('hidden');
+    //         }
 
-        async function handleSingleUpload(file) {
-            if (!file) return;
-            document.getElementById('uploadContainer').classList.add('hidden');
-            document.getElementById('result-container').classList.remove('hidden');
-            document.getElementById('result-loader').classList.remove('hidden');
-            document.getElementById('original-preview').src = URL.createObjectURL(file);
+    //     } catch (err) { 
+    //         alert("Error processing image: " + err.message); 
+    //         location.reload();
+    //     } finally { 
+    //         document.getElementById('result-loader').classList.add('hidden'); 
+    //     }
+    // }
 
-            const formData = new FormData();
-            formData.append('image', file);
+    async function handleSingleUpload(file) {
+        if (!file) return;
 
-            try {
+        const selectedAction = document.querySelector('input[name="action"]:checked').value;
+
+        document.getElementById('uploadContainer').classList.add('hidden');
+        document.getElementById('result-container').classList.remove('hidden');
+        document.getElementById('result-loader').classList.remove('hidden');
+        document.getElementById('original-preview').src = URL.createObjectURL(file);
+
+        const formData = new FormData();
+        formData.append('image', file);
+
+        try {
+            if (selectedAction === 'remove_bg') {
+                // --- REMOVE BG ---
                 const response = await fetch('/bg-remover/remove-bg/', {
                     method: 'POST',
                     body: formData,
                     headers: {'X-CSRFToken': '{{ csrf_token }}'}
                 });
-                if (!response.ok) throw new Error("Processing failed");
+                if (!response.ok) throw new Error("Background removal failed");
                 const blob = await response.blob();
                 const url = URL.createObjectURL(blob);
                 document.getElementById('processed-preview').src = url;
+                document.getElementById('processed-preview').classList.remove('hidden');
+                document.getElementById('caption-text').classList.add('hidden');
+
                 const btn = document.getElementById('download-btn');
                 btn.href = url;
                 btn.download = `cleared_${file.name.split('.')[0]}.png`;
                 btn.classList.remove('hidden');
-            } catch (err) { 
-                alert("Error processing image: " + err.message); 
-                location.reload();
-            } finally { 
-                document.getElementById('result-loader').classList.add('hidden'); 
-            }
-        }
 
-        // --- PRESERVED BULK LOGIC ---
-        const bulkInput = document.getElementById('bulk-file-input');
-        const fileQueue = document.getElementById('file-queue');
-        const bulkList = document.getElementById('bulk-list-container');
-        const startBulkBtn = document.getElementById('start-bulk-btn');
-        const bulkLoader = document.getElementById('bulk-loader');
-
-        if (bulkInput) {
-            bulkInput.onchange = (e) => {
-                const files = Array.from(e.target.files);
-                if (files.length === 0) return;
-                bulkList.classList.remove('hidden');
-                document.getElementById('file-count').innerText = files.length;
-                fileQueue.innerHTML = '';
-                files.forEach(file => {
-                    const div = document.createElement('div');
-                    div.className = "relative bg-gray-50 rounded-xl border border-gray-100 p-2 text-center";
-                    if (file.name.toLowerCase().endsWith('.zip')) {
-                        div.innerHTML = `<div class="aspect-square flex flex-col items-center justify-center text-indigo-500"><i class="bi bi-file-earmark-zip-fill text-4xl"></i><span class="text-[10px] font-bold mt-1 truncate w-full px-2">${file.name}</span></div>`;
-                    } else {
-                        const reader = new FileReader();
-                        reader.onload = (event) => {
-                            div.innerHTML = `<div class="aspect-square rounded-lg overflow-hidden bg-white mb-1"><img src="${event.target.result}" class="w-full h-full object-cover"></div><span class="text-[9px] font-bold text-gray-400 truncate block">${file.name}</span>`;
-                        };
-                        reader.readAsDataURL(file);
-                    }
-                    fileQueue.appendChild(div);
+                document.getElementById('caption-container').classList.add('hidden'); // hide caption
+                document.getElementById('caption-text-below').classList.add('hidden'); // hide caption
+                
+
+            } else if (selectedAction === 'caption') {
+                // --- GENERATE CAPTION ---
+                const captionResp = await fetch('/bg-remover/caption/', {
+                    method: 'POST',
+                    body: formData,
+                    headers: {'X-CSRFToken': '{{ csrf_token }}'}
                 });
-            };
+                if (!captionResp.ok) throw new Error("Caption generation failed");
+                const data = await captionResp.json();
+                document.getElementById('caption-text').innerText = data.caption;
+                document.getElementById('caption-container').classList.remove('hidden');
+                document.getElementById('caption-text-below').classList.remove('hidden');
+                
+                document.getElementById('caption-text').classList.remove('hidden');
+                document.getElementById('processed-preview').classList.add('hidden');
+                document.getElementById('download-btn').classList.add('hidden');
+
+                // document.getElementById('processed-preview').src = ''; // no image
+                // document.getElementById('download-btn').classList.add('hidden');
+            }
+
+        } catch (err) { 
+            alert("Error: " + err.message); 
+            location.reload();
+        } finally { 
+            document.getElementById('result-loader').classList.add('hidden'); 
         }
+    }
+    // BULK LOGIC PRESERVED
+    const bulkInput = document.getElementById('bulk-file-input');
+    const fileQueue = document.getElementById('file-queue');
+    const bulkList = document.getElementById('bulk-list-container');
+    const startBulkBtn = document.getElementById('start-bulk-btn');
+    const bulkLoader = document.getElementById('bulk-loader');
 
-        /*if (startBulkBtn) {
-            startBulkBtn.onclick = async () => {
-                const files = bulkInput.files;
-                if (!files || files.length === 0) return;
-                const formData = new FormData();
-                for (let i = 0; i < files.length; i++) formData.append('images', files[i]);
-                if (bulkLoader) bulkLoader.classList.remove('hidden');
-                try {
-                    const response = await fetch('/bg-remover/remove-bg/bulk/', {
-                        method: 'POST', body: formData, headers: {'X-CSRFToken': '{{ csrf_token }}'}
-                    });
-                    if (response.ok) {
-                        const blob = await response.blob();
-                        const url = window.URL.createObjectURL(blob);
-                        const bulkCardInner = document.querySelector('#pills-bulk .bg-white');
-                        bulkCardInner.innerHTML = `<div class="text-center py-12"><div class="bg-green-100 text-green-600 p-6 rounded-full inline-block mb-6"><i class="bi bi-check-all text-6xl"></i></div><h2 class="text-3xl font-bold">Complete!</h2><a href="${url}" download="processed.zip" class="bg-indigo-600 text-white px-10 py-4 rounded-2xl font-bold inline-block mt-4">Download ZIP</a></div>`;
-                    }
-                } finally { if (bulkLoader) bulkLoader.classList.add('hidden'); }
-            };
-        }*/
+    if (bulkInput) {
+        bulkInput.onchange = (e) => {
+            const files = Array.from(e.target.files);
+            if (files.length === 0) return;
+            bulkList.classList.remove('hidden');
+            document.getElementById('file-count').innerText = files.length;
+            fileQueue.innerHTML = '';
+            files.forEach(file => {
+                const div = document.createElement('div');
+                div.className = "relative bg-gray-50 rounded-xl border border-gray-100 p-2 text-center";
+                if (file.name.toLowerCase().endsWith('.zip')) {
+                    div.innerHTML = `<div class="aspect-square flex flex-col items-center justify-center text-indigo-500"><i class="bi bi-file-earmark-zip-fill text-4xl"></i><span class="text-[10px] font-bold mt-1 truncate w-full px-2">${file.name}</span></div>`;
+                } else {
+                    const reader = new FileReader();
+                    reader.onload = (event) => {
+                        div.innerHTML = `<div class="aspect-square rounded-lg overflow-hidden bg-white mb-1"><img src="${event.target.result}" class="w-full h-full object-cover"></div><span class="text-[9px] font-bold text-gray-400 truncate block">${file.name}</span>`;
+                    };
+                    reader.readAsDataURL(file);
+                }
+                fileQueue.appendChild(div);
+            });
+        };
+    }
 
-        if (startBulkBtn) {
+    if (startBulkBtn) {
     startBulkBtn.onclick = async () => {
-        const files = document.getElementById('bulk-file-input').files;
+        const files = bulkInput.files;
         if (!files.length) return;
+
+        // 1. Identify which action is selected for bulk
+        const selectedAction = document.querySelector('input[name="bulk-action"]:checked').value;
         const formData = new FormData();
-        for (let i = 0; i < files.length; i++) formData.append('images', files[i]);
         
-        document.getElementById('bulk-loader').classList.remove('hidden');
+        // Use 'images' as the key to match your single image logic/backend expectation
+        for (let i = 0; i < files.length; i++) {
+            formData.append('images', files[i]);
+        }
+        
+        const loader = document.getElementById('bulk-loader');
+        const loaderTitle = loader.querySelector('h2');
+        const loaderSub = loader.querySelector('p');
+
+        // Update loader text based on action
+        if (selectedAction === 'caption') {
+            loaderTitle.innerText = "Generating Captions...";
+            loaderSub.innerText = "AI is describing your images and preparing a report.";
+        } else {
+            loaderTitle.innerText = "Processing Batch...";
+            loaderSub.innerText = "AI is removing backgrounds and preparing your ZIP.";
+        }
+
+        loader.classList.remove('hidden');
+
         try {
-            const response = await fetch('/bg-remover/remove-bg/bulk/', { 
+            // 2. Determine URL based on action
+            const endpoint = selectedAction === 'caption' 
+                ? '/bg-remover/caption/bulk/' 
+                : '/bg-remover/remove-bg/bulk/';
+
+            const response = await fetch(endpoint, { 
                 method: 'POST', 
                 body: formData, 
                 headers: {'X-CSRFToken': '{{ csrf_token }}'} 
             });
+
             if (response.ok) {
-                // REDIRECT TO THE NEW HISTORY PAGE
-                window.location.href = "{% url 'bg_history_page' %}";
+                const data = await response.json();
+                
+                if (selectedAction === 'caption') {
+                    // If your caption bulk returns an Excel or CSV file
+                    if (data.file_url) {
+                        window.location.href = data.file_url;
+                    } else {
+                        alert("Captions generated successfully. Check Task History.");
+                    }
+                } else {
+                    // Standard ZIP download for background removal
+                    window.location.href = data.zip_url;
+                }
+            } else { 
+                const errorData = await response.json();
+                alert("Batch processing failed: " + (errorData.error || "Unknown error")); 
             }
-        } catch (err) { 
-            alert("Upload failed"); 
-            document.getElementById('bulk-loader').classList.add('hidden');
+        } catch(err) { 
+            console.error(err);
+            alert("An error occurred during batch processing."); 
+        } finally { 
+            loader.classList.add('hidden'); 
         }
     };
 }
-    
-    
-    </script>
-    <script>
-        const SELECTOR_SIDEBAR_WRAPPER = ".sidebar-wrapper";
-        const Default = {
-            scrollbarTheme: "os-theme-light",
-            scrollbarAutoHide: "leave",
-            scrollbarClickScroll: true,
-        };
-        document.addEventListener("DOMContentLoaded", function () {
-            const sidebarWrapper = document.querySelector(SELECTOR_SIDEBAR_WRAPPER);
-            if (
-                sidebarWrapper &&
-                typeof OverlayScrollbarsGlobal?.OverlayScrollbars !== "undefined"
-            ) {
-                OverlayScrollbarsGlobal.OverlayScrollbars(sidebarWrapper, {
-                    scrollbars: {
-                        theme: Default.scrollbarTheme,
-                        autoHide: Default.scrollbarAutoHide,
-                        clickScroll: Default.scrollbarClickScroll,
-                    },
-                });
-            }
-        });
-    </script>
+
+    // if (startBulkBtn) {
+    //     startBulkBtn.onclick = async () => {
+    //         const files = bulkInput.files;
+    //         if (!files.length) return;
+    //         const formData = new FormData();
+    //         for (let i = 0; i < files.length; i++) formData.append('images', files[i]);
+            
+    //         document.getElementById('bulk-loader').classList.remove('hidden');
+    //         try {
+    //             const response = await fetch('/bg-remover/remove-bg/bulk/', { 
+    //                 method: 'POST', 
+    //                 body: formData, 
+    //                 headers: {'X-CSRFToken': '{{ csrf_token }}'} 
+    //             });
+    //             if (response.ok) {
+    //                 window.location.href = await response.json().then(data => data.zip_url);
+    //             } else { alert("Batch processing failed."); }
+    //         } catch(err) { alert(err); }
+    //         finally { bulkLoader.classList.add('hidden'); }
+    //     };
+    // }
+</script>
 </body>
-</html>
+</html>

+ 5 - 0
bg_remover/urls.py

@@ -10,5 +10,10 @@ urlpatterns = [
     path('status/<str:task_id>/', views.TaskStatusAPI.as_view(), name='task_status'),
     path('tasks/list/', views.TaskHistoryAPI.as_view(), name='bg_tasks_json'),
     path('tasks/history/', views.TaskHistoryPageView.as_view(), name='bg_history_page'),
+    # path("caption/", views.caption_image, name="caption"),
+    path("caption/", views.ImageCaptionITMView.as_view(), name="caption"),
+    path("caption/bulk/", views.BulkImageCaptionITMAPI.as_view(), name="caption_bulk"),
+
+
 
 ]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 205 - 2
bg_remover/views.py

@@ -123,6 +123,7 @@ from PIL import Image
 
 from .models import BackgroundTask
 from .services import BiRefNetService
+# from .blip_service import BLIPService
 
 
 class BackgroundRemovalView(APIView):
@@ -160,7 +161,7 @@ class BulkBackgroundRemovalAPI(APIView):
 
         # 1. Create a database record for the task
         task_id = uuid.uuid4()
-        BackgroundTask.objects.create(task_id=task_id, status='PROCESSING')
+        BackgroundTask.objects.create(task_id=task_id, task_type="BG_REMOVE", status='PROCESSING')
 
         # 2. Read files into memory to pass to the thread
         file_list = []
@@ -284,6 +285,7 @@ class TaskHistoryAPI(APIView):
             data.append({
                 "task_id": str(t.task_id),
                 "status": t.status,
+                "task_type": t.task_type,
                 # Using 'date' key to match the JS 't.date' in the history page
                 "date": t.created_at.strftime("%Y-%m-%d %H:%M"), 
                 "url": t.zip_file.url if t.zip_file else None
@@ -293,4 +295,205 @@ class TaskHistoryAPI(APIView):
 
 class TaskHistoryPageView(APIView):
     def get(self, request):
-        return render(request, "bg_remover_history.html")           
+        return render(request, "bg_remover_history.html")           
+    
+# from django.http import JsonResponse
+
+
+
+# def caption_image(request):
+#     image_path = "media/test.jpg"  # example image
+#     caption = generate_caption(image_path)
+#     return JsonResponse({"caption": caption})
+
+
+from .blip_service import BLIPServiceHF
+from .models import BackgroundTask
+
+class ImageCaptionView(APIView):
+    parser_classes = [MultiPartParser]
+
+    def get(self, request):
+        return render(request, "caption_index.html")
+
+    def post(self, request):
+        if "image" not in request.FILES:
+            return HttpResponse("No image provided", status=400)
+
+        image = Image.open(request.FILES["image"]).convert("RGB")
+
+        service = BLIPServiceHF()  # Use HF version
+        caption = service.generate_caption(image)
+
+        return JsonResponse({
+            "caption": caption
+        })
+
+
+class BulkImageCaptionAPI(APIView):
+    parser_classes = [MultiPartParser]
+
+    def post(self, request):
+        files = request.FILES.getlist("images")
+        if not files:
+            return JsonResponse({"error": "No images provided"}, status=400)
+
+        task_id = uuid.uuid4()
+
+        BackgroundTask.objects.create(
+            task_id=task_id,
+            task_type="CAPTION",
+            status="PROCESSING"
+        )
+
+        file_list = [
+            {"name": f.name, "content": f.read()}
+            for f in files
+        ]
+
+        thread = threading.Thread(
+            target=self.process_caption_logic,
+            args=(task_id, file_list)
+        )
+        thread.start()
+
+        return JsonResponse({
+            "task_id": str(task_id),
+            "message": "Caption task started",
+            "status_url": f"/api/status/{task_id}/"
+        }, status=202)
+
+    def process_caption_logic(self, task_id, file_list):
+        service = BLIPServiceHF()  # Use HF version
+        log_data = []
+
+        folder_path = os.path.join(
+            settings.MEDIA_ROOT, "caption_results", str(task_id)
+        )
+        os.makedirs(folder_path, exist_ok=True)
+
+        zip_name = f"captions_{task_id}.zip"
+        zip_path = os.path.join(folder_path, zip_name)
+
+        try:
+            with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
+                for item in file_list:
+                    name, content = item["name"], item["content"]
+                    try:
+                        img = Image.open(io.BytesIO(content)).convert("RGB")
+                        caption = service.generate_caption(img)
+
+                        txt_name = name.rsplit(".", 1)[0] + ".txt"
+                        zipf.writestr(txt_name, caption)
+
+                        log_data.append([name, "SUCCESS", caption])
+                    except Exception as e:
+                        log_data.append([name, "FAIL", str(e)])
+
+                # Write CSV log
+                csv_buf = io.StringIO()
+                writer = csv.writer(csv_buf)
+                writer.writerow(["Filename", "Status", "Caption"])
+                writer.writerows(log_data)
+                zipf.writestr("captions.csv", csv_buf.getvalue())
+
+            task = BackgroundTask.objects.get(task_id=task_id)
+            task.status = "COMPLETED"
+            task.zip_file = f"caption_results/{task_id}/{zip_name}"
+            task.save()
+
+        except Exception as e:
+            task = BackgroundTask.objects.get(task_id=task_id)
+            task.status = "FAILED"
+            task.error_message = str(e)
+            task.save()
+
+
+
+
+from .blip_service_itm import BLIPDecoderService
+
+class ImageCaptionITMView(APIView):
+    parser_classes = [MultiPartParser]
+
+    def get(self, request):
+        return render(request, "caption_index.html")
+
+    def post(self, request):
+        if "image" not in request.FILES:
+            return JsonResponse({"error": "No image provided"}, status=400)
+
+        image = Image.open(request.FILES["image"]).convert("RGB")
+        
+        # Using the new Decoder Service
+        service = BLIPDecoderService() 
+        caption = service.generate_caption(image)
+
+        return JsonResponse({"caption": caption})
+
+class BulkImageCaptionITMAPI(APIView):
+    parser_classes = [MultiPartParser]
+
+    def post(self, request):
+        files = request.FILES.getlist("images")
+        if not files:
+            return JsonResponse({"error": "No images provided"}, status=400)
+
+        task_id = uuid.uuid4()
+        BackgroundTask.objects.create(
+            task_id=task_id,
+            task_type="CAPTION",
+            status="PROCESSING"
+        )
+
+        file_list = [{"name": f.name, "content": f.read()} for f in files]
+
+        thread = threading.Thread(
+            target=self.process_caption_logic,
+            args=(task_id, file_list)
+        )
+        thread.start()
+
+        return JsonResponse({
+            "task_id": str(task_id),
+            "status_url": f"/api/status/{task_id}/"
+        }, status=202)
+
+    def process_caption_logic(self, task_id, file_list):
+        service = BLIPDecoderService() # Singleton ensures model isn't reloaded
+        log_data = []
+        
+        folder_path = os.path.join(settings.MEDIA_ROOT, "caption_results", str(task_id))
+        os.makedirs(folder_path, exist_ok=True)
+        zip_path = os.path.join(folder_path, f"captions_{task_id}.zip")
+
+        try:
+            with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
+                for item in file_list:
+                    try:
+                        img = Image.open(io.BytesIO(item["content"])).convert("RGB")
+                        caption = service.generate_caption(img)
+                        
+                        txt_name = item["name"].rsplit(".", 1)[0] + ".txt"
+                        zipf.writestr(txt_name, caption)
+                        log_data.append([item["name"], "SUCCESS", caption])
+                    except Exception as e:
+                        log_data.append([item["name"], "FAIL", str(e)])
+
+                # CSV Report
+                csv_buf = io.StringIO()
+                writer = csv.writer(csv_buf)
+                writer.writerow(["Filename", "Status", "Caption"])
+                writer.writerows(log_data)
+                zipf.writestr("captions_report.csv", csv_buf.getvalue())
+
+            task = BackgroundTask.objects.get(task_id=task_id)
+            task.status = "COMPLETED"
+            task.zip_file = f"caption_results/{task_id}/captions_{task_id}.zip"
+            task.save()
+
+        except Exception as e:
+            task = BackgroundTask.objects.get(task_id=task_id)
+            task.status = "FAILED"
+            task.error_message = str(e)
+            task.save()

+ 1 - 1
content_quality_tool/settings.py

@@ -98,7 +98,7 @@ AUTH_PASSWORD_VALIDATORS = [
 # Internationalization
 # https://docs.djangoproject.com/en/5.2/topics/i18n/
 LANGUAGE_CODE = 'en-us'
-# TIME_ZONE = 'UTC'
+#TIME_ZONE = 'UTC'
 TIME_ZONE = 'Asia/Kolkata'
 USE_I18N = True
 USE_TZ = True

+ 2 - 0
manage.py

@@ -3,6 +3,8 @@
 import os
 import sys
 
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(BASE_DIR)
 
 def main():
     """Run administrative tasks."""