@aivue/image-caption
Version:
AI-powered image captioning for Vue.js applications using Hugging Face BLIP models
1,172 lines (1,171 loc) • 40.8 kB
JavaScript
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 { 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 AiImageCaption = /* @__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 ImageCaptionPlugin = {
install(app, options = {}) {
app.component("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