saq1b commited on
Commit
eceb7aa
·
1 Parent(s): 04d71ea

initialize project with Vite, React, and essential configurations

Browse files
Files changed (7) hide show
  1. Dockerfile +42 -0
  2. README copy.md +75 -0
  3. index.html +14 -0
  4. package.json +26 -0
  5. src/App.jsx +395 -0
  6. src/main.jsx +75 -0
  7. 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
+ });