Spaces:
Sleeping
Sleeping
initialize project with Vite, React, and essential configurations
Browse files- Dockerfile +42 -0
- README copy.md +75 -0
- index.html +14 -0
- package.json +26 -0
- src/App.jsx +395 -0
- src/main.jsx +75 -0
- vite.config.js +10 -0
Dockerfile
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM node:18-slim
|
2 |
+
|
3 |
+
# Set up a new user named "user" with user ID 1000
|
4 |
+
RUN useradd -m -u 1000 user
|
5 |
+
|
6 |
+
# Install basic dependencies
|
7 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
8 |
+
curl \
|
9 |
+
&& rm -rf /var/lib/apt/lists/*
|
10 |
+
|
11 |
+
# Switch to the "user" user
|
12 |
+
USER user
|
13 |
+
|
14 |
+
# Set home to the user's home directory
|
15 |
+
ENV HOME=/home/user \
|
16 |
+
PATH=/home/user/.local/bin:$PATH
|
17 |
+
|
18 |
+
# Set the working directory to the user's home directory
|
19 |
+
WORKDIR $HOME/app
|
20 |
+
|
21 |
+
# Copy package.json and package-lock.json first for better caching
|
22 |
+
COPY --chown=user package*.json ./
|
23 |
+
|
24 |
+
# Install dependencies
|
25 |
+
RUN npm install
|
26 |
+
|
27 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
28 |
+
COPY --chown=user . .
|
29 |
+
|
30 |
+
# Build the application
|
31 |
+
RUN npm run build
|
32 |
+
|
33 |
+
# Set environment variables
|
34 |
+
ENV NODE_ENV=production \
|
35 |
+
PORT=7860 \
|
36 |
+
HOST=0.0.0.0
|
37 |
+
|
38 |
+
# Expose the port
|
39 |
+
EXPOSE 7860
|
40 |
+
|
41 |
+
# Command to run the application
|
42 |
+
CMD ["npm", "run", "preview"]
|
README copy.md
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Glif Lora Autocaption
|
2 |
+
|
3 |
+
A sleek, minimalist web application to automatically caption your images using the Glif API.
|
4 |
+
|
5 |
+
## Features
|
6 |
+
|
7 |
+
- **Private API Key Management**: Enter your Glif API key securely
|
8 |
+
- **Batch Image Processing**: Upload and process multiple images at once
|
9 |
+
- **Real-time Progress Tracking**: See the status of each image as it's processed
|
10 |
+
- **Modern Dark UI**: Clean, minimalist interface inspired by Vercel and Apple designs
|
11 |
+
- **Responsive Design**: Works on mobile and desktop
|
12 |
+
|
13 |
+
## Getting Started
|
14 |
+
|
15 |
+
### Prerequisites
|
16 |
+
|
17 |
+
- Node.js 16+ installed
|
18 |
+
- A Glif API key
|
19 |
+
|
20 |
+
### Installation
|
21 |
+
|
22 |
+
1. Clone the repository:
|
23 |
+
```bash
|
24 |
+
git clone https://github.com/yourusername/glif-lora-autocaption.git
|
25 |
+
cd glif-lora-autocaption
|
26 |
+
```
|
27 |
+
|
28 |
+
2. Install dependencies:
|
29 |
+
```bash
|
30 |
+
npm install
|
31 |
+
```
|
32 |
+
|
33 |
+
3. Start the development server:
|
34 |
+
```bash
|
35 |
+
npm start
|
36 |
+
```
|
37 |
+
|
38 |
+
4. Open your browser and navigate to `http://localhost:5173`
|
39 |
+
|
40 |
+
### Building for Production
|
41 |
+
|
42 |
+
```bash
|
43 |
+
npm run build
|
44 |
+
```
|
45 |
+
|
46 |
+
## Usage
|
47 |
+
|
48 |
+
1. Enter your Glif API key in the secure input field
|
49 |
+
2. Click "Upload Images" to select one or more images
|
50 |
+
3. Click "Process Images" to generate captions
|
51 |
+
4. View the results as they are completed
|
52 |
+
|
53 |
+
## Deployment to Huggingface Spaces
|
54 |
+
|
55 |
+
This project includes a Dockerfile configured for Huggingface Spaces.
|
56 |
+
|
57 |
+
1. Create a new Space on Huggingface
|
58 |
+
2. Choose Docker as the SDK
|
59 |
+
3. Upload the project files
|
60 |
+
4. The Space will automatically build and deploy the application
|
61 |
+
|
62 |
+
## Tech Stack
|
63 |
+
|
64 |
+
- React
|
65 |
+
- Chakra UI
|
66 |
+
- Vite
|
67 |
+
- Axios
|
68 |
+
|
69 |
+
## License
|
70 |
+
|
71 |
+
MIT
|
72 |
+
|
73 |
+
---
|
74 |
+
|
75 |
+
*Created with ❤️ for the AI image captioning community*
|
index.html
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>Glif Lora Autocaption</title>
|
8 |
+
<meta name="description" content="Auto-caption your images using Glif API" />
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div id="root"></div>
|
12 |
+
<script type="module" src="/src/main.jsx"></script>
|
13 |
+
</body>
|
14 |
+
</html>
|
package.json
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "glif-lora-autocaption",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"description": "A tool to auto-caption images using the Glif API",
|
5 |
+
"main": "src/index.js",
|
6 |
+
"scripts": {
|
7 |
+
"start": "vite",
|
8 |
+
"build": "vite build",
|
9 |
+
"preview": "vite preview"
|
10 |
+
},
|
11 |
+
"dependencies": {
|
12 |
+
"react": "^18.2.0",
|
13 |
+
"react-dom": "^18.2.0",
|
14 |
+
"axios": "^1.6.2",
|
15 |
+
"@chakra-ui/react": "^2.8.2",
|
16 |
+
"@emotion/react": "^11.11.1",
|
17 |
+
"@emotion/styled": "^11.11.0",
|
18 |
+
"framer-motion": "^10.16.5",
|
19 |
+
"jszip": "^3.10.1",
|
20 |
+
"file-saver": "^2.0.5"
|
21 |
+
},
|
22 |
+
"devDependencies": {
|
23 |
+
"@vitejs/plugin-react": "^4.2.0",
|
24 |
+
"vite": "^5.0.2"
|
25 |
+
}
|
26 |
+
}
|
src/App.jsx
ADDED
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useRef } from 'react';
|
2 |
+
import {
|
3 |
+
Box,
|
4 |
+
Container,
|
5 |
+
Heading,
|
6 |
+
Text,
|
7 |
+
Input,
|
8 |
+
Button,
|
9 |
+
VStack,
|
10 |
+
HStack,
|
11 |
+
SimpleGrid,
|
12 |
+
Image,
|
13 |
+
FormControl,
|
14 |
+
FormLabel,
|
15 |
+
useToast,
|
16 |
+
Progress,
|
17 |
+
Badge,
|
18 |
+
Flex,
|
19 |
+
Code,
|
20 |
+
Icon,
|
21 |
+
Divider,
|
22 |
+
Tooltip
|
23 |
+
} from '@chakra-ui/react';
|
24 |
+
import axios from 'axios';
|
25 |
+
import JSZip from 'jszip';
|
26 |
+
import { saveAs } from 'file-saver';
|
27 |
+
|
28 |
+
// Function to convert file to base64
|
29 |
+
const toBase64 = file => new Promise((resolve, reject) => {
|
30 |
+
const reader = new FileReader();
|
31 |
+
reader.readAsDataURL(file);
|
32 |
+
reader.onload = () => resolve(reader.result.split(',')[1]);
|
33 |
+
reader.onerror = error => reject(error);
|
34 |
+
});
|
35 |
+
|
36 |
+
// Function to get file extension
|
37 |
+
const getFileExtension = filename => {
|
38 |
+
return filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2);
|
39 |
+
}
|
40 |
+
|
41 |
+
// Function to get filename without extension
|
42 |
+
const getFilenameWithoutExtension = filename => {
|
43 |
+
return filename.substring(0, filename.lastIndexOf("."));
|
44 |
+
}
|
45 |
+
|
46 |
+
const App = () => {
|
47 |
+
const [apiKey, setApiKey] = useState('');
|
48 |
+
const [uploadedImages, setUploadedImages] = useState([]);
|
49 |
+
const [processing, setProcessing] = useState(false);
|
50 |
+
const [downloading, setDownloading] = useState(false);
|
51 |
+
const [progress, setProgress] = useState(0);
|
52 |
+
const fileInputRef = useRef(null);
|
53 |
+
const toast = useToast();
|
54 |
+
|
55 |
+
const handleFileChange = async (e) => {
|
56 |
+
const files = Array.from(e.target.files);
|
57 |
+
|
58 |
+
if (files.length === 0) return;
|
59 |
+
|
60 |
+
// Initialize new images with placeholder data
|
61 |
+
const newImages = files.map(file => ({
|
62 |
+
id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
63 |
+
file: file,
|
64 |
+
name: file.name,
|
65 |
+
preview: URL.createObjectURL(file),
|
66 |
+
status: 'pending',
|
67 |
+
caption: '',
|
68 |
+
error: null
|
69 |
+
}));
|
70 |
+
|
71 |
+
setUploadedImages([...uploadedImages, ...newImages]);
|
72 |
+
};
|
73 |
+
|
74 |
+
const processImages = async () => {
|
75 |
+
if (!apiKey.trim()) {
|
76 |
+
toast({
|
77 |
+
title: 'API Key Required',
|
78 |
+
description: "Please enter your Glif API key.",
|
79 |
+
status: 'error',
|
80 |
+
duration: 3000,
|
81 |
+
isClosable: true,
|
82 |
+
});
|
83 |
+
return;
|
84 |
+
}
|
85 |
+
|
86 |
+
if (uploadedImages.length === 0) {
|
87 |
+
toast({
|
88 |
+
title: 'No Images',
|
89 |
+
description: "Please upload at least one image.",
|
90 |
+
status: 'warning',
|
91 |
+
duration: 3000,
|
92 |
+
isClosable: true,
|
93 |
+
});
|
94 |
+
return;
|
95 |
+
}
|
96 |
+
|
97 |
+
setProcessing(true);
|
98 |
+
const pendingImages = uploadedImages.filter(img => img.status === 'pending');
|
99 |
+
|
100 |
+
for (let i = 0; i < pendingImages.length; i++) {
|
101 |
+
const image = pendingImages[i];
|
102 |
+
|
103 |
+
setProgress(Math.floor((i / pendingImages.length) * 100));
|
104 |
+
|
105 |
+
// Update image status to processing
|
106 |
+
setUploadedImages(prev => prev.map(img =>
|
107 |
+
img.id === image.id ? { ...img, status: 'processing' } : img
|
108 |
+
));
|
109 |
+
|
110 |
+
try {
|
111 |
+
// Convert image to base64
|
112 |
+
const base64Image = await toBase64(image.file);
|
113 |
+
|
114 |
+
// Call the GLIF API
|
115 |
+
const response = await axios.post('https://simple-api.glif.app/cm7yya7850000la0ckalxpix2', {
|
116 |
+
image: base64Image
|
117 |
+
}, {
|
118 |
+
headers: {
|
119 |
+
'Authorization': `Bearer ${apiKey}`,
|
120 |
+
'Content-Type': 'application/json'
|
121 |
+
}
|
122 |
+
});
|
123 |
+
|
124 |
+
// Update image with caption
|
125 |
+
setUploadedImages(prev => prev.map(img =>
|
126 |
+
img.id === image.id ? {
|
127 |
+
...img,
|
128 |
+
status: 'completed',
|
129 |
+
caption: response.data.output || 'No caption generated'
|
130 |
+
} : img
|
131 |
+
));
|
132 |
+
|
133 |
+
} catch (error) {
|
134 |
+
console.error(`Error processing image ${image.name}:`, error);
|
135 |
+
|
136 |
+
// Update image with error
|
137 |
+
setUploadedImages(prev => prev.map(img =>
|
138 |
+
img.id === image.id ? {
|
139 |
+
...img,
|
140 |
+
status: 'error',
|
141 |
+
error: error.response?.data?.error || error.message || 'Unknown error occurred'
|
142 |
+
} : img
|
143 |
+
));
|
144 |
+
}
|
145 |
+
}
|
146 |
+
|
147 |
+
setProgress(100);
|
148 |
+
setProcessing(false);
|
149 |
+
|
150 |
+
toast({
|
151 |
+
title: 'Processing Complete',
|
152 |
+
description: "All images have been processed.",
|
153 |
+
status: 'success',
|
154 |
+
duration: 5000,
|
155 |
+
isClosable: true,
|
156 |
+
});
|
157 |
+
};
|
158 |
+
|
159 |
+
const downloadCaptions = async () => {
|
160 |
+
const completedImages = uploadedImages.filter(img => img.status === 'completed' && img.caption);
|
161 |
+
|
162 |
+
if (completedImages.length === 0) {
|
163 |
+
toast({
|
164 |
+
title: 'No Captions Available',
|
165 |
+
description: "There are no completed captions to download.",
|
166 |
+
status: 'warning',
|
167 |
+
duration: 3000,
|
168 |
+
isClosable: true,
|
169 |
+
});
|
170 |
+
return;
|
171 |
+
}
|
172 |
+
|
173 |
+
setDownloading(true);
|
174 |
+
|
175 |
+
try {
|
176 |
+
// Create a new zip file
|
177 |
+
const zip = new JSZip();
|
178 |
+
|
179 |
+
// Add text files to the zip with the same filenames as the images but .txt extension
|
180 |
+
completedImages.forEach(image => {
|
181 |
+
const fileName = getFilenameWithoutExtension(image.name) + ".txt";
|
182 |
+
zip.file(fileName, image.caption);
|
183 |
+
});
|
184 |
+
|
185 |
+
// Generate the zip file
|
186 |
+
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
187 |
+
|
188 |
+
// Save the zip file
|
189 |
+
saveAs(zipBlob, "glif-captions.zip");
|
190 |
+
|
191 |
+
toast({
|
192 |
+
title: 'Download Complete',
|
193 |
+
description: `Successfully packaged ${completedImages.length} captions.`,
|
194 |
+
status: 'success',
|
195 |
+
duration: 3000,
|
196 |
+
isClosable: true,
|
197 |
+
});
|
198 |
+
} catch (error) {
|
199 |
+
console.error("Error creating zip file:", error);
|
200 |
+
toast({
|
201 |
+
title: 'Download Failed',
|
202 |
+
description: "Failed to create zip file. " + error.message,
|
203 |
+
status: 'error',
|
204 |
+
duration: 3000,
|
205 |
+
isClosable: true,
|
206 |
+
});
|
207 |
+
} finally {
|
208 |
+
setDownloading(false);
|
209 |
+
}
|
210 |
+
};
|
211 |
+
|
212 |
+
const removeImage = (id) => {
|
213 |
+
setUploadedImages(prev => prev.filter(img => img.id !== id));
|
214 |
+
};
|
215 |
+
|
216 |
+
const clearAll = () => {
|
217 |
+
setUploadedImages([]);
|
218 |
+
if (fileInputRef.current) fileInputRef.current.value = '';
|
219 |
+
};
|
220 |
+
|
221 |
+
const getStatusBadge = (status) => {
|
222 |
+
switch (status) {
|
223 |
+
case 'pending':
|
224 |
+
return <Badge colorScheme="gray">Pending</Badge>;
|
225 |
+
case 'processing':
|
226 |
+
return <Badge colorScheme="blue">Processing</Badge>;
|
227 |
+
case 'completed':
|
228 |
+
return <Badge colorScheme="green">Completed</Badge>;
|
229 |
+
case 'error':
|
230 |
+
return <Badge colorScheme="red">Error</Badge>;
|
231 |
+
default:
|
232 |
+
return null;
|
233 |
+
}
|
234 |
+
};
|
235 |
+
|
236 |
+
// Check if we have any completed captions
|
237 |
+
const hasCaptions = uploadedImages.some(img => img.status === 'completed' && img.caption);
|
238 |
+
|
239 |
+
return (
|
240 |
+
<Container maxW="container.xl" py={10}>
|
241 |
+
<VStack spacing={8} align="stretch">
|
242 |
+
<Box textAlign="center">
|
243 |
+
<Heading as="h1" size="2xl" mb={2} bgGradient="linear(to-r, cyan.400, blue.500, purple.600)" bgClip="text">
|
244 |
+
Glif Lora Autocaption
|
245 |
+
</Heading>
|
246 |
+
<Text fontSize="lg" color="gray.400">
|
247 |
+
Generate captions for your images using Glif API
|
248 |
+
</Text>
|
249 |
+
</Box>
|
250 |
+
|
251 |
+
<Divider />
|
252 |
+
|
253 |
+
<Box>
|
254 |
+
<FormControl mb={4}>
|
255 |
+
<FormLabel>API Key</FormLabel>
|
256 |
+
<Input
|
257 |
+
type="password"
|
258 |
+
placeholder="Enter your Glif API key"
|
259 |
+
value={apiKey}
|
260 |
+
onChange={(e) => setApiKey(e.target.value)}
|
261 |
+
/>
|
262 |
+
</FormControl>
|
263 |
+
|
264 |
+
<Flex flexWrap="wrap" gap={4} mb={6} justify="center">
|
265 |
+
<Button
|
266 |
+
onClick={() => fileInputRef.current?.click()}
|
267 |
+
variant="solid"
|
268 |
+
size="md"
|
269 |
+
disabled={processing}
|
270 |
+
>
|
271 |
+
Upload Images
|
272 |
+
</Button>
|
273 |
+
<input
|
274 |
+
type="file"
|
275 |
+
multiple
|
276 |
+
accept="image/*"
|
277 |
+
onChange={handleFileChange}
|
278 |
+
style={{ display: 'none' }}
|
279 |
+
ref={fileInputRef}
|
280 |
+
/>
|
281 |
+
|
282 |
+
<Button
|
283 |
+
onClick={processImages}
|
284 |
+
colorScheme="blue"
|
285 |
+
isLoading={processing}
|
286 |
+
loadingText="Processing"
|
287 |
+
disabled={processing || uploadedImages.filter(img => img.status === 'pending').length === 0}
|
288 |
+
>
|
289 |
+
Process Images
|
290 |
+
</Button>
|
291 |
+
|
292 |
+
<Button
|
293 |
+
onClick={downloadCaptions}
|
294 |
+
colorScheme="teal"
|
295 |
+
isLoading={downloading}
|
296 |
+
loadingText="Downloading"
|
297 |
+
disabled={downloading || !hasCaptions}
|
298 |
+
>
|
299 |
+
Download Captions
|
300 |
+
</Button>
|
301 |
+
|
302 |
+
<Button
|
303 |
+
onClick={clearAll}
|
304 |
+
variant="outline"
|
305 |
+
disabled={processing || uploadedImages.length === 0}
|
306 |
+
>
|
307 |
+
Clear All
|
308 |
+
</Button>
|
309 |
+
</Flex>
|
310 |
+
|
311 |
+
{processing && (
|
312 |
+
<Progress
|
313 |
+
value={progress}
|
314 |
+
size="sm"
|
315 |
+
colorScheme="blue"
|
316 |
+
hasStripe
|
317 |
+
mb={4}
|
318 |
+
borderRadius="md"
|
319 |
+
/>
|
320 |
+
)}
|
321 |
+
</Box>
|
322 |
+
|
323 |
+
{uploadedImages.length > 0 ? (
|
324 |
+
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6}>
|
325 |
+
{uploadedImages.map((image) => (
|
326 |
+
<Box
|
327 |
+
key={image.id}
|
328 |
+
borderWidth="1px"
|
329 |
+
borderRadius="lg"
|
330 |
+
overflow="hidden"
|
331 |
+
bg="gray.900"
|
332 |
+
position="relative"
|
333 |
+
>
|
334 |
+
<Box position="absolute" top={2} right={2} zIndex={1}>
|
335 |
+
{getStatusBadge(image.status)}
|
336 |
+
</Box>
|
337 |
+
<Image
|
338 |
+
src={image.preview}
|
339 |
+
alt={image.name}
|
340 |
+
w="100%"
|
341 |
+
h="200px"
|
342 |
+
objectFit="cover"
|
343 |
+
/>
|
344 |
+
<Box p={4}>
|
345 |
+
<Flex justify="space-between" align="center" mb={2}>
|
346 |
+
<Text fontWeight="semibold" isTruncated maxW="70%">{image.name}</Text>
|
347 |
+
<Button
|
348 |
+
size="xs"
|
349 |
+
onClick={() => removeImage(image.id)}
|
350 |
+
disabled={processing}
|
351 |
+
>
|
352 |
+
Remove
|
353 |
+
</Button>
|
354 |
+
</Flex>
|
355 |
+
|
356 |
+
{image.status === 'completed' && (
|
357 |
+
<Box mt={2} p={2} bg="gray.800" borderRadius="md">
|
358 |
+
<Text fontSize="sm">{image.caption}</Text>
|
359 |
+
</Box>
|
360 |
+
)}
|
361 |
+
|
362 |
+
{image.status === 'error' && (
|
363 |
+
<Box mt={2} p={2} bg="red.900" borderRadius="md">
|
364 |
+
<Text fontSize="sm" color="red.200">{image.error}</Text>
|
365 |
+
</Box>
|
366 |
+
)}
|
367 |
+
</Box>
|
368 |
+
</Box>
|
369 |
+
))}
|
370 |
+
</SimpleGrid>
|
371 |
+
) : (
|
372 |
+
<Box
|
373 |
+
textAlign="center"
|
374 |
+
p={10}
|
375 |
+
borderWidth="1px"
|
376 |
+
borderRadius="lg"
|
377 |
+
borderStyle="dashed"
|
378 |
+
>
|
379 |
+
<Text color="gray.500">Upload images to get started</Text>
|
380 |
+
</Box>
|
381 |
+
)}
|
382 |
+
|
383 |
+
<Divider mt={6} />
|
384 |
+
|
385 |
+
<Box as="footer" textAlign="center">
|
386 |
+
<Text fontSize="sm" color="gray.500">
|
387 |
+
Glif Lora Autocaption • {new Date().getFullYear()}
|
388 |
+
</Text>
|
389 |
+
</Box>
|
390 |
+
</VStack>
|
391 |
+
</Container>
|
392 |
+
);
|
393 |
+
};
|
394 |
+
|
395 |
+
export default App;
|
src/main.jsx
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import ReactDOM from 'react-dom/client';
|
3 |
+
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
|
4 |
+
import App from './App';
|
5 |
+
|
6 |
+
// Theme configuration for dark mode
|
7 |
+
const theme = extendTheme({
|
8 |
+
config: {
|
9 |
+
initialColorMode: 'dark',
|
10 |
+
useSystemColorMode: false,
|
11 |
+
},
|
12 |
+
styles: {
|
13 |
+
global: {
|
14 |
+
body: {
|
15 |
+
bg: '#000',
|
16 |
+
color: '#fff',
|
17 |
+
}
|
18 |
+
}
|
19 |
+
},
|
20 |
+
colors: {
|
21 |
+
brand: {
|
22 |
+
100: '#f7fafc',
|
23 |
+
900: '#1a202c',
|
24 |
+
},
|
25 |
+
gray: {
|
26 |
+
50: '#f7fafc',
|
27 |
+
100: '#edf2f7',
|
28 |
+
700: '#2d3748',
|
29 |
+
800: '#1a202c',
|
30 |
+
900: '#0f1116',
|
31 |
+
}
|
32 |
+
},
|
33 |
+
components: {
|
34 |
+
Button: {
|
35 |
+
baseStyle: {
|
36 |
+
borderRadius: 'md',
|
37 |
+
},
|
38 |
+
variants: {
|
39 |
+
solid: {
|
40 |
+
bg: 'gray.700',
|
41 |
+
color: 'white',
|
42 |
+
_hover: {
|
43 |
+
bg: 'gray.800',
|
44 |
+
}
|
45 |
+
}
|
46 |
+
}
|
47 |
+
},
|
48 |
+
Input: {
|
49 |
+
variants: {
|
50 |
+
filled: {
|
51 |
+
field: {
|
52 |
+
bg: 'gray.800',
|
53 |
+
_hover: {
|
54 |
+
bg: 'gray.700',
|
55 |
+
},
|
56 |
+
_focus: {
|
57 |
+
bg: 'gray.700',
|
58 |
+
}
|
59 |
+
}
|
60 |
+
}
|
61 |
+
},
|
62 |
+
defaultProps: {
|
63 |
+
variant: 'filled',
|
64 |
+
}
|
65 |
+
}
|
66 |
+
}
|
67 |
+
});
|
68 |
+
|
69 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
70 |
+
<React.StrictMode>
|
71 |
+
<ChakraProvider theme={theme}>
|
72 |
+
<App />
|
73 |
+
</ChakraProvider>
|
74 |
+
</React.StrictMode>
|
75 |
+
);
|
vite.config.js
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { defineConfig } from 'vite';
|
2 |
+
import react from '@vitejs/plugin-react';
|
3 |
+
|
4 |
+
export default defineConfig({
|
5 |
+
plugins: [react()],
|
6 |
+
server: {
|
7 |
+
port: 7860, // Default port for HuggingFace Spaces
|
8 |
+
host: '0.0.0.0'
|
9 |
+
}
|
10 |
+
});
|