FancyAlt is an accessibility-focused web application and API that uses OpenAI to turn images into clear, useful text.
Users can upload an image or provide a public image URL, choose one result type, and receive that requested output: concise alt text, a detailed description, a social media description, a short creative story, or an image safety check.
π Live Web App: https://fancyalt.com
π Interactive API Documentation: https://fancyalt.com/api-docs
- Analyze uploaded JPEG, PNG, and WEBP images
- Analyze publicly accessible HTTP and HTTPS image URLs
- Generate concise, accessibility-focused alt text
- Generate detailed visual descriptions
- Create natural descriptions for social media
- Create short fictional stories inspired by an image
- Extract meaningful readable text when requested
- Moderate every image before returning generated content
- Return consistent, structured JSON results
- Provide a responsive, keyboard-accessible interface
- Support light and dark themes
- Provide interactive Swagger API documentation
FancyAlt requests only the result type selected by the user.
| Selected Mode | Generated Result |
|---|---|
concise |
Concise alt text |
detailed |
Detailed visual description |
social |
Social media description |
story |
Short fictional story |
moderateOnly |
Moderation result only |
Every image is still checked by the moderation service first.
When visible-text extraction is enabled, the text-detection fields are included in the same mode-specific image request. FancyAlt does not make a second image request for visible text.
The OpenAI client uses maxRetries: 0, so a failed mode-specific image request is not silently repeated by the SDK.
- Backend: Node.js 20+ and Express.js 5
- AI Image Processing: OpenAI Responses API
- Image and Story Model:
gpt-5.4-mini - Content Moderation:
omni-moderation-latest - Structured Output: Zod
- Image Uploads: Multer memory storage
- Request Validation: express-validator
- Frontend: HTML5, custom CSS, and vanilla JavaScript
- UI Assets: Bootstrap, Font Awesome, and JSONEditor
- Documentation: OpenAPI 3.0, Swagger UI, and YAMLJS
- Security: Helmet, CORS, rate limiting, and custom input sanitization
- The user uploads a JPEG, PNG, or WEBP image.
- Multer validates the file type and 5 MB size limit.
- The image is held temporarily in memory.
- OpenAI Moderation checks the image.
- If the mode is
moderateOnly, FancyAlt returns the moderation result immediately. - Otherwise, FancyAlt makes one mode-specific image request and returns only the selected output.
- The user submits a public HTTP or HTTPS image URL.
- FancyAlt validates the URL and rejects localhost or loopback addresses.
- OpenAI Moderation checks the image.
- If the mode is
moderateOnly, FancyAlt returns the moderation result immediately. - Otherwise, FancyAlt makes one mode-specific image request and returns only the selected output.
A successful non-moderation response includes:
- Mode: The selected result type
- Moderation: Safety results for the image
- Output Text: Only the requested description or story
- Model: The OpenAI model used for the generated output
- Visible Text: Optionally returned only when visible-text extraction is enabled
| Mode | Purpose |
|---|---|
concise |
Creates focused alt text for websites, documents, and applications |
detailed |
Creates a fuller visual description |
social |
Creates a natural description for social media |
story |
Creates a short fictional story directly from the image |
moderateOnly |
Runs only the image safety check |
git clone https://github.com/jillmpla/fancyalt.git
cd fancyaltFancyAlt requires Node.js 20 or newer.
node --versionnpm installCopy .env.example to a new file named .env.
The environment file uses these values:
OPENAI_API_KEY=your-openai-api-key
OPENAI_IMAGE_MODEL=gpt-5.4-mini
OPENAI_STORY_MODEL=gpt-5.4-mini
OPENAI_MODERATION_MODEL=omni-moderation-latestReplace the placeholder API key in .env with your own OpenAI API key.
npm startFor development:
npm run dev- Frontend: http://localhost:5000
- API Documentation: http://localhost:5000/api-docs
- API Status: http://localhost:5000/api/status
- Direct Status Check: http://localhost:5000/status
Production: https://fancyalt.com/api
Local: http://localhost:5000/api
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/generate-caption |
Analyze an uploaded image |
POST |
/api/analyze-url |
Analyze a public image URL |
GET |
/api/status |
Check whether the API is running |
POST /api/generate-captionmultipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
image |
File | Yes | JPEG, PNG, or WEBP image up to 5 MB |
mode |
String | No | Result mode; defaults to concise |
maxLength |
Integer | No | Preferred maximum length from 40 to 1,000 characters |
includeVisibleText |
Boolean | No | Whether readable text should be extracted; defaults to true when omitted |
curl -X POST http://localhost:5000/api/generate-caption \
-F "image=@example.jpg" \
-F "mode=concise" \
-F "maxLength=160" \
-F "includeVisibleText=true"POST /api/analyze-urlapplication/json
{
"imageUrl": "https://example.com/images/kayak-dog.jpg",
"mode": "detailed",
"maxLength": 500,
"includeVisibleText": true
}curl -X POST http://localhost:5000/api/analyze-url \
-H "Content-Type: application/json" \
-d '{
"imageUrl": "https://example.com/images/kayak-dog.jpg",
"mode": "detailed",
"maxLength": 500,
"includeVisibleText": true
}'The URL must be publicly accessible without authentication and must use HTTP or HTTPS.
{
"mode": "concise",
"flagged": false,
"moderation": {
"flagged": false,
"flaggedCategories": [],
"categories": {
"sexual": false,
"violence": false,
"self-harm": false
},
"categoryScores": {
"sexual": 0.00012,
"violence": 0.00048,
"self-harm": 0.00001
},
"appliedInputTypes": {
"sexual": ["image"],
"violence": ["image"]
},
"model": "omni-moderation-latest"
},
"output": {
"text": "Golden retriever sitting beside a blue kayak at the edge of a calm lake.",
"model": "gpt-5.4-mini",
"containsText": false,
"visibleText": []
}
}The containsText and visibleText properties are omitted when includeVisibleText is false.
{
"mode": "story",
"flagged": false,
"moderation": {
"flagged": false,
"flaggedCategories": [],
"categories": {},
"categoryScores": {},
"appliedInputTypes": {},
"model": "omni-moderation-latest"
},
"output": {
"text": "The retriever waited beside the kayak as the lake turned gold in the evening light. One quiet paddle remained before home.",
"model": "gpt-5.4-mini"
}
}Story mode creates the story directly from the image. It doesn't first request a separate alt-text analysis.
{
"mode": "moderateOnly",
"flagged": false,
"moderation": {
"flagged": false,
"flaggedCategories": [],
"categories": {},
"categoryScores": {},
"appliedInputTypes": {},
"model": "omni-moderation-latest"
}
}Moderation-only responses don't include an output property.
GET /api/statusThe status endpoint returns service, version, environment, uptime, provider, and timestamp information.
FancyAlt uses centralized error handling, consistent status codes, and request IDs.
{
"error": "Validation failed.",
"requestId": "1c8ca594-8758-4c97-bb86-49cff66704f8",
"details": [
{
"field": "mode",
"location": "body",
"message": "Mode must be one of the supported values.",
"value": "invalidMode"
}
]
}| Code | Meaning |
|---|---|
200 |
Request completed successfully |
400 |
Request data is missing or invalid |
403 |
Request origin or resource is not allowed |
404 |
Route does not exist |
413 |
Uploaded image exceeds the 5 MB limit |
415 |
Uploaded file type is not supported |
422 |
Image could not be retrieved or processed |
429 |
FancyAlt's API rate limit was exceeded |
500 |
Unexpected server error |
502 |
An upstream service returned an invalid response |
503 |
OpenAI authentication, rate limiting, or service availability prevented processing |
Temporary 503 responses may include a Retry-After header.
Every request receives an X-Request-ID response header. Error responses also include the request ID in the JSON body.
- The OpenAI API key is stored only on the server.
- Uploaded images are held in memory and are not intentionally saved to disk.
- Only one image is accepted per request.
- Uploads are limited to 5 MB.
- Only JPEG, PNG, and WEBP uploads are accepted.
- Public image URLs are validated before processing.
- Localhost and loopback image URLs are rejected.
- Custom middleware removes dangerous request keys.
- Helmet applies standard HTTP security headers.
- CORS limits browser requests to approved origins.
- Each IP address is limited to 25 API requests every 15 minutes.
- Every request receives a unique request ID.
The FancyAlt interface includes:
- Semantic HTML landmarks
- Clear form labels and descriptions
- A skip-navigation link
- Keyboard-accessible source tabs
- Keyboard-accessible upload controls
- Visible keyboard focus indicators
- Screen-reader status announcements
- Light and dark themes
- Reduced-motion support
- Human-readable result cards
- Copy-to-clipboard controls
- Responsive layouts
fancyalt/
βββ .env.example
βββ .gitignore
βββ index.js
βββ package.json
βββ package-lock.json
βββ README.md
βββ swagger.yaml
βββ LICENSE.txt
β
βββ middlewares/
β βββ asyncHandler.js
β βββ sanitizeKeys.js
β βββ validate.js
β
βββ routes/
β βββ caption.js
β βββ status.js
β
βββ services/
β βββ imageAnalyzer.js
β βββ imageInput.js
β βββ imageModerator.js
β βββ openaiClient.js
β βββ storyGenerator.js
β
βββ utils/
β βββ errors.js
β
βββ public/
βββ index.html
βββ index.css
βββ footer.css
βββ footer.js
βββ swagger-custom.css
β
βββ js/
βββ app.js
Static images, Bootstrap files, Font Awesome files, and third-party vendor assets are omitted for readability.
FancyAlt includes interactive Swagger documentation generated from swagger.yaml.
- Local: http://localhost:5000/api-docs
- Production: https://fancyalt.com/api-docs
- Raw OpenAPI File: http://localhost:5000/swagger.yaml
See LICENSE.txt for the full license terms.
For questions or support, email fancyaltdotcom@gmail.com.
If you find FancyAlt useful, consider giving the repository a star.