react-a11y-auto-caption 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -78
- package/dist/index.d.ts +18 -0
- package/dist/index.js +213 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +185 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next.d.ts +13 -0
- package/dist/next.js +221 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +186 -0
- package/dist/next.mjs.map +1 -0
- package/dist/useAICaption.d.ts +30 -0
- package/package.json +70 -41
- package/src/index.tsx +0 -161
- package/src/next-env.d.ts +0 -2
- package/src/next.tsx +0 -154
- package/tsconfig.json +0 -19
- package/tsup.config.ts +0 -11
package/README.md
CHANGED
|
@@ -1,53 +1,66 @@
|
|
|
1
1
|
# react-a11y-auto-caption
|
|
2
2
|
|
|
3
3
|
> A smart React & Next.js component <br/>
|
|
4
|
-
that automatically generates highly accurate `alt` text for images using AI.<br/>
|
|
5
|
-
**Generate captions effortlessly during local development, <br/>
|
|
6
|
-
save them to your database, and serve 100% accessible images in production<br/>
|
|
7
|
-
with zero API costs and zero latency.**
|
|
4
|
+
> that automatically generates highly accurate `alt` text for images using AI.<br/>
|
|
5
|
+
> **Generate captions effortlessly during local development, <br/>
|
|
6
|
+
> save them to your database, and serve 100% accessible images in production<br/>
|
|
7
|
+
> with zero API costs and zero latency.**
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/react-a11y-auto-caption)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
[]()
|
|
12
12
|
|
|
13
|
+
---
|
|
14
|
+
|
|
13
15
|
## Why use this library?
|
|
14
16
|
|
|
15
|
-
- **Generate Once, Serve Forever (Best Practice):** Automatically generate captions during local development, easily save them to your database using the `onCaptionGenerated` callback, and completely bypass AI requests in production.
|
|
17
|
+
- **Generate Once, Serve Forever (Best Practice):** Automatically generate captions during local development, easily save them to your database using the `onCaptionGenerated` callback, and completely bypass AI requests in production.
|
|
16
18
|
- **Cost-Free Local AI Server:** Comes with a ready-to-use, lightweight FastAPI Python server. It runs perfectly on your local machine, meaning you don't need to pay for expensive cloud GPUs or third-party API subscriptions (like OpenAI).
|
|
17
19
|
- **Zero-Config Accessibility:** Automatically describes images for screen readers without manual data entry.
|
|
18
|
-
- **First-Class Next.js Support:** Provides a dedicated `<SmartNextImage>` component optimized for Next.js.
|
|
19
|
-
- **Smart
|
|
20
|
+
- **First-Class Next.js Support:** Provides a dedicated `<SmartNextImage>` component optimized for Next.js, supporting both static imports and URL strings.
|
|
21
|
+
- **Smart LRU Caching:** Built-in LRU memory cache (max 500 entries) prevents duplicate API calls and unbounded memory growth.
|
|
20
22
|
- **Concurrent Request Defense:** Safely handles simultaneous renders of the same image across multiple components.
|
|
23
|
+
- **Reliable Error Handling:** Exposes errors via both `onCaptionError` callback and returned `error` state for full control over failure UX.
|
|
21
24
|
- **Global Provider Pattern:** Set your API endpoint once at the root level and forget about it.
|
|
22
25
|
- **Developer Experience (DX):** Built-in test mode (`disableAI`) and intelligent console warnings for smooth debugging.
|
|
23
26
|
|
|
24
27
|
---
|
|
25
28
|
|
|
29
|
+
## What's New in v1.1.0
|
|
30
|
+
|
|
31
|
+
- **LRU Cache:** Replaced unbounded `Map` cache with an LRU implementation to prevent memory leaks in long-running apps.
|
|
32
|
+
- **`onCaptionError` callback:** New prop to handle caption generation failures externally (e.g. logging, toast notifications).
|
|
33
|
+
- **`error` state:** `useAICaptions` hook now returns an `error` field so you can branch your UI on failure.
|
|
34
|
+
- **Unmount safety:** Caption generation is now safely cancelled when a component unmounts, preventing stale state updates.
|
|
35
|
+
- **Next.js static import support:** `<SmartNextImage>` now correctly resolves both `StaticImageData` and `StaticRequire` import types.
|
|
36
|
+
- **Subpath exports:** Added `react-a11y-auto-caption/next` entry point for cleaner Next.js-specific imports.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
26
40
|
## Installation
|
|
27
41
|
|
|
28
|
-
npm
|
|
29
42
|
```bash
|
|
43
|
+
# npm
|
|
30
44
|
npm install react-a11y-auto-caption
|
|
31
|
-
|
|
32
|
-
yarn
|
|
33
|
-
```bash
|
|
45
|
+
|
|
46
|
+
# yarn
|
|
34
47
|
yarn add react-a11y-auto-caption
|
|
35
|
-
|
|
36
|
-
pnpm
|
|
37
|
-
```bash
|
|
48
|
+
|
|
49
|
+
# pnpm
|
|
38
50
|
pnpm add react-a11y-auto-caption
|
|
39
51
|
```
|
|
40
52
|
|
|
41
53
|
---
|
|
54
|
+
|
|
42
55
|
## Quick Start
|
|
43
56
|
|
|
44
|
-
### 1. Wrap your app with the Provider (Optional but Recommended)
|
|
45
|
-
Set your backend API endpoint once globally. <br/>
|
|
46
|
-
Otherwise, you can simply pass apiEndpoint as a prop when using `<SmartImage>` <br/>
|
|
57
|
+
### 1. Wrap your app with the Provider (Optional but Recommended)
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
Set your backend API endpoint once globally. Otherwise, pass `apiEndpoint` directly as a prop.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
49
62
|
// App.tsx or layout.tsx
|
|
50
|
-
import { SmartImageProvider } from
|
|
63
|
+
import { SmartImageProvider } from "react-a11y-auto-caption";
|
|
51
64
|
|
|
52
65
|
export default function App({ children }) {
|
|
53
66
|
return (
|
|
@@ -57,96 +70,99 @@ export default function App({ children }) {
|
|
|
57
70
|
);
|
|
58
71
|
}
|
|
59
72
|
```
|
|
73
|
+
|
|
60
74
|
### 2. Use it in your components
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
|
|
76
|
+
**Vanilla React (Vite, CRA):**
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { SmartImage } from "react-a11y-auto-caption";
|
|
64
80
|
|
|
65
81
|
function Gallery() {
|
|
66
|
-
return
|
|
67
|
-
<SmartImage
|
|
68
|
-
src="[https://example.com/beautiful-landscape.jpg](https://example.com/beautiful-landscape.jpg)"
|
|
69
|
-
announceLive={true}
|
|
70
|
-
/>
|
|
71
|
-
);
|
|
82
|
+
return <SmartImage src="https://example.com/beautiful-landscape.jpg" announceLive={true} />;
|
|
72
83
|
}
|
|
73
84
|
```
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
import
|
|
86
|
+
**Next.js:**
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { SmartNextImage } from "react-a11y-auto-caption/next";
|
|
90
|
+
import dogPic from "../public/dog.jpg";
|
|
79
91
|
|
|
80
92
|
function NextGallery() {
|
|
81
|
-
return
|
|
82
|
-
<SmartNextImage
|
|
83
|
-
src={dogPic}
|
|
84
|
-
width={500}
|
|
85
|
-
height={300}
|
|
86
|
-
// You can override global settings per component
|
|
87
|
-
disableAI={process.env.NODE_ENV === 'development'}
|
|
88
|
-
/>
|
|
89
|
-
);
|
|
93
|
+
return <SmartNextImage src={dogPic} width={500} height={300} disableAI={process.env.NODE_ENV === "production"} />;
|
|
90
94
|
}
|
|
91
95
|
```
|
|
92
|
-
---
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
Both `SmartImage` and `SmartNextImage` inherit all standard `<img>` / `next/image` props, plus the following: <br/>
|
|
96
|
-
## API Reference (Props)
|
|
97
|
+
---
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
## API Reference
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
| :--- | :--- | :--- | :--- |
|
|
102
|
-
| `apiEndpoint` | `string` | `undefined` | The URL of your AI backend API. Overrides the `SmartImageProvider`'s endpoint if provided. |
|
|
103
|
-
| `alt` | `string` | `undefined` | Manual alt text. If provided, the AI generation is completely bypassed. |
|
|
104
|
-
| `fallbackAlt` | `string` | `"Image loading or caption unavailable"` | The text displayed if the AI API request fails or times out. |
|
|
105
|
-
| `disableAI` | `boolean` | `false` | Disables AI generation and uses a mock caption. Highly recommended for local development and testing. |
|
|
106
|
-
| `announceLive` | `boolean` | `false` | Enables `aria-live` regions to dynamically announce the generation status to screen readers. |
|
|
107
|
-
| `onCaptionGenerated`| `function` | `undefined` | Callback fired when a caption is successfully generated. `(caption: string) => void` |
|
|
101
|
+
Both `<SmartImage>` and `<SmartNextImage>` inherit all standard HTML `<img>` (or `next/image`) attributes, plus the following:
|
|
108
102
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
| Prop | Type | Default | Description |
|
|
104
|
+
| :------------------- | :-------------------------- | :--------------------------------------- | :--------------------------------------------------------------------------------------- |
|
|
105
|
+
| `apiEndpoint` | `string` | `undefined` | The URL of your AI backend API. Overrides the `SmartImageProvider` endpoint if provided. |
|
|
106
|
+
| `alt` | `string` | `undefined` | Manual alt text. If provided, AI generation is completely bypassed. |
|
|
107
|
+
| `fallbackAlt` | `string` | `"Image loading or caption unavailable"` | Text used when the AI request fails or times out. |
|
|
108
|
+
| `disableAI` | `boolean` | `false` | Disables AI generation and uses a mock caption. Recommended for testing. |
|
|
109
|
+
| `announceLive` | `boolean` | `false` | Enables `aria-live` region to announce generation status to screen readers. |
|
|
110
|
+
| `onCaptionGenerated` | `(caption: string) => void` | `undefined` | Callback fired when a caption is successfully generated. |
|
|
111
|
+
| `onCaptionError` | `(error: Error) => void` | `undefined` | Callback fired when caption generation fails. Use for logging or toast notifications. |
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
We provide a ready-to-use FastAPI reference server in [this repository.](https://github.com/kong33/SmartImage)<br/>
|
|
113
|
+
> **Note:** `<SmartNextImage>` requires standard Next.js image props such as `width` and `height` (unless using `fill`).
|
|
115
114
|
|
|
116
|
-
Depending on your architecture, choose one of the following integration methods:<br/>
|
|
117
115
|
---
|
|
118
|
-
### First step: Deploying our Standalone AI Microservice
|
|
119
|
-
For security reasons, all cross-origin requests are blocked by default. <br/>
|
|
120
|
-
You MUST set the ALLOWED_ORIGINS environment variable in your server's .env file<br/>
|
|
121
|
-
to allow your frontend to communicate with it. <br/>
|
|
122
116
|
|
|
117
|
+
## Error Handling
|
|
118
|
+
|
|
119
|
+
You can handle errors in two ways depending on your use case.
|
|
123
120
|
|
|
124
|
-
|
|
125
|
-
# For production
|
|
121
|
+
**Via callback** — for external handling like logging or toasts:
|
|
126
122
|
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
```tsx
|
|
124
|
+
<SmartImage src={imageUrl} onCaptionError={(err) => toast.error(err.message)} />
|
|
129
125
|
```
|
|
130
|
-
```python
|
|
131
|
-
# For local development
|
|
132
126
|
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
**Via `useAICaptions` hook** — for UI branching:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
const { generatedAlt, isGenerating, error } = useAICaptions({ src });
|
|
131
|
+
|
|
132
|
+
if (error) return <p>Failed to generate caption.</p>;
|
|
135
133
|
```
|
|
136
|
-
> **Note:** Change the port if you are using Vite 5173 or another local server. You can allow multiple environments simultaneously by separating them with a comma (no spaces).
|
|
137
134
|
|
|
138
135
|
---
|
|
139
136
|
|
|
140
137
|
## Best Practice: Generate Once, Save, Reuse
|
|
141
|
-
|
|
142
|
-
The
|
|
138
|
+
|
|
139
|
+
Generating captions on-the-fly for every user is slow. The recommended pattern is to generate once and persist to your database:
|
|
143
140
|
|
|
144
141
|
```tsx
|
|
145
|
-
<SmartImage
|
|
142
|
+
<SmartImage
|
|
146
143
|
src={image.url}
|
|
147
|
-
alt={image.savedAlt} // If provided,
|
|
144
|
+
alt={image.savedAlt} // If provided, AI is skipped entirely
|
|
148
145
|
apiEndpoint="http://localhost:8000/api/generate-caption"
|
|
149
146
|
onCaptionGenerated={(text) => {
|
|
150
|
-
saveToDatabase(image.id, text);
|
|
147
|
+
saveToDatabase(image.id, text);
|
|
151
148
|
}}
|
|
152
149
|
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Security & Backend Integration
|
|
155
|
+
|
|
156
|
+
This package requires a backend to process images through an AI model (e.g. ViT-GPT2). We provide a ready-to-use FastAPI reference server in [this repository](https://github.com/kong33/SmartImage).
|
|
157
|
+
|
|
158
|
+
For security, all cross-origin requests are blocked by default. Set the `ALLOWED_ORIGINS` environment variable in your server's `.env` file:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Production
|
|
162
|
+
ALLOWED_ORIGINS=https://your-frontend-domain.com
|
|
163
|
+
|
|
164
|
+
# Local development
|
|
165
|
+
ALLOWED_ORIGINS=http://localhost:3000
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> **Note:** Separate multiple origins with a comma and no spaces. Adjust the port if using Vite (`5173`) or another local server.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React, { ImgHTMLAttributes } from "react";
|
|
2
|
+
export declare const SmartImageProvider: React.FC<{
|
|
3
|
+
value: {
|
|
4
|
+
apiEndpoint?: string;
|
|
5
|
+
disableAI?: boolean;
|
|
6
|
+
};
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}>;
|
|
9
|
+
export interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
|
10
|
+
src?: string;
|
|
11
|
+
apiEndpoint?: string;
|
|
12
|
+
fallbackAlt?: string;
|
|
13
|
+
onCaptionGenerated?: (caption: string) => void;
|
|
14
|
+
disableAI?: boolean;
|
|
15
|
+
announceLive?: boolean;
|
|
16
|
+
onCaptionError?: (error: Error) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const SmartImage: ({ src, alt, apiEndpoint: propsEndpoint, fallbackAlt, onCaptionGenerated, disableAI: propsDisableAI, announceLive, onCaptionError, ...props }: SmartImageProps) => import("react/jsx-runtime").JSX.Element;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.tsx
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
SmartImage: () => SmartImage,
|
|
24
|
+
SmartImageProvider: () => SmartImageProvider
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/useAICaption.ts
|
|
29
|
+
var import_react = require("react");
|
|
30
|
+
var SmartImageContext = (0, import_react.createContext)(void 0);
|
|
31
|
+
var MAX_SIZE = 500;
|
|
32
|
+
var LRUCaptionCache = class {
|
|
33
|
+
constructor() {
|
|
34
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
35
|
+
}
|
|
36
|
+
get(key) {
|
|
37
|
+
return this.cache.get(key);
|
|
38
|
+
}
|
|
39
|
+
set(key, value) {
|
|
40
|
+
if (this.cache.size >= MAX_SIZE) {
|
|
41
|
+
this.cache.delete(this.cache.keys().next().value);
|
|
42
|
+
}
|
|
43
|
+
this.cache.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
has(key) {
|
|
46
|
+
return this.cache.has(key);
|
|
47
|
+
}
|
|
48
|
+
clear() {
|
|
49
|
+
this.cache.clear();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var captionCache = new LRUCaptionCache();
|
|
53
|
+
var pendingRequestCache = /* @__PURE__ */ new Map();
|
|
54
|
+
function isStaticImageData(src) {
|
|
55
|
+
return typeof src === "object" && src !== null && "src" in src;
|
|
56
|
+
}
|
|
57
|
+
function resolveImageUrl(src) {
|
|
58
|
+
if (typeof src === "string") return src;
|
|
59
|
+
if (isStaticImageData(src)) return src.src;
|
|
60
|
+
return src.default.src;
|
|
61
|
+
}
|
|
62
|
+
var log = process.env.NODE_ENV === "development" ? console.log : () => {
|
|
63
|
+
};
|
|
64
|
+
var useAICaptions = ({
|
|
65
|
+
src,
|
|
66
|
+
alt,
|
|
67
|
+
apiEndpoint: propsEndpoint,
|
|
68
|
+
fallbackAlt = "Image loading or caption unavailable",
|
|
69
|
+
onCaptionGenerated,
|
|
70
|
+
disableAI: propsDisableAI,
|
|
71
|
+
onCaptionError
|
|
72
|
+
}) => {
|
|
73
|
+
const context = (0, import_react.useContext)(SmartImageContext);
|
|
74
|
+
const apiEndpoint = propsEndpoint || context?.apiEndpoint;
|
|
75
|
+
const disableAI = propsDisableAI ?? context?.disableAI ?? false;
|
|
76
|
+
const [generatedAlt, setGeneratedAlt] = (0, import_react.useState)("");
|
|
77
|
+
const [isGenerating, setIsGenerating] = (0, import_react.useState)(false);
|
|
78
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
79
|
+
const onCaptionGeneratedRef = (0, import_react.useRef)(onCaptionGenerated);
|
|
80
|
+
const onCaptionErrorRef = (0, import_react.useRef)(onCaptionError);
|
|
81
|
+
(0, import_react.useEffect)(() => {
|
|
82
|
+
onCaptionGeneratedRef.current = onCaptionGenerated;
|
|
83
|
+
onCaptionErrorRef.current = onCaptionError;
|
|
84
|
+
});
|
|
85
|
+
(0, import_react.useEffect)(() => {
|
|
86
|
+
setError(null);
|
|
87
|
+
if (alt) {
|
|
88
|
+
setGeneratedAlt(alt);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!src) return;
|
|
92
|
+
if (disableAI) {
|
|
93
|
+
setGeneratedAlt("[Testing mode: AI caption generation disabled]");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!apiEndpoint) {
|
|
97
|
+
console.warn(
|
|
98
|
+
"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation."
|
|
99
|
+
);
|
|
100
|
+
setGeneratedAlt(fallbackAlt);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const imageUrl = resolveImageUrl(src);
|
|
104
|
+
if (captionCache.has(imageUrl)) {
|
|
105
|
+
log("[SmartImage] Cache hit: Reusing existing caption.");
|
|
106
|
+
const cachedCaption = captionCache.get(imageUrl);
|
|
107
|
+
setGeneratedAlt(cachedCaption);
|
|
108
|
+
if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let cancelled = false;
|
|
112
|
+
const generateCaption = async () => {
|
|
113
|
+
setIsGenerating(true);
|
|
114
|
+
try {
|
|
115
|
+
if (pendingRequestCache.has(imageUrl)) {
|
|
116
|
+
log("[SmartImage] Pending request detected. Waiting for the existing API call to complete.");
|
|
117
|
+
const caption = await pendingRequestCache.get(imageUrl);
|
|
118
|
+
if (cancelled) return;
|
|
119
|
+
setGeneratedAlt(caption);
|
|
120
|
+
if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const fetchPromise = (async () => {
|
|
124
|
+
const imageResponse = await fetch(imageUrl);
|
|
125
|
+
const imageBlob = await imageResponse.blob();
|
|
126
|
+
const imageFile = new File([imageBlob], "image.jpg", {
|
|
127
|
+
type: imageBlob.type || "image/jpeg"
|
|
128
|
+
});
|
|
129
|
+
const formData = new FormData();
|
|
130
|
+
formData.append("file", imageFile);
|
|
131
|
+
const response = await fetch(apiEndpoint, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
body: formData
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) throw new Error("AI API request failed");
|
|
136
|
+
const data = await response.json();
|
|
137
|
+
if (data.caption) return data.caption;
|
|
138
|
+
throw new Error("No caption returned from the API.");
|
|
139
|
+
})();
|
|
140
|
+
pendingRequestCache.set(imageUrl, fetchPromise);
|
|
141
|
+
const newCaption = await fetchPromise;
|
|
142
|
+
pendingRequestCache.delete(imageUrl);
|
|
143
|
+
captionCache.set(imageUrl, newCaption);
|
|
144
|
+
if (cancelled) return;
|
|
145
|
+
setGeneratedAlt(newCaption);
|
|
146
|
+
if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const normalizedError = err instanceof Error ? err : new Error("Unknown error");
|
|
149
|
+
pendingRequestCache.delete(imageUrl);
|
|
150
|
+
if (cancelled) return;
|
|
151
|
+
setError(normalizedError);
|
|
152
|
+
setGeneratedAlt(fallbackAlt);
|
|
153
|
+
if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);
|
|
154
|
+
} finally {
|
|
155
|
+
if (!cancelled) setIsGenerating(false);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
generateCaption();
|
|
159
|
+
return () => {
|
|
160
|
+
cancelled = true;
|
|
161
|
+
};
|
|
162
|
+
}, [src, alt, apiEndpoint, fallbackAlt, disableAI]);
|
|
163
|
+
return { generatedAlt, isGenerating, error };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/index.tsx
|
|
167
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
168
|
+
var SmartImageProvider = ({ value, children }) => {
|
|
169
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SmartImageContext.Provider, { value, children });
|
|
170
|
+
};
|
|
171
|
+
var SR_ONLY_STYLE = {
|
|
172
|
+
position: "absolute",
|
|
173
|
+
width: "1px",
|
|
174
|
+
height: "1px",
|
|
175
|
+
padding: 0,
|
|
176
|
+
margin: "-1px",
|
|
177
|
+
overflow: "hidden",
|
|
178
|
+
clip: "rect(0, 0, 0, 0)",
|
|
179
|
+
whiteSpace: "nowrap",
|
|
180
|
+
borderWidth: 0
|
|
181
|
+
};
|
|
182
|
+
var SmartImage = ({
|
|
183
|
+
src,
|
|
184
|
+
alt,
|
|
185
|
+
apiEndpoint: propsEndpoint,
|
|
186
|
+
fallbackAlt = "Image loading or caption unavailable",
|
|
187
|
+
onCaptionGenerated,
|
|
188
|
+
disableAI: propsDisableAI,
|
|
189
|
+
announceLive = false,
|
|
190
|
+
onCaptionError,
|
|
191
|
+
...props
|
|
192
|
+
}) => {
|
|
193
|
+
const { isGenerating, generatedAlt } = useAICaptions({
|
|
194
|
+
src,
|
|
195
|
+
alt,
|
|
196
|
+
apiEndpoint: propsEndpoint,
|
|
197
|
+
fallbackAlt,
|
|
198
|
+
onCaptionGenerated,
|
|
199
|
+
disableAI: propsDisableAI,
|
|
200
|
+
announceLive,
|
|
201
|
+
onCaptionError
|
|
202
|
+
});
|
|
203
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
204
|
+
announceLive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: SR_ONLY_STYLE, "aria-live": "polite", "aria-atomic": "true", children: isGenerating ? "Generating image description. Please wait..." : generatedAlt ? `Image description generated: ${generatedAlt}` : "" }),
|
|
205
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src, alt: generatedAlt, "aria-busy": isGenerating, ...props })
|
|
206
|
+
] });
|
|
207
|
+
};
|
|
208
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
209
|
+
0 && (module.exports = {
|
|
210
|
+
SmartImage,
|
|
211
|
+
SmartImageProvider
|
|
212
|
+
});
|
|
213
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx","../src/useAICaption.ts"],"sourcesContent":["import React, { ImgHTMLAttributes } from \"react\";\r\nimport { SmartImageContext, useAICaptions } from \"./useAICaption\";\r\n\r\nexport const SmartImageProvider: React.FC<{\r\n value: { apiEndpoint?: string; disableAI?: boolean };\r\n children: React.ReactNode;\r\n}> = ({ value, children }) => {\r\n return <SmartImageContext.Provider value={value}>{children}</SmartImageContext.Provider>;\r\n};\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n src?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n onCaptionError?: (error: Error) => void;\r\n}\r\n\r\nconst SR_ONLY_STYLE: React.CSSProperties = {\r\n position: \"absolute\",\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: \"-1px\",\r\n overflow: \"hidden\",\r\n clip: \"rect(0, 0, 0, 0)\",\r\n whiteSpace: \"nowrap\",\r\n borderWidth: 0,\r\n};\r\n\r\nexport const SmartImage = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive = false,\r\n onCaptionError,\r\n ...props\r\n}: SmartImageProps) => {\r\n const { isGenerating, generatedAlt } = useAICaptions({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt,\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n announceLive,\r\n onCaptionError,\r\n });\r\n\r\n return (\r\n <>\r\n {announceLive && (\r\n <span style={SR_ONLY_STYLE} aria-live=\"polite\" aria-atomic=\"true\">\r\n {isGenerating\r\n ? \"Generating image description. Please wait...\"\r\n : generatedAlt\r\n ? `Image description generated: ${generatedAlt}`\r\n : \"\"}\r\n </span>\r\n )}\r\n <img src={src} alt={generatedAlt} aria-busy={isGenerating} {...props} />\r\n </>\r\n );\r\n};\r\n","import { StaticImport } from \"next/dist/shared/lib/get-img-props\";\r\nimport { StaticImageData } from \"next/image\";\r\nimport { createContext, ImgHTMLAttributes, useContext, useEffect, useRef, useState } from \"react\";\r\n\r\nexport interface UseAICaptionOptions {\r\n src?: string | StaticImport;\r\n alt?: string;\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n onCaptionError?: (error: Error) => void;\r\n}\r\n\r\ninterface SmartImageContextProps {\r\n apiEndpoint?: string;\r\n disableAI?: boolean;\r\n}\r\n\r\nexport const SmartImageContext = createContext<SmartImageContextProps | undefined>(undefined);\r\n\r\nconst MAX_SIZE = 500;\r\n\r\nclass LRUCaptionCache {\r\n private cache = new Map<string, string>();\r\n\r\n get(key: string): string | undefined {\r\n return this.cache.get(key);\r\n }\r\n set(key: string, value: string) {\r\n if (this.cache.size >= MAX_SIZE) {\r\n this.cache.delete(this.cache.keys().next().value!);\r\n }\r\n this.cache.set(key, value);\r\n }\r\n has(key: string) {\r\n return this.cache.has(key);\r\n }\r\n clear() {\r\n this.cache.clear();\r\n }\r\n}\r\n\r\nconst captionCache = new LRUCaptionCache();\r\nconst pendingRequestCache = new Map<string, Promise<string>>();\r\n\r\nexport interface SmartImageProps extends ImgHTMLAttributes<HTMLImageElement> {\r\n apiEndpoint?: string;\r\n fallbackAlt?: string;\r\n onCaptionGenerated?: (caption: string) => void;\r\n disableAI?: boolean;\r\n announceLive?: boolean;\r\n}\r\n\r\nfunction isStaticImageData(src: string | StaticImport): src is StaticImageData {\r\n return typeof src === \"object\" && src !== null && \"src\" in src;\r\n}\r\n\r\nfunction resolveImageUrl(src: string | StaticImport): string {\r\n if (typeof src === \"string\") return src;\r\n if (isStaticImageData(src)) return src.src;\r\n return src.default.src;\r\n}\r\n\r\nconst log = process.env.NODE_ENV === \"development\" ? console.log : () => {};\r\n\r\nexport const useAICaptions = ({\r\n src,\r\n alt,\r\n apiEndpoint: propsEndpoint,\r\n fallbackAlt = \"Image loading or caption unavailable\",\r\n onCaptionGenerated,\r\n disableAI: propsDisableAI,\r\n onCaptionError,\r\n}: UseAICaptionOptions) => {\r\n const context = useContext(SmartImageContext);\r\n\r\n const apiEndpoint = propsEndpoint || context?.apiEndpoint;\r\n const disableAI = propsDisableAI ?? context?.disableAI ?? false;\r\n\r\n const [generatedAlt, setGeneratedAlt] = useState(\"\");\r\n const [isGenerating, setIsGenerating] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const onCaptionGeneratedRef = useRef(onCaptionGenerated);\r\n const onCaptionErrorRef = useRef(onCaptionError);\r\n\r\n useEffect(() => {\r\n onCaptionGeneratedRef.current = onCaptionGenerated;\r\n onCaptionErrorRef.current = onCaptionError;\r\n });\r\n\r\n useEffect(() => {\r\n setError(null);\r\n\r\n if (alt) {\r\n setGeneratedAlt(alt);\r\n return;\r\n }\r\n\r\n if (!src) return;\r\n\r\n if (disableAI) {\r\n setGeneratedAlt(\"[Testing mode: AI caption generation disabled]\");\r\n return;\r\n }\r\n\r\n if (!apiEndpoint) {\r\n console.warn(\r\n \"[SmartImage] Missing 'apiEndpoint' prop. Please provide a backend API URL via props or SmartImageProvider to enable AI caption generation.\",\r\n );\r\n setGeneratedAlt(fallbackAlt);\r\n return;\r\n }\r\n\r\n const imageUrl = resolveImageUrl(src);\r\n\r\n if (captionCache.has(imageUrl)) {\r\n log(\"[SmartImage] Cache hit: Reusing existing caption.\");\r\n const cachedCaption = captionCache.get(imageUrl)!;\r\n setGeneratedAlt(cachedCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(cachedCaption);\r\n return;\r\n }\r\n\r\n let cancelled = false;\r\n\r\n const generateCaption = async () => {\r\n setIsGenerating(true);\r\n try {\r\n if (pendingRequestCache.has(imageUrl)) {\r\n log(\"[SmartImage] Pending request detected. Waiting for the existing API call to complete.\");\r\n const caption = await pendingRequestCache.get(imageUrl)!;\r\n if (cancelled) return;\r\n setGeneratedAlt(caption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(caption);\r\n return;\r\n }\r\n\r\n const fetchPromise = (async () => {\r\n const imageResponse = await fetch(imageUrl);\r\n const imageBlob = await imageResponse.blob();\r\n const imageFile = new File([imageBlob], \"image.jpg\", {\r\n type: imageBlob.type || \"image/jpeg\",\r\n });\r\n const formData = new FormData();\r\n formData.append(\"file\", imageFile);\r\n\r\n const response = await fetch(apiEndpoint, {\r\n method: \"POST\",\r\n body: formData,\r\n });\r\n\r\n if (!response.ok) throw new Error(\"AI API request failed\");\r\n const data = await response.json();\r\n if (data.caption) return data.caption;\r\n throw new Error(\"No caption returned from the API.\");\r\n })();\r\n\r\n pendingRequestCache.set(imageUrl, fetchPromise);\r\n\r\n const newCaption = await fetchPromise;\r\n pendingRequestCache.delete(imageUrl);\r\n captionCache.set(imageUrl, newCaption);\r\n\r\n if (cancelled) return;\r\n setGeneratedAlt(newCaption);\r\n if (onCaptionGeneratedRef.current) onCaptionGeneratedRef.current(newCaption);\r\n } catch (err) {\r\n const normalizedError = err instanceof Error ? err : new Error(\"Unknown error\");\r\n pendingRequestCache.delete(imageUrl);\r\n if (cancelled) return;\r\n setError(normalizedError);\r\n setGeneratedAlt(fallbackAlt);\r\n if (onCaptionErrorRef.current) onCaptionErrorRef.current(normalizedError);\r\n } finally {\r\n if (!cancelled) setIsGenerating(false);\r\n }\r\n };\r\n\r\n generateCaption();\r\n\r\n return () => {\r\n cancelled = true;\r\n };\r\n }, [src, alt, apiEndpoint, fallbackAlt, disableAI]);\r\n\r\n return { generatedAlt, isGenerating, error };\r\n};\r\n"],"mappings":"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","names":[]}
|