Jack1808's picture
Upload folder using huggingface_hub
039472f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Menu Extractor & Food Image Generator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #8B4513 0%, #D2691E 50%, #CD853F 100%);
min-height: 100vh;
color: #2F1B14;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 40px;
background: rgba(255, 255, 255, 0.95);
padding: 30px;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.header h1 {
font-size: 2.5em;
color: #8B4513;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.header p {
font-size: 1.1em;
color: #666;
}
.upload-section {
background: rgba(255, 255, 255, 0.95);
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.upload-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.upload-option {
border: 2px dashed #D2691E;
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: #FFF8DC;
}
.upload-option:hover {
border-color: #8B4513;
background: #F5DEB3;
transform: translateY(-2px);
}
.upload-option.active {
border-color: #8B4513;
background: #F5DEB3;
}
.upload-option i {
font-size: 3em;
color: #D2691E;
margin-bottom: 10px;
display: block;
}
.upload-option h3 {
color: #8B4513;
margin-bottom: 10px;
}
.btn {
background: linear-gradient(45deg, #D2691E, #CD853F);
color: white;
border: none;
padding: 12px 25px;
border-radius: 25px;
cursor: pointer;
font-size: 1em;
font-weight: bold;
transition: all 0.3s ease;
margin: 5px;
}
.btn:hover {
background: linear-gradient(45deg, #8B4513, #A0522D);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
#fileInput {
display: none;
}
#cameraSection {
display: none;
text-align: center;
margin-top: 20px;
}
#videoElement {
max-width: 100%;
height: auto;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
#canvas {
display: none;
}
.preview-section {
margin-top: 20px;
text-align: center;
}
#imagePreview {
max-width: 100%;
max-height: 400px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #D2691E;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.results-section {
display: none;
background: rgba(255, 255, 255, 0.95);
padding: 30px;
border-radius: 15px;
margin-top: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.view-controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
justify-content: center;
}
.view-btn {
background: linear-gradient(45deg, #4CAF50, #45a049);
padding: 10px 20px;
border-radius: 20px;
}
.view-btn:hover {
background: linear-gradient(45deg, #45a049, #3e8e41);
}
.view-btn.active {
background: linear-gradient(45deg, #2196F3, #1976D2);
}
.frontend-preview {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
border: 1px solid #ddd;
margin-bottom: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.restaurant-header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #8B4513, #D2691E);
color: white;
border-radius: 15px;
}
.restaurant-header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.menu-cards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 25px;
padding: 20px 0;
}
.menu-card {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid #e0e0e0;
}
.menu-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
}
.card-image {
width: 100%;
height: 200px;
background: linear-gradient(45deg, #f0f0f0, #e0e0e0);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-image .loading-placeholder {
display: flex;
flex-direction: column;
align-items: center;
color: #666;
}
.card-image .loading-placeholder i {
font-size: 2em;
margin-bottom: 10px;
}
.card-content {
padding: 20px;
}
.card-title {
font-size: 1.4em;
font-weight: bold;
color: #8B4513;
margin-bottom: 8px;
}
.card-description {
color: #666;
line-height: 1.5;
margin-bottom: 15px;
font-size: 0.95em;
}
.card-price {
font-size: 1.3em;
font-weight: bold;
color: #D2691E;
text-align: right;
}
.category-section {
margin-bottom: 40px;
}
.category-title {
font-size: 2em;
color: #8B4513;
text-align: center;
margin-bottom: 25px;
padding-bottom: 10px;
border-bottom: 3px solid #D2691E;
position: relative;
}
.category-title::after {
content: '';
position: absolute;
bottom: -3px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: #8B4513;
}
.code-preview {
display: none;
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 10px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
}
.copy-btn {
background: linear-gradient(45deg, #6c757d, #495057);
padding: 8px 15px;
font-size: 0.9em;
margin-left: 10px;
}
.copy-btn:hover {
background: linear-gradient(45deg, #495057, #343a40);
}
.error {
background: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #c62828;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2em;
}
.upload-options {
grid-template-columns: 1fr;
}
.menu-cards-container {
grid-template-columns: 1fr;
}
.view-controls {
flex-direction: column;
align-items: center;
}
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-utensils"></i> Menu Extractor & Food Generator</h1>
<p>Upload your restaurant menu and generate beautiful food images!</p>
</div>
<div class="upload-section">
<h2 style="margin-bottom: 20px; color: #8B4513;">
<i class="fas fa-cloud-upload-alt"></i> Choose Your Image Source
</h2>
<div class="upload-options">
<div class="upload-option" onclick="selectFileUpload()">
<i class="fas fa-file-image"></i>
<h3>Upload Image</h3>
<p>Select a menu image from your device</p>
</div>
<div class="upload-option" onclick="selectCameraCapture()">
<i class="fas fa-camera"></i>
<h3>Take Photo</h3>
<p>Use your camera to capture a menu</p>
</div>
</div>
<input type="file" id="fileInput" accept="image/*" onchange="handleFileSelect(event)">
<div id="cameraSection">
<video id="videoElement" autoplay></video>
<br>
<button class="btn" onclick="capturePhoto()">
<i class="fas fa-camera"></i> Capture Photo
</button>
<button class="btn" onclick="stopCamera()">
<i class="fas fa-times"></i> Cancel
</button>
</div>
<canvas id="canvas"></canvas>
<div class="preview-section">
<img id="imagePreview" style="display: none;" alt="Preview">
<div style="margin-top: 15px;">
<button class="btn" id="extractBtn" onclick="extractMenu()" disabled>
<i class="fas fa-magic"></i> Extract Menu Items
</button>
</div>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Processing your menu image and generating food images...</p>
</div>
</div>
<div class="results-section" id="resultsSection">
<div class="view-controls">
<button class="btn view-btn active" id="previewBtn" onclick="showPreview()">
<i class="fas fa-eye"></i> Frontend Preview
</button>
<button class="btn view-btn" id="codeBtn" onclick="showCode()">
<i class="fas fa-code"></i> Show Code
</button>
<button class="btn copy-btn" onclick="copyCode()">
<i class="fas fa-copy"></i> Copy Code
</button>
</div>
<div id="frontendPreview" class="frontend-preview">
<!-- Frontend preview will be generated here -->
</div>
<div id="codePreview" class="code-preview">
<!-- Code will be displayed here -->
</div>
</div>
</div>
<script>
let currentImageData = null;
let stream = null;
let extractedMenu = null;
let generatedCode = '';
function selectFileUpload() {
document.getElementById('fileInput').click();
document.getElementById('cameraSection').style.display = 'none';
updateUploadOptions('file');
}
function selectCameraCapture() {
startCamera();
updateUploadOptions('camera');
}
function updateUploadOptions(selected) {
const options = document.querySelectorAll('.upload-option');
options.forEach(option => option.classList.remove('active'));
if (selected === 'file') {
options[0].classList.add('active');
} else if (selected === 'camera') {
options[1].classList.add('active');
}
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
displayImagePreview(e.target.result);
currentImageData = file;
};
reader.readAsDataURL(file);
}
}
async function startCamera() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
});
const video = document.getElementById('videoElement');
video.srcObject = stream;
document.getElementById('cameraSection').style.display = 'block';
} catch (err) {
console.error('Error accessing camera:', err);
alert('Could not access camera. Please check permissions or use file upload.');
}
}
function capturePhoto() {
const video = document.getElementById('videoElement');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0);
const imageDataUrl = canvas.toDataURL('image/jpeg');
displayImagePreview(imageDataUrl);
currentImageData = imageDataUrl;
stopCamera();
}
function stopCamera() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
document.getElementById('cameraSection').style.display = 'none';
}
function displayImagePreview(src) {
const preview = document.getElementById('imagePreview');
preview.src = src;
preview.style.display = 'block';
document.getElementById('extractBtn').disabled = false;
}
async function extractMenu() {
const loading = document.getElementById('loading');
const resultsSection = document.getElementById('resultsSection');
loading.style.display = 'block';
resultsSection.style.display = 'none';
try {
let response;
if (typeof currentImageData === 'string') {
response = await fetch('/extract_menu', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
image_data: currentImageData
})
});
} else {
const formData = new FormData();
formData.append('image', currentImageData);
response = await fetch('/extract_menu', {
method: 'POST',
body: formData
});
}
const result = await response.json();
if (result.success) {
extractedMenu = result.menu;
await generateFrontendPreview(result.menu);
generateCode(result.menu);
resultsSection.style.display = 'block';
} else {
showError(result.error || 'Failed to extract menu');
}
} catch (error) {
showError('Error processing image: ' + error.message);
} finally {
loading.style.display = 'none';
}
}
// async function generateFrontendPreview(menu) {
// const previewDiv = document.getElementById('frontendPreview');
// if (!menu || menu.length === 0) {
// previewDiv.innerHTML = '<p>No menu items found in the image.</p>';
// return;
// }
// let html = `
// <div class="restaurant-header">
// <h1><i class="fas fa-utensils"></i> Our Menu</h1>
// <p>Delicious food crafted with love</p>
// </div>
// `;
// for (const category of menu) {
// html += `
// <div class="category-section">
// <h2 class="category-title">${category.category}</h2>
// <div class="menu-cards-container">
// `;
// for (const item of category.items) {
// const cardId = `card-${item.name.replace(/\s+/g, '-').toLowerCase()}`;
// html += `
// <div class="menu-card">
// <div class="card-image" id="${cardId}-image">
// <div class="loading-placeholder">
// <i class="fas fa-image"></i>
// <span>Generating image...</span>
// </div>
// </div>
// <div class="card-content">
// <div class="card-title">${item.name}</div>
// <div class="card-description">${item.description || 'Delicious and carefully prepared dish'}</div>
// <div class="card-price">${item.price && item.price.trim() !== '' ? `₹${item.price}` : 'Price on request'}</div>
// </div>
// </div>
// `;
// }
// html += `
// </div>
// </div>
// `;
// }
// previewDiv.innerHTML = html;
// // Generate images for all items automatically
// for (const category of menu) {
// for (const item of category.items) {
// await generateFoodImageAuto(item.name, item.description || '');
// }
// }
// }
async function generateFrontendPreview(menu) {
const previewDiv = document.getElementById('frontendPreview');
if (!menu || menu.length === 0) {
previewDiv.innerHTML = '<p>No menu items found in the image.</p>';
return;
}
let html = `
<div class="restaurant-header">
<h1><i class="fas fa-utensils"></i> Our Menu</h1>
<p>Delicious food crafted with love</p>
</div>
`;
for (const category of menu) {
html += `
<div class="category-section">
<h2 class="category-title">${category.category}</h2>
<div class="menu-cards-container">
`;
for (const item of category.items) {
const cardId = `card-${item.name.replace(/\s+/g, '-').toLowerCase()}`;
html += `
<div class="menu-card">
<div class="card-image" id="${cardId}-image">
<div class="loading-placeholder">
<i class="fas fa-image"></i>
<span>Generating image...</span>
</div>
</div>
<div class="card-content">
<div class="card-title">${item.name}</div>
<div class="card-description">${item.description || 'Delicious and carefully prepared dish'}</div>
<div class="card-price">${item.price && item.price.trim() !== '' ? `₹${item.price}` : 'Price on request'}</div>
</div>
</div>
`;
}
html += `
</div>
</div>
`;
}
previewDiv.innerHTML = html;
// ✅ FIXED: Generate images one-by-one with `await`
for (const category of menu) {
for (const item of category.items) {
await generateFoodImageAuto(item.name, item.description || '');
}
}
}
async function generateFoodImageAuto(name, description) {
const cardId = `card-${name.replace(/\s+/g, '-').toLowerCase()}`;
const imageContainer = document.getElementById(`${cardId}-image`);
try {
const response = await fetch('/generate_image', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
description: description
})
});
const result = await response.json();
if (result.success) {
imageContainer.innerHTML = `<img src="${result.image}" alt="${name}">`;
} else {
imageContainer.innerHTML = `
<div class="loading-placeholder">
<i class="fas fa-exclamation-triangle" style="color: #ff6b6b;"></i>
<span>Image generation failed</span>
</div>
`;
}
} catch (error) {
imageContainer.innerHTML = `
<div class="loading-placeholder">
<i class="fas fa-exclamation-triangle" style="color: #ff6b6b;"></i>
<span>Error loading image</span>
</div>
`;
}
}
function generateCode(menu) {
generatedCode = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Restaurant Menu</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
}
.restaurant-header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #8B4513, #D2691E);
color: white;
border-radius: 15px;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.restaurant-header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.category-section {
margin-bottom: 40px;
}
.category-title {
font-size: 2em;
color: #8B4513;
text-align: center;
margin-bottom: 25px;
padding-bottom: 10px;
border-bottom: 3px solid #D2691E;
position: relative;
}
.category-title::after {
content: '';
position: absolute;
bottom: -3px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: #8B4513;
}
.menu-cards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 25px;
padding: 20px 0;
}
.menu-card {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid #e0e0e0;
}
.menu-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
}
.card-image {
width: 100%;
height: 200px;
background: linear-gradient(45deg, #f0f0f0, #e0e0e0);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-content {
padding: 20px;
}
.card-title {
font-size: 1.4em;
font-weight: bold;
color: #8B4513;
margin-bottom: 8px;
}
.card-description {
color: #666;
line-height: 1.5;
margin-bottom: 15px;
font-size: 0.95em;
}
.card-price {
font-size: 1.3em;
font-weight: bold;
color: #D2691E;
text-align: right;
}
@media (max-width: 768px) {
.menu-cards-container {
grid-template-columns: 1fr;
}
.restaurant-header h1 {
font-size: 2em;
}
}
</style>
</head>
<body>
<div class="restaurant-header">
<h1><i class="fas fa-utensils"></i> Our Menu</h1>
<p>Delicious food crafted with love</p>
</div>
${menu.map(category => `
<div class="category-section">
<h2 class="category-title">${category.category}</h2>
<div class="menu-cards-container">
${category.items.map(item => `
<div class="menu-card">
<div class="card-image">
<img src="https://via.placeholder.com/350x200/8B4513/FFFFFF?text=${encodeURIComponent(item.name)}" alt="${item.name}">
</div>
<div class="card-content">
<div class="card-title">${item.name}</div>
<div class="card-description">${item.description || 'Delicious and carefully prepared dish'}</div>
<div class="card-price">${item.price && item.price.trim() !== '' ? `₹${item.price}` : 'Price on request'}</div>
</div>
</div>`).join('')}
</div>
</div>`).join('')}
</body>
</html>`;
document.getElementById('codePreview').textContent = generatedCode;
}
function showPreview() {
document.getElementById('frontendPreview').style.display = 'block';
document.getElementById('codePreview').style.display = 'none';
document.getElementById('previewBtn').classList.add('active');
document.getElementById('codeBtn').classList.remove('active');
}
function showCode() {
document.getElementById('frontendPreview').style.display = 'none';
document.getElementById('codePreview').style.display = 'block';
document.getElementById('previewBtn').classList.remove('active');
document.getElementById('codeBtn').classList.add('active');
}
function copyCode() {
if (generatedCode) {
navigator.clipboard.writeText(generatedCode).then(() => {
alert('Code copied to clipboard!');
}).catch(() => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = generatedCode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
alert('Code copied to clipboard!');
});
}
}
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.innerHTML = `<i class="fas fa-exclamation-triangle"></i> ${message}`;
const uploadSection = document.querySelector('.upload-section');
uploadSection.appendChild(errorDiv);
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
// Clean up camera on page unload
window.addEventListener('beforeunload', function() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
});
</script>
</body>
</html>