hardik-2004 commited on
Commit
36a42d4
·
verified ·
1 Parent(s): 873f8cd

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. .env +1 -0
  2. Dockerfile +26 -0
  3. README.md +51 -11
  4. app.py +121 -0
  5. requirements.txt +8 -0
  6. templates/index.html +705 -0
.env ADDED
@@ -0,0 +1 @@
 
 
1
+ GOOGLE_API = "AIzaSyBzOJkd7iq1S3reFlgMeMrQb0azVsfUn6c"
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ git \
9
+ libgl1-mesa-glx \
10
+ libglib2.0-0 \
11
+ ffmpeg \
12
+ curl \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Copy files
16
+ COPY . .
17
+
18
+ # Install Python packages
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Expose the port for Hugging Face
22
+ ENV PORT 7860
23
+ EXPOSE 7860
24
+
25
+ # Run the FastAPI server
26
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,51 @@
1
- ---
2
- title: Food Menu
3
- emoji: 🚀
4
- colorFrom: yellow
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: 🍽️ AI Food Menu Generator
3
+ emoji: 📸
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: docker
7
+ pinned: false
8
+ app_file: app.py
9
+ ---
10
+
11
+ # 🍽️ AI Food Menu Generator
12
+
13
+ Welcome to the **AI Food Menu Generator** – an intelligent web app that extracts food items from a menu image and generates realistic food images using text-to-image AI.
14
+
15
+ ## 🚀 Features
16
+
17
+ - 📸 Upload a food menu image
18
+ - 🧠 Extract food names using image-to-text models
19
+ - 🎨 Generate food images using AI models like `gemma:3b` or similar via Ollama
20
+ - 🖼️ View results instantly in your browser
21
+ - ⚡ FastAPI backend + Hugging Face Spaces deployment via Docker
22
+
23
+
24
+ ## 🧪 Example Usage
25
+
26
+ 1. Upload a food menu image
27
+ 2. The app extracts menu items (like `Paneer Tikka`, `Burger`)
28
+ 3. For each item, it generates a realistic food image using text-to-image AI
29
+ 4. Results are shown with image previews
30
+
31
+ ---
32
+
33
+ ## 🔧 Tech Stack
34
+
35
+ - **FastAPI** - backend API server
36
+ - **Ollama + Gemma** - local model for image generation
37
+ - **Docker** - for containerized deployment
38
+ - **Hugging Face Spaces** - for live hosting
39
+ - **Diffusers & Transformers** - model inference
40
+ - **Pillow** - image handling
41
+ - **Python** - ❤️
42
+
43
+ ---
44
+
45
+ ## 🐳 Docker Usage (Optional for Local Run)
46
+
47
+ ```bash
48
+ docker build -t ai-food-menu .
49
+
50
+ docker run -p 7860:7860 ai-food-menu
51
+
app.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, Request
2
+ from fastapi.responses import HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.templating import Jinja2Templates
5
+
6
+ import os
7
+ import json
8
+ import base64
9
+ import re
10
+ from dotenv import load_dotenv
11
+ import requests
12
+ import torch
13
+ from diffusers import StableDiffusionPipeline
14
+ from PIL import Image
15
+ from io import BytesIO
16
+ from imghdr import what
17
+
18
+ load_dotenv()
19
+ api_key = os.getenv("GOOGLE_API")
20
+
21
+ app = FastAPI()
22
+
23
+ templates = Jinja2Templates(directory="templates")
24
+
25
+ device = "cuda" if torch.cuda.is_available() else "cpu"
26
+ pipe = StableDiffusionPipeline.from_pretrained(
27
+ "runwayml/stable-diffusion-v1-5",
28
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32
29
+ )
30
+ pipe.to(device)
31
+
32
+
33
+ def clean_filename(text):
34
+ return re.sub(r'[^\w\-_\. ]', '_', text.strip().lower().replace(" ", "_"))
35
+
36
+
37
+ def generate_image_base64(food_name):
38
+ prompt = f"Professional food photography of {food_name}, top-down view, realistic lighting"
39
+ image = pipe(prompt).images[0]
40
+
41
+ buffered = BytesIO()
42
+ image.save(buffered, format="PNG")
43
+ encoded_image = base64.b64encode(buffered.getvalue()).decode("utf-8")
44
+ return encoded_image
45
+
46
+ def get_mime_type(image_bytes):
47
+ kind = what(None, h=image_bytes)
48
+ return f"image/{kind or 'jpeg'}"
49
+
50
+ def extract_menu_from_image(image_bytes):
51
+ base64_image = base64.b64encode(image_bytes).decode('utf-8')
52
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={api_key}"
53
+
54
+ prompt = """
55
+ Extract the menu items from this image and return ONLY a JSON array like:
56
+ [
57
+ {
58
+ "food": "Dish Name",
59
+ "description": "Short description or empty string",
60
+ "price": 10,
61
+ "category": "Category"
62
+ }
63
+ ]
64
+ """
65
+
66
+ payload = {
67
+ "contents": [
68
+ {
69
+ "parts": [
70
+ {"text": prompt},
71
+ {
72
+ "inline_data": {
73
+ "mime_type": get_mime_type(image_bytes),
74
+ "data": base64_image
75
+ }
76
+ }
77
+ ]
78
+ }
79
+ ],
80
+ "generationConfig": {
81
+ "responseMimeType": "application/json"
82
+ }
83
+ }
84
+
85
+ headers = {'Content-Type': 'application/json'}
86
+
87
+ try:
88
+ res = requests.post(url, headers=headers, json=payload)
89
+ res.raise_for_status()
90
+ text = res.json()['candidates'][0]['content']['parts'][0]['text']
91
+ return json.loads(text)
92
+ except Exception as e:
93
+ print("Error extracting menu:", e)
94
+ return []
95
+
96
+
97
+ @app.get("/", response_class=HTMLResponse)
98
+ async def form(request: Request):
99
+ return templates.TemplateResponse("index.html", {"request": request})
100
+
101
+
102
+ @app.post("/upload", response_class=HTMLResponse)
103
+ async def upload(request: Request, menu_image: UploadFile = File(...)):
104
+ image_bytes = await menu_image.read()
105
+ menu_items = extract_menu_from_image(image_bytes)
106
+
107
+ for item in menu_items:
108
+ item["img_base64"] = generate_image_base64(item["food"])
109
+
110
+ html = "<h2>🍽️ AI Food Menu</h2><div style='display:flex; flex-wrap:wrap;'>"
111
+ for item in menu_items:
112
+ html += f"""
113
+ <div style='border:1px solid #ccc; margin:10px; width:220px; text-align:center; padding:10px; border-radius:10px; box-shadow:2px 2px 5px #aaa;'>
114
+ <img src='data:image/png;base64,{item["img_base64"]}' width='200'><br>
115
+ <h3>{item['food']}</h3>
116
+ <p><b>${item['price']}</b></p>
117
+ <p>{item['description']}</p>
118
+ </div>
119
+ """
120
+ html += "</div><br><a href='/'>Upload Another</a>"
121
+ return HTMLResponse(content=html)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ requests
5
+ python-dotenv
6
+ torch
7
+ diffusers[torch]
8
+ Pillow
templates/index.html ADDED
@@ -0,0 +1,705 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Food Menu Generator</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
16
+ background: linear-gradient(135deg, #fff7ed 0%, #ffffff 50%, #fef2f2 100%);
17
+ min-height: 100vh;
18
+ color: #374151;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ padding: 2rem 1rem;
25
+ }
26
+
27
+ .header {
28
+ text-align: center;
29
+ margin-bottom: 3rem;
30
+ }
31
+
32
+ .title {
33
+ font-size: 3rem;
34
+ font-weight: bold;
35
+ background: linear-gradient(to right, #ea580c, #dc2626);
36
+ -webkit-background-clip: text;
37
+ -webkit-text-fill-color: transparent;
38
+ background-clip: text;
39
+ margin-bottom: 1rem;
40
+ }
41
+
42
+ .subtitle {
43
+ font-size: 1.25rem;
44
+ color: #6b7280;
45
+ max-width: 32rem;
46
+ margin: 0 auto;
47
+ }
48
+
49
+ .main-content {
50
+ max-width: 64rem;
51
+ margin: 0 auto;
52
+ }
53
+
54
+ .card {
55
+ background: white;
56
+ border-radius: 0.5rem;
57
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
58
+ margin-bottom: 2rem;
59
+ }
60
+
61
+ .upload-card {
62
+ padding: 2rem;
63
+ }
64
+
65
+ .upload-content {
66
+ text-align: center;
67
+ }
68
+
69
+ .upload-area {
70
+ border: 2px dashed #d1d5db;
71
+ border-radius: 0.5rem;
72
+ padding: 2rem;
73
+ margin-bottom: 1.5rem;
74
+ transition: border-color 0.3s ease;
75
+ cursor: pointer;
76
+ }
77
+
78
+ .upload-area:hover {
79
+ border-color: #fb923c;
80
+ }
81
+
82
+ .file-input {
83
+ display: none;
84
+ }
85
+
86
+ .upload-label {
87
+ cursor: pointer;
88
+ display: block;
89
+ }
90
+
91
+ .upload-icon-container {
92
+ display: flex;
93
+ flex-direction: column;
94
+ align-items: center;
95
+ }
96
+
97
+ .upload-icon {
98
+ width: 3rem;
99
+ height: 3rem;
100
+ color: #9ca3af;
101
+ margin-bottom: 1rem;
102
+ }
103
+
104
+ .upload-title {
105
+ font-size: 1.125rem;
106
+ font-weight: 500;
107
+ color: #374151;
108
+ margin-bottom: 0.5rem;
109
+ }
110
+
111
+ .upload-subtitle {
112
+ font-size: 0.875rem;
113
+ color: #6b7280;
114
+ }
115
+
116
+ .preview-section {
117
+ margin-bottom: 1.5rem;
118
+ }
119
+
120
+ .preview-title {
121
+ font-size: 1.125rem;
122
+ font-weight: 600;
123
+ margin-bottom: 1rem;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ gap: 0.5rem;
128
+ }
129
+
130
+ .preview-icon {
131
+ width: 1.25rem;
132
+ height: 1.25rem;
133
+ }
134
+
135
+ .preview-container {
136
+ max-width: 24rem;
137
+ margin: 0 auto;
138
+ }
139
+
140
+ .preview-image {
141
+ width: 100%;
142
+ height: auto;
143
+ border-radius: 0.5rem;
144
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
145
+ border: 1px solid #e5e7eb;
146
+ }
147
+
148
+ .button-container {
149
+ display: flex;
150
+ gap: 1rem;
151
+ justify-content: center;
152
+ }
153
+
154
+ .btn {
155
+ padding: 0.75rem 2rem;
156
+ font-size: 1.125rem;
157
+ font-weight: 500;
158
+ border-radius: 0.375rem;
159
+ border: none;
160
+ cursor: pointer;
161
+ transition: all 0.3s ease;
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 0.5rem;
165
+ }
166
+
167
+ .btn:disabled {
168
+ opacity: 0.6;
169
+ cursor: not-allowed;
170
+ }
171
+
172
+ .btn-primary {
173
+ background: linear-gradient(to right, #f97316, #ef4444);
174
+ color: white;
175
+ }
176
+
177
+ .btn-primary:hover:not(:disabled) {
178
+ background: linear-gradient(to right, #ea580c, #dc2626);
179
+ }
180
+
181
+ .btn-secondary {
182
+ border: 1px solid #d1d5db;
183
+ background: white;
184
+ color: #374151;
185
+ }
186
+
187
+ .btn-secondary:hover {
188
+ background: #f9fafb;
189
+ }
190
+
191
+ .loading-icon {
192
+ width: 1.25rem;
193
+ height: 1.25rem;
194
+ animation: spin 1s linear infinite;
195
+ }
196
+
197
+ @keyframes spin {
198
+ from { transform: rotate(0deg); }
199
+ to { transform: rotate(360deg); }
200
+ }
201
+
202
+ .processing-card {
203
+ padding: 2rem;
204
+ }
205
+
206
+ .processing-content {
207
+ text-align: center;
208
+ }
209
+
210
+ .processing-header {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ gap: 1rem;
215
+ margin-bottom: 1rem;
216
+ }
217
+
218
+ .processing-spinner {
219
+ width: 2rem;
220
+ height: 2rem;
221
+ color: #f97316;
222
+ animation: spin 1s linear infinite;
223
+ }
224
+
225
+ .processing-title {
226
+ font-size: 1.25rem;
227
+ font-weight: 600;
228
+ color: #374151;
229
+ }
230
+
231
+ .processing-steps {
232
+ color: #6b7280;
233
+ }
234
+
235
+ .processing-steps p {
236
+ margin-bottom: 0.25rem;
237
+ }
238
+
239
+ .menu-section {
240
+ margin-bottom: 2rem;
241
+ }
242
+
243
+ .menu-title {
244
+ font-size: 1.875rem;
245
+ font-weight: bold;
246
+ text-align: center;
247
+ margin-bottom: 2rem;
248
+ color: #1f2937;
249
+ }
250
+
251
+ .menu-grid {
252
+ display: grid;
253
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
254
+ gap: 1.5rem;
255
+ }
256
+
257
+ .menu-item {
258
+ background: white;
259
+ border-radius: 0.5rem;
260
+ overflow: hidden;
261
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
262
+ transition: all 0.3s ease;
263
+ cursor: pointer;
264
+ }
265
+
266
+ .menu-item:hover {
267
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
268
+ transform: translateY(-0.5rem);
269
+ }
270
+
271
+ .menu-item-image-container {
272
+ position: relative;
273
+ overflow: hidden;
274
+ }
275
+
276
+ .menu-item-image {
277
+ width: 100%;
278
+ height: 12rem;
279
+ object-fit: cover;
280
+ transition: transform 0.3s ease;
281
+ }
282
+
283
+ .menu-item:hover .menu-item-image {
284
+ transform: scale(1.05);
285
+ }
286
+
287
+ .menu-item-price-badge {
288
+ position: absolute;
289
+ top: 0.5rem;
290
+ right: 0.5rem;
291
+ background: linear-gradient(to right, #f97316, #ef4444);
292
+ color: white;
293
+ padding: 0.25rem 0.75rem;
294
+ border-radius: 9999px;
295
+ font-size: 0.875rem;
296
+ font-weight: 600;
297
+ }
298
+
299
+ .menu-item-content {
300
+ padding: 1rem;
301
+ }
302
+
303
+ .menu-item-name {
304
+ font-weight: bold;
305
+ font-size: 1.125rem;
306
+ margin-bottom: 0.5rem;
307
+ color: #1f2937;
308
+ display: -webkit-box;
309
+ -webkit-box-orient: vertical;
310
+ overflow: hidden;
311
+ }
312
+
313
+ .menu-item-description {
314
+ color: #6b7280;
315
+ font-size: 0.875rem;
316
+ margin-bottom: 0.5rem;
317
+ display: -webkit-box;
318
+ -webkit-box-orient: vertical;
319
+ overflow: hidden;
320
+ }
321
+
322
+ .menu-item-footer {
323
+ display: flex;
324
+ justify-content: space-between;
325
+ align-items: center;
326
+ }
327
+
328
+ .menu-item-category {
329
+ font-size: 0.75rem;
330
+ background: #f3f4f6;
331
+ padding: 0.25rem 0.5rem;
332
+ border-radius: 9999px;
333
+ color: #6b7280;
334
+ }
335
+
336
+ .menu-item-price {
337
+ font-weight: bold;
338
+ font-size: 1.125rem;
339
+ color: #ea580c;
340
+ }
341
+
342
+ .hidden {
343
+ display: none !important;
344
+ }
345
+
346
+ .toast {
347
+ position: fixed;
348
+ top: 1rem;
349
+ right: 1rem;
350
+ background: white;
351
+ border-radius: 0.5rem;
352
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
353
+ padding: 1rem;
354
+ max-width: 24rem;
355
+ z-index: 1000;
356
+ border-left: 4px solid #10b981;
357
+ display: flex;
358
+ align-items: start;
359
+ justify-content: space-between;
360
+ }
361
+
362
+ .toast.error {
363
+ border-left-color: #ef4444;
364
+ }
365
+
366
+ .toast-content {
367
+ flex: 1;
368
+ }
369
+
370
+ .toast-title {
371
+ font-weight: 600;
372
+ margin-bottom: 0.25rem;
373
+ color: #1f2937;
374
+ }
375
+
376
+ .toast-description {
377
+ color: #6b7280;
378
+ font-size: 0.875rem;
379
+ }
380
+
381
+ .toast-close {
382
+ background: none;
383
+ border: none;
384
+ font-size: 1.25rem;
385
+ cursor: pointer;
386
+ color: #6b7280;
387
+ padding: 0.25rem;
388
+ margin-left: 1rem;
389
+ width: 1.5rem;
390
+ height: 1.5rem;
391
+ }
392
+
393
+ .toast-close:hover {
394
+ color: #374151;
395
+ }
396
+
397
+ @media (max-width: 768px) {
398
+ .title {
399
+ font-size: 2rem;
400
+ }
401
+
402
+ .subtitle {
403
+ font-size: 1rem;
404
+ }
405
+
406
+ .button-container {
407
+ flex-direction: column;
408
+ align-items: center;
409
+ }
410
+
411
+ .btn {
412
+ width: 100%;
413
+ max-width: 16rem;
414
+ }
415
+
416
+ .menu-grid {
417
+ grid-template-columns: 1fr;
418
+ }
419
+ }
420
+ </style>
421
+ </head>
422
+ <body>
423
+ <div class="container">
424
+ <div class="header">
425
+ <h1 class="title">🍽️ AI Food Menu Generator</h1>
426
+ <p class="subtitle">
427
+ Upload your menu image and watch AI extract menu items and generate beautiful food photos
428
+ </p>
429
+ </div>
430
+
431
+ <div class="main-content">
432
+ <div class="upload-card card">
433
+ <div class="upload-content">
434
+ <div class="upload-area" id="uploadArea">
435
+ <input type="file" accept="image/*" id="menuUpload" class="file-input">
436
+ <label for="menuUpload" class="upload-label">
437
+ <div class="upload-icon-container">
438
+ <svg class="upload-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
439
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
440
+ </svg>
441
+ <p class="upload-title">Upload Menu Image</p>
442
+ <p class="upload-subtitle">Click to select or drag and drop your menu image</p>
443
+ </div>
444
+ </label>
445
+ </div>
446
+
447
+ <div id="previewSection" class="preview-section hidden">
448
+ <h3 class="preview-title">
449
+ <svg class="preview-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
450
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
451
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
452
+ </svg>
453
+ Uploaded Menu Image
454
+ </h3>
455
+ <div class="preview-container">
456
+ <img id="previewImage" src="" alt="Menu preview" class="preview-image">
457
+ </div>
458
+ </div>
459
+
460
+ <div class="button-container">
461
+ <button id="generateBtn" class="btn btn-primary" disabled>
462
+ <span id="generateBtnText">Generate Menu</span>
463
+ <svg id="loadingIcon" class="loading-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
464
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
465
+ </svg>
466
+ </button>
467
+
468
+ <button id="resetBtn" class="btn btn-secondary hidden">
469
+ Upload Another
470
+ </button>
471
+ </div>
472
+ </div>
473
+ </div>
474
+
475
+ <div id="processingCard" class="processing-card card hidden">
476
+ <div class="processing-content">
477
+ <div class="processing-header">
478
+ <svg class="processing-spinner" fill="none" stroke="currentColor" viewBox="0 0 24 24">
479
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
480
+ </svg>
481
+ <span class="processing-title">AI is processing your menu...</span>
482
+ </div>
483
+ <div class="processing-steps">
484
+ <p>🔍 Extracting menu items from image</p>
485
+ <p>🎨 Generating beautiful food photos</p>
486
+ <p>✨ Creating your digital menu</p>
487
+ </div>
488
+ </div>
489
+ </div>
490
+
491
+ <div id="menuSection" class="menu-section hidden">
492
+ <h2 class="menu-title">🍽️ Your AI-Generated Menu</h2>
493
+ <div id="menuGrid" class="menu-grid">
494
+ </div>
495
+ </div>
496
+ </div>
497
+
498
+ <div id="toast" class="toast hidden">
499
+ <div class="toast-content">
500
+ <h4 id="toastTitle" class="toast-title"></h4>
501
+ <p id="toastDescription" class="toast-description"></p>
502
+ </div>
503
+ <button id="toastClose" class="toast-close">×</button>
504
+ </div>
505
+ </div>
506
+
507
+ <script>
508
+ class MenuUploader {
509
+ constructor() {
510
+ this.selectedImage = null;
511
+ this.previewUrl = null;
512
+ this.menuItems = [];
513
+ this.isProcessing = false;
514
+
515
+ this.initializeElements();
516
+ this.bindEvents();
517
+ }
518
+
519
+ initializeElements() {
520
+ this.uploadArea = document.getElementById('uploadArea');
521
+ this.fileInput = document.getElementById('menuUpload');
522
+ this.previewSection = document.getElementById('previewSection');
523
+ this.previewImage = document.getElementById('previewImage');
524
+ this.generateBtn = document.getElementById('generateBtn');
525
+ this.generateBtnText = document.getElementById('generateBtnText');
526
+ this.loadingIcon = document.getElementById('loadingIcon');
527
+ this.resetBtn = document.getElementById('resetBtn');
528
+ this.processingCard = document.getElementById('processingCard');
529
+ this.menuSection = document.getElementById('menuSection');
530
+ this.menuGrid = document.getElementById('menuGrid');
531
+ this.toast = document.getElementById('toast');
532
+ this.toastTitle = document.getElementById('toastTitle');
533
+ this.toastDescription = document.getElementById('toastDescription');
534
+ this.toastClose = document.getElementById('toastClose');
535
+ }
536
+
537
+ bindEvents() {
538
+ this.fileInput.addEventListener('change', (e) => this.handleImageSelect(e));
539
+ this.generateBtn.addEventListener('click', () => this.handleUpload());
540
+ this.resetBtn.addEventListener('click', () => this.handleReset());
541
+ this.toastClose.addEventListener('click', () => this.hideToast());
542
+
543
+ this.uploadArea.addEventListener('dragover', (e) => {
544
+ e.preventDefault();
545
+ this.uploadArea.style.borderColor = '#fb923c';
546
+ });
547
+
548
+ this.uploadArea.addEventListener('dragleave', (e) => {
549
+ e.preventDefault();
550
+ this.uploadArea.style.borderColor = '#d1d5db';
551
+ });
552
+
553
+ this.uploadArea.addEventListener('drop', (e) => {
554
+ e.preventDefault();
555
+ this.uploadArea.style.borderColor = '#d1d5db';
556
+ const files = e.dataTransfer.files;
557
+ if (files.length > 0) {
558
+ this.processFile(files[0]);
559
+ }
560
+ });
561
+ }
562
+
563
+ handleImageSelect(event) {
564
+ const file = event.target.files?.[0];
565
+ if (file) {
566
+ this.processFile(file);
567
+ }
568
+ }
569
+
570
+ processFile(file) {
571
+ if (!file.type.startsWith('image/')) {
572
+ this.showToast('Invalid file type', 'Please select an image file.', 'error');
573
+ return;
574
+ }
575
+
576
+ this.selectedImage = file;
577
+ const reader = new FileReader();
578
+ reader.onload = (e) => {
579
+ this.previewUrl = e.target.result;
580
+ this.previewImage.src = this.previewUrl;
581
+ this.previewSection.classList.remove('hidden');
582
+ this.resetBtn.classList.remove('hidden');
583
+ this.generateBtn.disabled = false;
584
+ };
585
+ reader.readAsDataURL(file);
586
+ }
587
+
588
+ async handleUpload() {
589
+ if (!this.selectedImage) {
590
+ this.showToast('No image selected', 'Please select a menu image to upload.', 'error');
591
+ return;
592
+ }
593
+
594
+ this.setProcessingState(true);
595
+
596
+ const formData = new FormData();
597
+ formData.append("menu_image", this.selectedImage);
598
+
599
+ try {
600
+ const response = await fetch("/upload", {
601
+ method: "POST",
602
+ body: formData
603
+ });
604
+
605
+ const html = await response.text();
606
+ document.open();
607
+ document.write(html);
608
+ document.close();
609
+ } catch (error) {
610
+ console.error("Upload error:", error);
611
+ this.showToast('Upload failed', 'Could not process the image. Please try again.', 'error');
612
+ } finally {
613
+ this.setProcessingState(false);
614
+ }
615
+ }
616
+
617
+ setProcessingState(processing) {
618
+ this.isProcessing = processing;
619
+
620
+ if (processing) {
621
+ this.generateBtnText.textContent = 'Processing Menu...';
622
+ this.loadingIcon.classList.remove('hidden');
623
+ this.generateBtn.disabled = true;
624
+ this.processingCard.classList.remove('hidden');
625
+ } else {
626
+ this.generateBtnText.textContent = 'Generate Menu';
627
+ this.loadingIcon.classList.add('hidden');
628
+ this.generateBtn.disabled = false;
629
+ this.processingCard.classList.add('hidden');
630
+ }
631
+ }
632
+
633
+ displayMenuItems() {
634
+ this.menuGrid.innerHTML = '';
635
+
636
+ this.menuItems.forEach((item, index) => {
637
+ const menuItemElement = this.createMenuItemElement(item, index);
638
+ this.menuGrid.appendChild(menuItemElement);
639
+ });
640
+
641
+ this.menuSection.classList.remove('hidden');
642
+ }
643
+
644
+ createMenuItemElement(item, index) {
645
+ const menuItem = document.createElement('div');
646
+ menuItem.className = 'menu-item';
647
+
648
+ menuItem.innerHTML = `
649
+ <div class="menu-item-image-container">
650
+ <img src="${item.img_path}" alt="${item.food}" class="menu-item-image">
651
+ <div class="menu-item-price-badge">$${item.price}</div>
652
+ </div>
653
+ <div class="menu-item-content">
654
+ <h3 class="menu-item-name">${item.food}</h3>
655
+ <p class="menu-item-description">${item.description}</p>
656
+ <div class="menu-item-footer">
657
+ <span class="menu-item-category">${item.category}</span>
658
+ <span class="menu-item-price">$${item.price}</span>
659
+ </div>
660
+ </div>
661
+ `;
662
+
663
+ return menuItem;
664
+ }
665
+
666
+ handleReset() {
667
+ this.selectedImage = null;
668
+ this.previewUrl = null;
669
+ this.menuItems = [];
670
+
671
+ this.fileInput.value = '';
672
+ this.previewSection.classList.add('hidden');
673
+ this.resetBtn.classList.add('hidden');
674
+ this.generateBtn.disabled = true;
675
+ this.menuSection.classList.add('hidden');
676
+ this.processingCard.classList.add('hidden');
677
+ }
678
+
679
+ showToast(title, description, type = 'success') {
680
+ this.toastTitle.textContent = title;
681
+ this.toastDescription.textContent = description;
682
+
683
+ this.toast.classList.remove('error');
684
+ if (type === 'error') {
685
+ this.toast.classList.add('error');
686
+ }
687
+
688
+ this.toast.classList.remove('hidden');
689
+
690
+ setTimeout(() => {
691
+ this.hideToast();
692
+ }, 5000);
693
+ }
694
+
695
+ hideToast() {
696
+ this.toast.classList.add('hidden');
697
+ }
698
+ }
699
+
700
+ document.addEventListener('DOMContentLoaded', () => {
701
+ new MenuUploader();
702
+ });
703
+ </script>
704
+ </body>
705
+ </html>