UNPKG

@aivue/image-caption

Version:

AI-powered image captioning for Vue.js applications using Hugging Face BLIP models

1,174 lines (1,173 loc) 41 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { createCompatComponent, createCompatPlugin, registerCompatComponent } from "@aivue/core"; import { reactive, ref, computed, createElementBlock, openBlock, createElementVNode, createCommentVNode, createTextVNode, withDirectives, Fragment, renderList, toDisplayString, vModelSelect, normalizeClass, withModifiers, withKeys, vModelText, onMounted, watch } from "vue"; const AVAILABLE_MODELS$1 = [ { id: "nlpconnect/vit-gpt2-image-captioning", name: "ViT-GPT2 (Recommended)", description: "Vision Transformer + GPT2 for image captioning - Most reliable", provider: "huggingface", type: "image-to-text", maxImageSize: 5 * 1024 * 1024, supportedFormats: ["jpeg", "jpg", "png"] }, { id: "Salesforce/blip-image-captioning-base", name: "BLIP Base", description: "Salesforce BLIP base model for image captioning", provider: "huggingface", type: "image-to-text", maxImageSize: 10 * 1024 * 1024, // 10MB supportedFormats: ["jpeg", "jpg", "png", "webp"] }, { id: "Salesforce/blip-image-captioning-large", name: "BLIP Large", description: "Salesforce BLIP large model for better accuracy", provider: "huggingface", type: "image-to-text", maxImageSize: 10 * 1024 * 1024, supportedFormats: ["jpeg", "jpg", "png", "webp"] }, { id: "microsoft/git-base-coco", name: "GIT Base", description: "Microsoft GIT model trained on COCO dataset", provider: "huggingface", type: "image-to-text", maxImageSize: 8 * 1024 * 1024, supportedFormats: ["jpeg", "jpg", "png", "webp"] } ]; const DEFAULT_CONFIG = { model: "nlpconnect/vit-gpt2-image-captioning", maxLength: 50, minLength: 5, temperature: 1, topK: 50, topP: 0.9, repetitionPenalty: 1, useCache: true, waitForModel: true }; const SUPPORTED_IMAGE_TYPES = [ "image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif" ]; const MAX_IMAGE_SIZE = 10 * 1024 * 1024; const MAX_BATCH_SIZE = 10; const AVAILABLE_MODELS = [ { id: "gpt-4o", name: "GPT-4o", description: "Latest and most capable vision model with multimodal understanding", provider: "OpenAI", maxTokens: 4096, costPer1000Tokens: 5e-3 }, { id: "gpt-4o-mini", name: "GPT-4o Mini", description: "Faster and more cost-effective vision model", provider: "OpenAI", maxTokens: 16384, costPer1000Tokens: 15e-5 }, { id: "gpt-4-turbo", name: "GPT-4 Turbo", description: "High-intelligence vision model with detailed analysis", provider: "OpenAI", maxTokens: 4096, costPer1000Tokens: 0.01 } ]; function useImageCaption(options = {}) { var _a, _b, _c; const { config: initialConfig = {}, autoCaption = false, saveHistory = true, maxHistorySize = 50 } = options; const state = reactive({ isLoading: false, isProcessing: false, error: null, result: null, history: [], currentImage: null }); const config = ref({ apiKey: initialConfig.apiKey || typeof window !== "undefined" && ((_c = (_b = (_a = window.import) == null ? void 0 : _a.meta) == null ? void 0 : _b.env) == null ? void 0 : _c.VITE_OPENAI_API_KEY) || "", model: "gpt-4o", ...initialConfig }); const isLoading = computed(() => state.isLoading); const isProcessing = computed(() => state.isProcessing); const error = computed(() => state.error); const result = computed(() => state.result); const history = computed(() => state.history); const currentImage = computed(() => state.currentImage); const validateImage = (file) => { const maxSize = 20 * 1024 * 1024; const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"]; if (!allowedTypes.includes(file.type.toLowerCase())) { return { valid: false, error: `Invalid file type: ${file.type}. Please use JPEG, PNG, WebP, or GIF.` }; } if (file.size > maxSize) { return { valid: false, error: `File too large: ${(file.size / 1024 / 1024).toFixed(2)}MB. Maximum size is 20MB.` }; } if (file.size === 0) { return { valid: false, error: "File is empty or corrupted." }; } return { valid: true }; }; const fileToBase64 = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); }; const urlToBase64 = async (url) => { try { const response = await fetch(url); const blob = await response.blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } catch (error2) { throw new Error(`Failed to load image from URL: ${error2}`); } }; const generateCaption = async (image, options2 = {}) => { var _a2, _b2, _c2, _d, _e, _f, _g; const startTime = Date.now(); state.isProcessing = true; state.error = null; try { if (!((_a2 = config.value.apiKey) == null ? void 0 : _a2.startsWith("sk-"))) { throw new Error('Please provide a valid OpenAI API key (starts with "sk-")'); } let imageData; if (typeof image === "string") { if (image.startsWith("http")) { imageData = await urlToBase64(image); } else { imageData = image; } } else { const validation = validateImage(image); if (!validation.valid) { throw new Error(validation.error); } imageData = await fileToBase64(image); state.currentImage = imageData; } if (!imageData.startsWith("data:image/")) { throw new Error("Invalid image format. Expected base64 data URL."); } const base64Size = imageData.length * 0.75; if (base64Size > 20 * 1024 * 1024) { throw new Error("Image too large after encoding. Please use a smaller image."); } console.log("🚀 Making API call to OpenAI Vision"); console.log("🔑 Using OpenAI API key:", config.value.apiKey ? "Yes (length: " + config.value.apiKey.length + ")" : "No"); console.log("📸 Image data type:", typeof imageData); console.log("📸 Image data length:", imageData.length); console.log("📸 Image data starts with:", imageData.substring(0, 50) + "..."); const model = options2.model || config.value.model || "gpt-4o"; const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${config.value.apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ model, messages: [ { role: "user", content: [ { type: "text", text: options2.prompt || "Describe this image in detail. Focus on the main subjects, actions, and setting." }, { type: "image_url", image_url: { url: imageData, detail: options2.detail || "auto" } } ] } ], max_tokens: options2.maxTokens || 300, temperature: options2.temperature || 0.7 }) }); if (!response.ok) { let errorMessage = `OpenAI API error: ${response.status} ${response.statusText}`; try { const errorData = await response.json(); console.error("❌ OpenAI API Error Response:", errorData); errorMessage = ((_b2 = errorData.error) == null ? void 0 : _b2.message) || errorMessage; if (response.status === 401) { errorMessage = "Invalid OpenAI API key. Please check your API key."; } else if (response.status === 403) { errorMessage = "Access denied. Please ensure your API key has the correct permissions."; } else if (response.status === 429) { errorMessage = "Rate limit exceeded. Please try again later."; } else if (response.status === 400) { if ((_d = (_c2 = errorData.error) == null ? void 0 : _c2.message) == null ? void 0 : _d.includes("image")) { errorMessage = `Image processing error: ${errorData.error.message}`; } else { errorMessage = `Invalid request: ${((_e = errorData.error) == null ? void 0 : _e.message) || "Please check your image format and size."}`; } } } catch (e) { console.error("Could not parse error response:", e); } console.error("❌ Final error message:", errorMessage); throw new Error(errorMessage); } const data = await response.json(); const caption = ((_g = (_f = data.choices[0]) == null ? void 0 : _f.message) == null ? void 0 : _g.content) || "No caption generated"; console.log("✅ OpenAI Vision API call successful"); const processingTime = Date.now() - startTime; const result2 = { caption, model, processingTime, timestamp: /* @__PURE__ */ new Date() }; state.result = result2; if (saveHistory) { const historyItem = { id: `caption_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, image: imageData, caption, model, timestamp: /* @__PURE__ */ new Date(), processingTime }; state.history.unshift(historyItem); if (state.history.length > maxHistorySize) { state.history = state.history.slice(0, maxHistorySize); } } return result2; } catch (error2) { const errorMessage = error2 instanceof Error ? error2.message : "Unknown error occurred"; state.error = errorMessage; throw error2; } finally { state.isProcessing = false; } }; const generateBatchCaptions = async (request) => { const { images, options: options2 = {} } = request; const results = []; const errors = []; const startTime = Date.now(); state.isLoading = true; state.error = null; try { for (let i = 0; i < images.length; i++) { try { const result2 = await generateCaption(images[i], options2); results.push(result2); } catch (error2) { errors.push({ index: i, error: { message: error2 instanceof Error ? error2.message : "Unknown error", code: "CAPTION_GENERATION_FAILED", timestamp: /* @__PURE__ */ new Date() } }); } } return { results, errors, totalProcessed: images.length, successCount: results.length, errorCount: errors.length, processingTime: Date.now() - startTime }; } finally { state.isLoading = false; } }; const uploadImage = async (file) => { const validation = validateImage(file); if (!validation.valid) { throw new Error(validation.error); } const dataUrl = await fileToBase64(file); state.currentImage = dataUrl; const uploadEvent = { file, dataUrl, size: file.size, type: file.type, name: file.name, timestamp: /* @__PURE__ */ new Date() }; if (autoCaption) { try { await generateCaption(file); } catch (error2) { console.warn("Auto-caption failed:", error2); } } return uploadEvent; }; const processImageUrl = async (url) => { try { const dataUrl = await urlToBase64(url); state.currentImage = dataUrl; if (autoCaption) { try { await generateCaption(url); } catch (error2) { console.warn("Auto-caption failed:", error2); } } return dataUrl; } catch (error2) { throw new Error(`Failed to process image URL: ${error2}`); } }; const clearHistory = () => { state.history = []; }; const clearError = () => { state.error = null; }; const clearResult = () => { state.result = null; }; const setConfig = (newConfig) => { config.value = { ...config.value, ...newConfig }; }; const getModelInfo = (modelId) => { return AVAILABLE_MODELS.find((model) => model.id === modelId); }; const exportHistory = (format) => { if (format === "json") { return JSON.stringify(state.history, null, 2); } else { const headers = ["id", "caption", "model", "timestamp", "processingTime"]; const rows = state.history.map((item) => [ item.id, `"${item.caption.replace(/"/g, '""')}"`, // Escape quotes item.model, item.timestamp.toISOString(), item.processingTime.toString() ]); return [headers, ...rows].map((row) => row.join(",")).join("\n"); } }; return { // State isLoading, isProcessing, error, result, history, currentImage, // Methods generateCaption, generateBatchCaptions, uploadImage, processImageUrl, clearHistory, clearError, clearResult, setConfig, getModelInfo, exportHistory }; } const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _sfc_main = { name: "AiImageCaption", props: { apiKey: { type: String, default: "" }, model: { type: String, default: "gpt-4o" }, autoCaption: { type: Boolean, default: false }, maxHistorySize: { type: Number, default: 20 } }, emits: [ "caption-generated", "caption-error", "image-uploaded", "image-removed" ], setup(props, { emit }) { const fileInput = ref(null); const imageUrl = ref(""); const selectedModel = ref(props.model); const isDragOver = ref(false); const imageCaption = useImageCaption({ config: { apiKey: props.apiKey || void 0, model: props.model }, autoCaption: props.autoCaption, maxHistorySize: props.maxHistorySize }); const { state, config, isProcessing, error, result, history, currentImage, generateCaption: generateCaptionFn, uploadImage, processImageUrl, clearHistory: clearHistoryFn, clearError: clearErrorFn, clearResult, setConfig, exportHistory: exportHistoryFn } = imageCaption; const availableModels = computed(() => AVAILABLE_MODELS$1); const triggerFileInput = () => { if (!isProcessing.value && fileInput.value) { fileInput.value.click(); } }; const handleFileSelect = async (event) => { var _a; const file = (_a = event.target.files) == null ? void 0 : _a[0]; if (file) { try { const uploadEvent = await uploadImage(file); emit("image-uploaded", uploadEvent); } catch (error2) { console.error("Upload failed:", error2); } } }; const handleDrop = async (event) => { event.preventDefault(); isDragOver.value = false; const files = event.dataTransfer.files; if (files.length > 0) { try { const uploadEvent = await uploadImage(files[0]); emit("image-uploaded", uploadEvent); } catch (error2) { console.error("Drop upload failed:", error2); } } }; const handleDragOver = (event) => { event.preventDefault(); isDragOver.value = true; }; const handleDragLeave = () => { isDragOver.value = false; }; const loadImageFromUrl = async () => { if (!imageUrl.value.trim()) return; try { await processImageUrl(imageUrl.value); imageUrl.value = ""; emit("image-uploaded", { url: imageUrl.value }); } catch (error2) { console.error("URL load failed:", error2); } }; const removeImage = () => { state.value.currentImage = null; clearResult(); clearErrorFn(); emit("image-removed"); }; const generateCaption = async () => { if (!currentImage.value) return; try { const result2 = await generateCaptionFn(currentImage.value, { model: selectedModel.value }); emit("caption-generated", result2); } catch (error2) { console.error("Caption generation error:", error2); emit("caption-error", error2); } }; const updateModel = () => { setConfig({ model: selectedModel.value }); }; const copyCaption = async () => { var _a; if ((_a = result.value) == null ? void 0 : _a.caption) { try { await navigator.clipboard.writeText(result.value.caption); } catch (error2) { console.error("Copy failed:", error2); } } }; const shareCaption = () => { var _a; if (((_a = result.value) == null ? void 0 : _a.caption) && navigator.share) { navigator.share({ title: "AI Generated Caption", text: result.value.caption }); } }; const clearAll = () => { removeImage(); clearHistoryFn(); }; const clearError = () => { clearErrorFn(); }; const clearHistory = () => { clearHistoryFn(); }; const exportHistory = (format) => { const data = exportHistoryFn(format); const blob = new Blob([data], { type: format === "json" ? "application/json" : "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `image-captions-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${format}`; a.click(); URL.revokeObjectURL(url); }; const loadHistoryItem = (item) => { state.value.currentImage = item.image; state.value.result = { caption: item.caption, model: item.model, processingTime: item.processingTime, timestamp: item.timestamp }; }; const formatTime = (timestamp) => { return new Date(timestamp).toLocaleString(); }; onMounted(() => { if (props.apiKey) { setConfig({ apiKey: props.apiKey }); } }); watch(() => props.apiKey, (newApiKey) => { if (newApiKey) { setConfig({ apiKey: newApiKey }); } }); watch(() => props.model, (newModel) => { if (newModel) { selectedModel.value = newModel; setConfig({ model: newModel }); } }); return { // Refs fileInput, imageUrl, selectedModel, isDragOver, // Computed availableModels, isProcessing, error, result, history, currentImage, // Methods triggerFileInput, handleFileSelect, handleDrop, handleDragOver, handleDragLeave, loadImageFromUrl, removeImage, generateCaption, updateModel, copyCaption, shareCaption, clearAll, clearError, clearHistory, exportHistory, loadHistoryItem, formatTime }; } }; const _hoisted_1 = { class: "ai-image-caption" }; const _hoisted_2 = { class: "image-caption-container" }; const _hoisted_3 = { class: "caption-header" }; const _hoisted_4 = { class: "caption-controls" }; const _hoisted_5 = ["disabled"]; const _hoisted_6 = ["value"]; const _hoisted_7 = ["disabled"]; const _hoisted_8 = { class: "upload-section" }; const _hoisted_9 = ["disabled"]; const _hoisted_10 = { key: 0, class: "upload-placeholder" }; const _hoisted_11 = { key: 1, class: "image-preview" }; const _hoisted_12 = ["src"]; const _hoisted_13 = { class: "image-overlay" }; const _hoisted_14 = { class: "url-input-section" }; const _hoisted_15 = { class: "url-input-group" }; const _hoisted_16 = ["disabled"]; const _hoisted_17 = ["disabled"]; const _hoisted_18 = { class: "generate-section" }; const _hoisted_19 = ["disabled"]; const _hoisted_20 = { key: 0, class: "btn-spinner" }; const _hoisted_21 = { key: 1, class: "btn-icon" }; const _hoisted_22 = { key: 0, class: "results-section" }; const _hoisted_23 = { key: 0, class: "error-message" }; const _hoisted_24 = { class: "error-content" }; const _hoisted_25 = { key: 1, class: "result-card" }; const _hoisted_26 = { class: "result-header" }; const _hoisted_27 = { class: "result-meta" }; const _hoisted_28 = { class: "model-badge" }; const _hoisted_29 = { class: "time-badge" }; const _hoisted_30 = { class: "result-content" }; const _hoisted_31 = { class: "caption-text" }; const _hoisted_32 = { class: "result-actions" }; const _hoisted_33 = { key: 1, class: "history-section" }; const _hoisted_34 = { class: "history-header" }; const _hoisted_35 = { class: "history-controls" }; const _hoisted_36 = { class: "history-list" }; const _hoisted_37 = ["onClick"]; const _hoisted_38 = { class: "history-image" }; const _hoisted_39 = ["src"]; const _hoisted_40 = { class: "history-content" }; const _hoisted_41 = { class: "history-caption" }; const _hoisted_42 = { class: "history-meta" }; const _hoisted_43 = { class: "history-time" }; const _hoisted_44 = { class: "history-model" }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return openBlock(), createElementBlock("div", _hoisted_1, [ createElementVNode("div", _hoisted_2, [ createElementVNode("div", _hoisted_3, [ _cache[19] || (_cache[19] = createElementVNode("h3", { class: "caption-title" }, [ createElementVNode("span", { class: "title-icon" }, "🖼️"), createTextVNode(" AI Image Caption ") ], -1)), createElementVNode("div", _hoisted_4, [ withDirectives(createElementVNode("select", { "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => $setup.selectedModel = $event), onChange: _cache[1] || (_cache[1] = (...args) => $setup.updateModel && $setup.updateModel(...args)), class: "model-select", disabled: $setup.isProcessing }, [ (openBlock(true), createElementBlock(Fragment, null, renderList($setup.availableModels, (model) => { return openBlock(), createElementBlock("option", { key: model.id, value: model.id }, toDisplayString(model.name), 9, _hoisted_6); }), 128)) ], 40, _hoisted_5), [ [vModelSelect, $setup.selectedModel] ]), createElementVNode("button", { onClick: _cache[2] || (_cache[2] = (...args) => $setup.clearAll && $setup.clearAll(...args)), class: "clear-btn", disabled: $setup.isProcessing }, _cache[18] || (_cache[18] = [ createElementVNode("span", { class: "btn-icon" }, "🗑️", -1), createTextVNode(" Clear ") ]), 8, _hoisted_7) ]) ]), createElementVNode("div", _hoisted_8, [ createElementVNode("div", { class: normalizeClass(["upload-area", { "upload-active": $setup.isDragOver, "upload-disabled": $setup.isProcessing, "has-image": $setup.currentImage }]), onDrop: _cache[5] || (_cache[5] = (...args) => $setup.handleDrop && $setup.handleDrop(...args)), onDragover: _cache[6] || (_cache[6] = (...args) => $setup.handleDragOver && $setup.handleDragOver(...args)), onDragleave: _cache[7] || (_cache[7] = (...args) => $setup.handleDragLeave && $setup.handleDragLeave(...args)), onClick: _cache[8] || (_cache[8] = (...args) => $setup.triggerFileInput && $setup.triggerFileInput(...args)) }, [ createElementVNode("input", { ref: "fileInput", type: "file", accept: "image/*", onChange: _cache[3] || (_cache[3] = (...args) => $setup.handleFileSelect && $setup.handleFileSelect(...args)), class: "file-input", disabled: $setup.isProcessing }, null, 40, _hoisted_9), !$setup.currentImage ? (openBlock(), createElementBlock("div", _hoisted_10, _cache[20] || (_cache[20] = [ createElementVNode("div", { class: "upload-icon" }, "📸", -1), createElementVNode("div", { class: "upload-text" }, [ createElementVNode("p", { class: "upload-primary" }, "Drop an image here or click to upload"), createElementVNode("p", { class: "upload-secondary" }, "Supports JPG, PNG, WebP up to 10MB") ], -1) ]))) : (openBlock(), createElementBlock("div", _hoisted_11, [ createElementVNode("img", { src: $setup.currentImage, alt: "Uploaded image", class: "preview-image" }, null, 8, _hoisted_12), createElementVNode("div", _hoisted_13, [ createElementVNode("button", { onClick: _cache[4] || (_cache[4] = withModifiers((...args) => $setup.removeImage && $setup.removeImage(...args), ["stop"])), class: "remove-btn" }, _cache[21] || (_cache[21] = [ createElementVNode("span", null, "✕", -1) ])) ]) ])) ], 34), createElementVNode("div", _hoisted_14, [ createElementVNode("div", _hoisted_15, [ withDirectives(createElementVNode("input", { "onUpdate:modelValue": _cache[9] || (_cache[9] = ($event) => $setup.imageUrl = $event), type: "url", placeholder: "Or paste an image URL...", class: "url-input", disabled: $setup.isProcessing, onKeyup: _cache[10] || (_cache[10] = withKeys((...args) => $setup.loadImageFromUrl && $setup.loadImageFromUrl(...args), ["enter"])) }, null, 40, _hoisted_16), [ [vModelText, $setup.imageUrl] ]), createElementVNode("button", { onClick: _cache[11] || (_cache[11] = (...args) => $setup.loadImageFromUrl && $setup.loadImageFromUrl(...args)), class: "url-load-btn", disabled: $setup.isProcessing || !$setup.imageUrl.trim() }, " Load ", 8, _hoisted_17) ]) ]) ]), createElementVNode("div", _hoisted_18, [ createElementVNode("button", { onClick: _cache[12] || (_cache[12] = (...args) => $setup.generateCaption && $setup.generateCaption(...args)), class: normalizeClass(["generate-btn", { "btn-loading": $setup.isProcessing }]), disabled: !$setup.currentImage || $setup.isProcessing }, [ $setup.isProcessing ? (openBlock(), createElementBlock("span", _hoisted_20, "⏳")) : (openBlock(), createElementBlock("span", _hoisted_21, "✨")), createTextVNode(" " + toDisplayString($setup.isProcessing ? "Generating..." : "Generate Caption"), 1) ], 10, _hoisted_19) ]), $setup.result || $setup.error ? (openBlock(), createElementBlock("div", _hoisted_22, [ $setup.error ? (openBlock(), createElementBlock("div", _hoisted_23, [ _cache[23] || (_cache[23] = createElementVNode("div", { class: "error-icon" }, "❌", -1)), createElementVNode("div", _hoisted_24, [ _cache[22] || (_cache[22] = createElementVNode("h4", null, "Caption Generation Failed", -1)), createElementVNode("p", null, toDisplayString($setup.error), 1), createElementVNode("button", { onClick: _cache[13] || (_cache[13] = (...args) => $setup.clearError && $setup.clearError(...args)), class: "error-dismiss" }, "Dismiss") ]) ])) : createCommentVNode("", true), $setup.result ? (openBlock(), createElementBlock("div", _hoisted_25, [ createElementVNode("div", _hoisted_26, [ _cache[24] || (_cache[24] = createElementVNode("h4", null, "Generated Caption", -1)), createElementVNode("div", _hoisted_27, [ createElementVNode("span", _hoisted_28, toDisplayString($setup.result.model.split("/").pop()), 1), createElementVNode("span", _hoisted_29, toDisplayString($setup.result.processingTime) + "ms", 1) ]) ]), createElementVNode("div", _hoisted_30, [ createElementVNode("p", _hoisted_31, toDisplayString($setup.result.caption), 1), createElementVNode("div", _hoisted_32, [ createElementVNode("button", { onClick: _cache[14] || (_cache[14] = (...args) => $setup.copyCaption && $setup.copyCaption(...args)), class: "action-btn copy-btn" }, _cache[25] || (_cache[25] = [ createElementVNode("span", { class: "btn-icon" }, "📋", -1), createTextVNode(" Copy ") ])), createElementVNode("button", { onClick: _cache[15] || (_cache[15] = (...args) => $setup.shareCaption && $setup.shareCaption(...args)), class: "action-btn share-btn" }, _cache[26] || (_cache[26] = [ createElementVNode("span", { class: "btn-icon" }, "🔗", -1), createTextVNode(" Share ") ])) ]) ]) ])) : createCommentVNode("", true) ])) : createCommentVNode("", true), $setup.history.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_33, [ createElementVNode("div", _hoisted_34, [ _cache[29] || (_cache[29] = createElementVNode("h4", null, "Caption History", -1)), createElementVNode("div", _hoisted_35, [ createElementVNode("button", { onClick: _cache[16] || (_cache[16] = ($event) => $setup.exportHistory("json")), class: "export-btn" }, _cache[27] || (_cache[27] = [ createElementVNode("span", { class: "btn-icon" }, "📥", -1), createTextVNode(" Export JSON ") ])), createElementVNode("button", { onClick: _cache[17] || (_cache[17] = (...args) => $setup.clearHistory && $setup.clearHistory(...args)), class: "clear-history-btn" }, _cache[28] || (_cache[28] = [ createElementVNode("span", { class: "btn-icon" }, "🗑️", -1), createTextVNode(" Clear History ") ])) ]) ]), createElementVNode("div", _hoisted_36, [ (openBlock(true), createElementBlock(Fragment, null, renderList($setup.history.slice(0, 5), (item) => { return openBlock(), createElementBlock("div", { key: item.id, class: "history-item", onClick: ($event) => $setup.loadHistoryItem(item) }, [ createElementVNode("div", _hoisted_38, [ createElementVNode("img", { src: item.image, alt: "Historical image" }, null, 8, _hoisted_39) ]), createElementVNode("div", _hoisted_40, [ createElementVNode("p", _hoisted_41, toDisplayString(item.caption), 1), createElementVNode("div", _hoisted_42, [ createElementVNode("span", _hoisted_43, toDisplayString($setup.formatTime(item.timestamp)), 1), createElementVNode("span", _hoisted_44, toDisplayString(item.model.split("/").pop()), 1) ]) ]) ], 8, _hoisted_37); }), 128)) ]) ])) : createCommentVNode("", true) ]) ]); } const AiImageCaptionComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]); class ImageCaptionClient { constructor(config = {}) { __publicField(this, "config"); this.config = { ...DEFAULT_CONFIG, ...config }; } async generateCaption(imageData, options = {}) { const startTime = Date.now(); if (!this.config.apiKey) { throw new Error("Hugging Face API key is required"); } const model = options.model || this.config.model || "gpt-4o"; const modelsToTry = [ model, "nlpconnect/vit-gpt2-image-captioning", "Salesforce/blip-image-captioning-base" ].filter((m, i, arr) => arr.indexOf(m) === i); let lastError = null; for (const currentModel of modelsToTry) { try { console.log("Trying model:", currentModel); const result = await this.tryGenerateCaption(imageData, currentModel, options, startTime); return result; } catch (error) { console.warn(`Model ${currentModel} failed:`, error); lastError = error; continue; } } throw lastError || new Error("All models failed to generate caption"); } async tryGenerateCaption(imageData, model, options, startTime) { var _a, _b; let apiUrl = "https://api.openai.com/v1/chat/completions"; console.log("Using model:", model); console.log("Full API URL:", apiUrl); let imageBytes; if (typeof imageData === "string") { const base64Data = imageData.includes(",") ? imageData.split(",")[1] : imageData; const binaryString = atob(base64Data); imageBytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { imageBytes[i] = binaryString.charCodeAt(i); } } else { imageBytes = imageData; } const requestOptions = { method: "POST", headers: { "Authorization": `Bearer ${this.config.apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ model, messages: [ { role: "user", content: [ { type: "text", text: options.prompt || "Describe this image in detail. Focus on the main subjects, actions, and setting." }, { type: "image_url", image_url: { url: imageData, detail: options.detail || "auto" } } ] } ], max_tokens: options.maxTokens || 300, temperature: options.temperature || 0.7 }) }; try { console.log("Making request to:", apiUrl); console.log("Request headers:", requestOptions.headers); console.log("Image data type:", typeof imageData); const response = await fetch(apiUrl, requestOptions); console.log("Response status:", response.status, response.statusText); if (!response.ok) { let errorMessage = `API request failed: ${response.status} ${response.statusText}`; try { const errorData = await response.json(); console.log("Error response:", errorData); errorMessage = errorData.error || errorData.message || errorMessage; } catch (e) { console.log("Could not parse error response"); } if (response.status === 404) { errorMessage = `Model "${model}" not found or not accessible. This may be due to CORS restrictions in the browser. For production use, implement a backend proxy.`; } throw new Error(errorMessage); } const data = await response.json(); console.log("API response:", data); if (!data || !data.choices || !Array.isArray(data.choices) || data.choices.length === 0) { throw new Error("Invalid response from OpenAI API"); } const caption = ((_b = (_a = data.choices[0]) == null ? void 0 : _a.message) == null ? void 0 : _b.content) || "No caption generated"; const processingTime = Date.now() - startTime; return { caption, model, processingTime, timestamp: /* @__PURE__ */ new Date() }; } catch (error) { console.error("Caption generation error:", error); throw new Error(`Caption generation failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } getConfig() { return { ...this.config }; } async testConnection() { try { if (!this.config.apiKey) { return false; } if (!this.config.apiKey.startsWith("sk-") || this.config.apiKey.length < 40) { return false; } return true; } catch (error) { console.warn("Connection test failed:", error); return false; } } } function createImageCaptionClient(config = {}) { return new ImageCaptionClient(config); } function validateImageFile(file) { const warnings = []; if (!SUPPORTED_IMAGE_TYPES.includes(file.type)) { return { valid: false, error: `Unsupported image type: ${file.type}. Supported types: ${SUPPORTED_IMAGE_TYPES.join(", ")}` }; } if (file.size > MAX_IMAGE_SIZE) { return { valid: false, error: `Image size too large: ${(file.size / 1024 / 1024).toFixed(2)}MB. Maximum size: ${MAX_IMAGE_SIZE / 1024 / 1024}MB` }; } if (file.size > 5 * 1024 * 1024) { warnings.push("Large image file may take longer to process"); } return new Promise((resolve) => { const img = new Image(); img.onload = () => { if (img.width > 2048 || img.height > 2048) { warnings.push("Large image dimensions may affect processing speed"); } resolve({ valid: true, warnings }); }; img.onerror = () => { resolve({ valid: true, warnings }); }; img.src = URL.createObjectURL(file); }); } function resizeImageFile(file, options = {}) { const { maxWidth = 1024, maxHeight = 1024, quality = 0.8, format = "jpeg", resize = true } = options; return new Promise((resolve, reject) => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); if (!ctx) { reject(new Error("Canvas context not available")); return; } img.onload = () => { let { width, height } = img; if (resize && (width > maxWidth || height > maxHeight)) { const ratio = Math.min(maxWidth / width, maxHeight / height); width = Math.floor(width * ratio); height = Math.floor(height * ratio); } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); canvas.toBlob( (blob) => { if (blob) { const resizedFile = new File([blob], file.name, { type: `image/${format}`, lastModified: Date.now() }); resolve(resizedFile); } else { reject(new Error("Failed to resize image")); } }, `image/${format}`, quality ); URL.revokeObjectURL(img.src); }; img.onerror = () => { URL.revokeObjectURL(img.src); reject(new Error("Failed to load image for resizing")); }; img.src = URL.createObjectURL(file); }); } function convertToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = reader.result; resolve(result); }; reader.onerror = () => { reject(new Error("Failed to convert file to base64")); }; reader.readAsDataURL(file); }); } const AiImageCaption = createCompatComponent(AiImageCaptionComponent); const ImageCaptionPlugin = createCompatPlugin({ install(app, options = {}) { registerCompatComponent(app, "AiImageCaption", AiImageCaption); if (options.globalConfig || options.apiKey || options.defaultModel) { const globalConfig = { ...options.globalConfig, ...options.apiKey && { apiKey: options.apiKey }, ...options.defaultModel && { model: options.defaultModel } }; app.provide("imageCaption:config", globalConfig); } } }); const index = { AiImageCaption, useImageCaption, ImageCaptionPlugin, AVAILABLE_MODELS: AVAILABLE_MODELS$1, DEFAULT_CONFIG }; export { AVAILABLE_MODELS$1 as AVAILABLE_MODELS, AiImageCaption, DEFAULT_CONFIG, ImageCaptionPlugin, MAX_BATCH_SIZE, MAX_IMAGE_SIZE, SUPPORTED_IMAGE_TYPES, convertToBase64, createImageCaptionClient, index as default, resizeImageFile, useImageCaption, validateImageFile }; //# sourceMappingURL=index.js.map