Add Discord notifications for image generation and snapshot creation
This commit is contained in:
615
README.md
615
README.md
@@ -7,12 +7,14 @@ Generate Twitter-style quote images programmatically. Perfect for creating socia
|
||||
## Features
|
||||
|
||||
- Twitter-authentic styling
|
||||
- Support for images and text-only tweets
|
||||
- Support for images, videos, GIFs, and text-only tweets
|
||||
- Verified badge (blue checkmark)
|
||||
- Engagement metrics (likes, retweets, replies, views)
|
||||
- High-resolution output (3240x3240)
|
||||
- Built-in caching (24-hour TTL)
|
||||
- Base64 image support
|
||||
- Video/GIF rendering with Playwright (v2 API only)
|
||||
- Background video processing with status tracking
|
||||
- Docker-ready
|
||||
- v2 API with stateful sessions for incremental updates
|
||||
- Persistent snapshot links (48-hour TTL, immutable, shareable)
|
||||
@@ -26,14 +28,14 @@ Generate a quote image using JSON request body.
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST https://quotes.imbenji.net/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"displayName": "Geoff Marshall",
|
||||
"username": "@geofftech",
|
||||
"avatarUrl": "https://example.com/avatar.jpg",
|
||||
"text": "Does anyone else find it immensely satisfying when you turn a pocket inside out and get rid of the crumbs and fluff stuck in the bottom.",
|
||||
"timestamp": 1499766270
|
||||
}' --output quote.png
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"displayName": "Geoff Marshall",
|
||||
"username": "@geofftech",
|
||||
"avatarUrl": "https://example.com/avatar.jpg",
|
||||
"text": "Does anyone else find it immensely satisfying when you turn a pocket inside out and get rid of the crumbs and fluff stuck in the bottom.",
|
||||
"timestamp": 1499766270
|
||||
}' --output quote.png
|
||||
```
|
||||
|
||||
### GET /generate
|
||||
@@ -62,7 +64,7 @@ curl "https://quotes.imbenji.net/generate?displayName=John%20Doe&username=@johnd
|
||||
|
||||
- **Content-Type:** `image/png`
|
||||
- **Headers:**
|
||||
- `X-Cache`: `HIT` (served from cache) or `MISS` (newly generated)
|
||||
- `X-Cache`: `HIT` (served from cache) or `MISS` (newly generated)
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -70,19 +72,19 @@ curl "https://quotes.imbenji.net/generate?displayName=John%20Doe&username=@johnd
|
||||
|
||||
```javascript
|
||||
fetch('https://quotes.imbenji.net/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'John Doe',
|
||||
username: '@johndoe',
|
||||
text: 'Just deployed my new API!',
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'John Doe',
|
||||
username: '@johndoe',
|
||||
text: 'Just deployed my new API!',
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
})
|
||||
})
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
document.getElementById('img').src = url;
|
||||
const url = URL.createObjectURL(blob);
|
||||
document.getElementById('img').src = url;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -90,16 +92,16 @@ fetch('https://quotes.imbenji.net/generate', {
|
||||
|
||||
```javascript
|
||||
const response = await fetch('https://quotes.imbenji.net/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Jane Smith',
|
||||
username: '@janesmith',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
text: 'Check out this amazing view! 🌄',
|
||||
imageUrl: 'https://example.com/photo.jpg',
|
||||
timestamp: 1735574400
|
||||
})
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Jane Smith',
|
||||
username: '@janesmith',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
text: 'Check out this amazing view! 🌄',
|
||||
imageUrl: 'https://example.com/photo.jpg',
|
||||
timestamp: 1735574400
|
||||
})
|
||||
});
|
||||
|
||||
const blob = await response.blob();
|
||||
@@ -110,16 +112,16 @@ const blob = await response.blob();
|
||||
|
||||
```javascript
|
||||
fetch('https://quotes.imbenji.net/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Alice',
|
||||
username: '@alice',
|
||||
avatarUrl: 'data:image/png;base64,iVBORw0KGgo...',
|
||||
text: 'Using base64 images works too!',
|
||||
imageUrl: 'data:image/jpeg;base64,/9j/4AAQSkZJ...',
|
||||
timestamp: 1735574400
|
||||
})
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Alice',
|
||||
username: '@alice',
|
||||
avatarUrl: 'data:image/png;base64,iVBORw0KGgo...',
|
||||
text: 'Using base64 images works too!',
|
||||
imageUrl: 'data:image/jpeg;base64,/9j/4AAQSkZJ...',
|
||||
timestamp: 1735574400
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
@@ -130,40 +132,40 @@ import requests
|
||||
import time
|
||||
|
||||
response = requests.post('https://quotes.imbenji.net/generate', json={
|
||||
'displayName': 'Python User',
|
||||
'username': '@pythonista',
|
||||
'text': 'Making API calls with Python!',
|
||||
'timestamp': int(time.time())
|
||||
'displayName': 'Python User',
|
||||
'username': '@pythonista',
|
||||
'text': 'Making API calls with Python!',
|
||||
'timestamp': int(time.time())
|
||||
})
|
||||
|
||||
with open('quote.png', 'wb') as f:
|
||||
f.write(response.content)
|
||||
f.write(response.content)
|
||||
```
|
||||
|
||||
### cURL with GET
|
||||
|
||||
```bash
|
||||
curl -G "https://quotes.imbenji.net/generate" \
|
||||
--data-urlencode "displayName=Test User" \
|
||||
--data-urlencode "username=@testuser" \
|
||||
--data-urlencode "text=This is a test tweet" \
|
||||
--data-urlencode "timestamp=1735574400" \
|
||||
--output quote.png
|
||||
--data-urlencode "displayName=Test User" \
|
||||
--data-urlencode "username=@testuser" \
|
||||
--data-urlencode "text=This is a test tweet" \
|
||||
--data-urlencode "timestamp=1735574400" \
|
||||
--output quote.png
|
||||
```
|
||||
|
||||
### Verified badge
|
||||
|
||||
```javascript
|
||||
fetch('https://quotes.imbenji.net/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Elon Musk',
|
||||
username: '@elonmusk',
|
||||
text: 'Just bought Twitter!',
|
||||
verified: true,
|
||||
timestamp: 1735574400
|
||||
})
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Elon Musk',
|
||||
username: '@elonmusk',
|
||||
text: 'Just bought Twitter!',
|
||||
verified: true,
|
||||
timestamp: 1735574400
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
@@ -171,21 +173,21 @@ fetch('https://quotes.imbenji.net/generate', {
|
||||
|
||||
```javascript
|
||||
fetch('https://quotes.imbenji.net/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Popular User',
|
||||
username: '@viral',
|
||||
text: 'This tweet went viral!',
|
||||
verified: true,
|
||||
engagement: {
|
||||
replies: 1234,
|
||||
retweets: 5678,
|
||||
likes: 98765,
|
||||
views: 1500000
|
||||
},
|
||||
timestamp: 1735574400
|
||||
})
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Popular User',
|
||||
username: '@viral',
|
||||
text: 'This tweet went viral!',
|
||||
verified: true,
|
||||
engagement: {
|
||||
replies: 1234,
|
||||
retweets: 5678,
|
||||
likes: 98765,
|
||||
views: 1500000
|
||||
},
|
||||
timestamp: 1735574400
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
@@ -205,6 +207,22 @@ The v2 API lets you build quote images incrementally. Instead of sending everyth
|
||||
|
||||
Sessions expire after 24 hours of inactivity.
|
||||
|
||||
### Video/GIF Support (v2 Only)
|
||||
|
||||
The v2 API supports video and animated GIF rendering. When you provide a video or GIF in the `imageUrl` parameter, the API automatically:
|
||||
- Detects the video format (MP4, WebM, MOV, GIF)
|
||||
- Renders the video in the background using Playwright
|
||||
- Returns an animated MP4 file showing the video playing within the tweet frame
|
||||
- Tracks render status (`pending`, `rendering`, `completed`, `failed`)
|
||||
|
||||
**Supported formats:**
|
||||
- MP4 (H.264, H.265)
|
||||
- WebM (VP8, VP9)
|
||||
- MOV (QuickTime)
|
||||
- GIF (animated, converted to MP4)
|
||||
|
||||
**Note:** Video rendering happens asynchronously in the background. The `GET /v2/quote/:id/image` endpoint will wait (up to 15 minutes) for the render to complete before returning the video file.
|
||||
|
||||
### POST /v2/quote
|
||||
|
||||
Create a new session.
|
||||
@@ -212,31 +230,41 @@ Create a new session.
|
||||
**Request:**
|
||||
```bash
|
||||
curl -X POST https://quotes.imbenji.net/v2/quote \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"displayName": "John Doe",
|
||||
"username": "@johndoe",
|
||||
"text": "Hello world"
|
||||
}'
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"displayName": "John Doe",
|
||||
"username": "@johndoe",
|
||||
"text": "Hello world"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response (201):**
|
||||
```json
|
||||
{
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"displayName": "John Doe",
|
||||
"username": "@johndoe",
|
||||
"text": "Hello world",
|
||||
"avatarUrl": null,
|
||||
"imageUrl": null,
|
||||
"timestamp": null,
|
||||
"verified": false,
|
||||
"engagement": null,
|
||||
"createdAt": 1735600000,
|
||||
"updatedAt": 1735600000
|
||||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"displayName": "John Doe",
|
||||
"username": "@johndoe",
|
||||
"text": "Hello world",
|
||||
"avatarUrl": null,
|
||||
"imageUrl": null,
|
||||
"timestamp": null,
|
||||
"verified": false,
|
||||
"engagement": null,
|
||||
"renderStatus": "completed",
|
||||
"renderProgress": 100,
|
||||
"renderPid": null,
|
||||
"renderError": null,
|
||||
"createdAt": 1735600000,
|
||||
"updatedAt": 1735600000
|
||||
}
|
||||
```
|
||||
|
||||
**Render Status Fields:**
|
||||
- `renderStatus`: Current render state (`pending`, `rendering`, `completed`, `failed`)
|
||||
- `renderProgress`: Render progress percentage (0-100)
|
||||
- `renderPid`: Process ID of background render (only present during `rendering`)
|
||||
- `renderError`: Error message if `renderStatus` is `failed`
|
||||
|
||||
### GET /v2/quote/:id
|
||||
|
||||
Get current session state.
|
||||
@@ -251,11 +279,11 @@ Update specific fields. Only send the fields you want to change.
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"text": "Updated text!",
|
||||
"avatarUrl": "data:image/png;base64,..."
|
||||
}'
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"text": "Updated text!",
|
||||
"avatarUrl": "data:image/png;base64,..."
|
||||
}'
|
||||
```
|
||||
|
||||
**Engagement updates:**
|
||||
@@ -263,31 +291,31 @@ curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234
|
||||
```bash
|
||||
# Set all engagement metrics
|
||||
curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"engagement": {
|
||||
"replies": 100,
|
||||
"retweets": 250,
|
||||
"likes": 5000,
|
||||
"views": 50000
|
||||
}
|
||||
}'
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"engagement": {
|
||||
"replies": 100,
|
||||
"retweets": 250,
|
||||
"likes": 5000,
|
||||
"views": 50000
|
||||
}
|
||||
}'
|
||||
|
||||
# Partial update (update only likes, keep other fields)
|
||||
curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"engagement": {
|
||||
"likes": 10000
|
||||
}
|
||||
}'
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"engagement": {
|
||||
"likes": 10000
|
||||
}
|
||||
}'
|
||||
|
||||
# Clear engagement entirely (removes engagement bar)
|
||||
curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"engagement": null
|
||||
}'
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"engagement": null
|
||||
}'
|
||||
```
|
||||
|
||||
**Verified badge:**
|
||||
@@ -295,25 +323,135 @@ curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234
|
||||
```bash
|
||||
# Add verified badge
|
||||
curl -X PATCH https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"verified": true
|
||||
}'
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"verified": true
|
||||
}'
|
||||
```
|
||||
|
||||
### GET /v2/quote/:id/render-info
|
||||
|
||||
Get real-time render progress via Server-Sent Events (SSE).
|
||||
|
||||
```bash
|
||||
curl -N https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890/render-info
|
||||
```
|
||||
|
||||
**Response (text/event-stream):**
|
||||
```
|
||||
data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":0,"stage":"rendering","error":null}
|
||||
|
||||
data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":25,"stage":"rendering","error":null}
|
||||
|
||||
data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":50,"stage":"rendering","error":null}
|
||||
|
||||
data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":99,"stage":"rendering","error":null}
|
||||
|
||||
data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":0,"stage":"encoding","error":null}
|
||||
|
||||
data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":50,"stage":"encoding","error":null}
|
||||
|
||||
data: {"status":"rendering","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":99,"stage":"encoding","error":null}
|
||||
|
||||
data: {"status":"completed","sessionId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","progress":100,"stage":"completed","error":null}
|
||||
```
|
||||
|
||||
**JavaScript Example:**
|
||||
```javascript
|
||||
const eventSource = new EventSource(
|
||||
`https://quotes.imbenji.net/v2/quote/${sessionId}/render-info`
|
||||
);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(`Render: ${data.stage} (${data.progress}%)`);
|
||||
|
||||
// Update progress bar and stage label in UI
|
||||
document.getElementById('progress-bar').style.width = `${data.progress}%`;
|
||||
document.getElementById('progress-text').textContent = `${data.progress}%`;
|
||||
|
||||
// Show current stage
|
||||
let stageText = '';
|
||||
if (data.stage === 'rendering') {
|
||||
stageText = 'Rendering video...';
|
||||
} else if (data.stage === 'encoding') {
|
||||
stageText = 'Encoding to H.264...';
|
||||
} else if (data.stage === 'completed') {
|
||||
stageText = 'Complete!';
|
||||
}
|
||||
document.getElementById('stage-label').textContent = stageText;
|
||||
|
||||
if (data.status === 'completed') {
|
||||
console.log('Render complete! Fetching video...');
|
||||
eventSource.close();
|
||||
// Now fetch the video
|
||||
fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}/image`)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
// Handle the video blob
|
||||
});
|
||||
} else if (data.status === 'failed') {
|
||||
console.error('Render failed:', data.error);
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('SSE error:', error);
|
||||
eventSource.close();
|
||||
};
|
||||
```
|
||||
|
||||
**Status values:**
|
||||
- `pending` - Render queued but not started (progress: 0%)
|
||||
- `rendering` - In progress (progress: 0-99%)
|
||||
- `completed` - Successfully rendered (progress: 100%)
|
||||
- `failed` - Render failed (check `error` field)
|
||||
- `deleted` - Session was deleted
|
||||
|
||||
**Response fields:**
|
||||
- `status` - Current render status (`pending`, `rendering`, `completed`, `failed`)
|
||||
- `sessionId` - Session identifier
|
||||
- `progress` - Current stage completion percentage (0-100)
|
||||
- `stage` - Current render stage (`idle`, `rendering`, `encoding`, `completed`, `failed`)
|
||||
- `error` - Error message (only present if status is `failed`)
|
||||
|
||||
**Render Stages:**
|
||||
1. **rendering** - Recording video with Playwright (progress 0-99%)
|
||||
2. **encoding** - Converting to H.264 MP4 with FFmpeg (progress 0-99%)
|
||||
3. **completed** - Render finished successfully (progress 100%)
|
||||
|
||||
**Notes:**
|
||||
- The stream automatically closes when render is `completed` or `failed`
|
||||
- Progress updates are sent every 200ms while render is in progress
|
||||
- Progress resets to 0% when transitioning between stages (rendering → encoding)
|
||||
- Each stage reports its own progress percentage independently
|
||||
- Use the `stage` field to show different messages in your UI
|
||||
|
||||
### GET /v2/quote/:id/image
|
||||
|
||||
Render the current session state as a PNG image.
|
||||
Render the current session state as an image or video.
|
||||
|
||||
```bash
|
||||
# For images
|
||||
curl https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890/image --output quote.png
|
||||
|
||||
# For videos
|
||||
curl https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef1234567890/image --output quote.mp4
|
||||
```
|
||||
|
||||
**Response headers:**
|
||||
- `Content-Type: image/png`
|
||||
- `Content-Type: image/png` (for images) or `video/mp4` (for videos)
|
||||
- `X-Session-Id: <session-id>`
|
||||
- `X-Cache: HIT` or `MISS`
|
||||
|
||||
**Notes:**
|
||||
- For video renders, this endpoint will wait up to 15 minutes for the background render to complete
|
||||
- If the render is still in progress, the response will be delayed (polling internally)
|
||||
- If the render fails, a 500 error will be returned with details
|
||||
- If the render times out, a 504 error will be returned
|
||||
- For better UX, use `/render-info` to track progress and only call this when complete
|
||||
|
||||
### DELETE /v2/quote/:id
|
||||
|
||||
Delete a session.
|
||||
@@ -338,11 +476,11 @@ curl -X POST https://quotes.imbenji.net/v2/quote/a1b2c3d4-e5f6-7890-abcd-ef12345
|
||||
**Response (201):**
|
||||
```json
|
||||
{
|
||||
"token": "xY9pQmN3kL8vFw2jRtZ7",
|
||||
"url": "https://quotes.imbenji.net/v2/snapshot/xY9pQmN3kL8vFw2jRtZ7",
|
||||
"sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"createdAt": 1735600000,
|
||||
"expiresAt": 1735772800
|
||||
"token": "xY9pQmN3kL8vFw2jRtZ7",
|
||||
"url": "https://quotes.imbenji.net/v2/snapshot/xY9pQmN3kL8vFw2jRtZ7",
|
||||
"sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"createdAt": 1735600000,
|
||||
"expiresAt": 1735772800
|
||||
}
|
||||
```
|
||||
|
||||
@@ -370,33 +508,33 @@ curl https://quotes.imbenji.net/v2/snapshot/xY9pQmN3kL8vFw2jRtZ7 --output snapsh
|
||||
```javascript
|
||||
// 1. Create session with initial data
|
||||
const res = await fetch('https://quotes.imbenji.net/v2/quote', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Jane Doe',
|
||||
username: '@janedoe',
|
||||
text: 'Working on something cool'
|
||||
})
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Jane Doe',
|
||||
username: '@janedoe',
|
||||
text: 'Working on something cool'
|
||||
})
|
||||
});
|
||||
const session = await res.json();
|
||||
const sessionId = session.id;
|
||||
|
||||
// 2. Later, add an avatar (only sends the avatar, not everything again)
|
||||
await fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
avatarUrl: 'data:image/png;base64,iVBORw0KGgo...'
|
||||
})
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
avatarUrl: 'data:image/png;base64,iVBORw0KGgo...'
|
||||
})
|
||||
});
|
||||
|
||||
// 3. Update the text
|
||||
await fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text: 'Finished building something cool!'
|
||||
})
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text: 'Finished building something cool!'
|
||||
})
|
||||
});
|
||||
|
||||
// 4. Generate the final image
|
||||
@@ -405,13 +543,172 @@ const blob = await imgRes.blob();
|
||||
|
||||
// 5. (Optional) Create a shareable snapshot link
|
||||
const snapshotRes = await fetch(`https://quotes.imbenji.net/v2/quote/${sessionId}/snapshot-link`, {
|
||||
method: 'POST'
|
||||
method: 'POST'
|
||||
});
|
||||
const snapshot = await snapshotRes.json();
|
||||
console.log('Shareable URL:', snapshot.url);
|
||||
// Anyone can access: https://quotes.imbenji.net/v2/snapshot/<token>
|
||||
```
|
||||
|
||||
### v2 Video Example
|
||||
|
||||
```javascript
|
||||
// 1. Create session with video
|
||||
const res = await fetch('https://quotes.imbenji.net/v2/quote', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Video Creator',
|
||||
username: '@videocreator',
|
||||
text: 'Check out this awesome clip!',
|
||||
imageUrl: 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21...' // base64 video
|
||||
})
|
||||
});
|
||||
const session = await res.json();
|
||||
console.log('Render status:', session.renderStatus); // "rendering"
|
||||
|
||||
// 2. Track render progress with SSE (recommended)
|
||||
const eventSource = new EventSource(
|
||||
`https://quotes.imbenji.net/v2/quote/${session.id}/render-info`
|
||||
);
|
||||
|
||||
eventSource.onmessage = async (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(`Render: ${data.status} (${data.progress}%)`);
|
||||
|
||||
// Update UI with progress
|
||||
if (data.status === 'rendering') {
|
||||
// Show progress bar: 0% -> 99%
|
||||
updateProgressBar(data.progress);
|
||||
}
|
||||
|
||||
if (data.status === 'completed') {
|
||||
eventSource.close();
|
||||
updateProgressBar(100);
|
||||
|
||||
// 3. Get the rendered video
|
||||
const videoRes = await fetch(`https://quotes.imbenji.net/v2/quote/${session.id}/image`);
|
||||
const videoBlob = await videoRes.blob();
|
||||
console.log('Content-Type:', videoRes.headers.get('Content-Type')); // "video/mp4"
|
||||
|
||||
// Save the video
|
||||
const url = URL.createObjectURL(videoBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'quote-video.mp4';
|
||||
a.click();
|
||||
} else if (data.status === 'failed') {
|
||||
console.error('Render failed:', data.error);
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Alternative: Simple approach (blocks until complete)
|
||||
// const videoRes = await fetch(`https://quotes.imbenji.net/v2/quote/${session.id}/image`);
|
||||
// const videoBlob = await videoRes.blob();
|
||||
```
|
||||
|
||||
**GIF Example:**
|
||||
```javascript
|
||||
// Animated GIFs are automatically detected and rendered as MP4 videos
|
||||
const res = await fetch('https://quotes.imbenji.net/v2/quote', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'GIF Lover',
|
||||
username: '@giflover',
|
||||
text: 'This GIF is fire 🔥',
|
||||
imageUrl: 'data:image/gif;base64,R0lGODlhAQABAIAAAP...' // animated GIF
|
||||
})
|
||||
});
|
||||
// The output will be an MP4 video file
|
||||
```
|
||||
|
||||
**Full HTML Progress Bar Example:**
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 30px;
|
||||
background-color: #4CAF50;
|
||||
width: 0%;
|
||||
transition: width 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" id="progressBar">0%</div>
|
||||
</div>
|
||||
<video id="videoPlayer" controls style="display:none; margin-top:20px; width:100%;"></video>
|
||||
|
||||
<script>
|
||||
async function renderVideo() {
|
||||
// Create session with video
|
||||
const res = await fetch('https://quotes.imbenji.net/v2/quote', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: 'Video Creator',
|
||||
username: '@videocreator',
|
||||
text: 'Check out this clip!',
|
||||
imageUrl: 'data:video/mp4;base64,AAAAIGZ0eXBpc29t...'
|
||||
})
|
||||
});
|
||||
const session = await res.json();
|
||||
|
||||
// Track progress with SSE
|
||||
const eventSource = new EventSource(
|
||||
`https://quotes.imbenji.net/v2/quote/${session.id}/render-info`
|
||||
);
|
||||
|
||||
eventSource.onmessage = async (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
progressBar.style.width = data.progress + '%';
|
||||
progressBar.textContent = data.progress + '%';
|
||||
|
||||
if (data.status === 'completed') {
|
||||
eventSource.close();
|
||||
|
||||
// Fetch and display video
|
||||
const videoRes = await fetch(
|
||||
`https://quotes.imbenji.net/v2/quote/${session.id}/image`
|
||||
);
|
||||
const blob = await videoRes.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const video = document.getElementById('videoPlayer');
|
||||
video.src = url;
|
||||
video.style.display = 'block';
|
||||
} else if (data.status === 'failed') {
|
||||
eventSource.close();
|
||||
alert('Render failed: ' + data.error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
renderVideo();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### v2 Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
@@ -428,12 +725,12 @@ console.log('Shareable URL:', snapshot.url);
|
||||
**Engagement object:**
|
||||
```json
|
||||
{
|
||||
"engagement": {
|
||||
"replies": 89,
|
||||
"retweets": 567,
|
||||
"likes": 1234,
|
||||
"views": 50000
|
||||
}
|
||||
"engagement": {
|
||||
"replies": 89,
|
||||
"retweets": 567,
|
||||
"likes": 1234,
|
||||
"views": 50000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -495,9 +792,12 @@ The API will be available at `http://localhost:3000`
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
npm start
|
||||
```
|
||||
|
||||
**Note:** Video rendering requires Playwright and FFmpeg. The Docker setup handles this automatically.
|
||||
|
||||
## Health Check
|
||||
|
||||
```bash
|
||||
@@ -507,20 +807,23 @@ curl https://quotes.imbenji.net/health
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Resolution:** 3240x3240 (1:1 aspect ratio)
|
||||
- **Format:** PNG
|
||||
- **Resolution:** 3240x3240 (1:1 aspect ratio) for images, 1500x1500 for videos
|
||||
- **Format:** PNG (images), MP4 (videos)
|
||||
- **Max tweet width:** 450px (auto-scaled to fit)
|
||||
- **Cache TTL:** 24 hours from last access
|
||||
- **Session TTL:** 24 hours from last update (v2 API)
|
||||
- **Snapshot TTL:** 48 hours from last access (v2 API)
|
||||
- **Cleanup interval:** Every hour
|
||||
- **Database:** SQLite (for v2 sessions and snapshots)
|
||||
- **Video rendering:** Playwright Chromium with background worker processes
|
||||
- **Video timeout:** 15 minutes maximum render time
|
||||
- **Supported video formats:** MP4, WebM, MOV, GIF
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -528,6 +831,10 @@ Response:
|
||||
- Very long tweets may be cut off
|
||||
- External image URLs must be publicly accessible
|
||||
- Base64 images should be reasonably sized
|
||||
- Video rendering is only available in v2 API (not in `/generate` endpoint)
|
||||
- Video renders can take several seconds depending on video duration
|
||||
- Very long videos (>15 minutes) may timeout
|
||||
- Video audio is muted in the output
|
||||
|
||||
## Support
|
||||
|
||||
@@ -535,4 +842,4 @@ For issues or questions, please contact the maintainer.
|
||||
|
||||
---
|
||||
|
||||
**Built with:** Node.js, Express, Puppeteer, SQLite
|
||||
**Built with:** Node.js, Express, Puppeteer, Playwright, SQLite, FFmpeg
|
||||
|
||||
Reference in New Issue
Block a user