Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Type.ai - Handwriting to Font</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg-dark: #0f0f1a; | |
| --bg-card: #1a1a2e; | |
| --bg-hover: #252542; | |
| --accent: #7c3aed; | |
| --accent-light: #a78bfa; | |
| --accent-glow: rgba(124, 58, 237, 0.3); | |
| --text-primary: #ffffff; | |
| --text-secondary: #a1a1aa; | |
| --success: #10b981; | |
| --error: #ef4444; | |
| --border: #2d2d4a; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background: var(--bg-dark); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| } | |
| /* Header */ | |
| header { | |
| text-align: center; | |
| padding: 3rem 0; | |
| } | |
| header h1 { | |
| font-size: 3rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, var(--accent-light) 0%, var(--accent) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 0.5rem; | |
| } | |
| header p { | |
| color: var(--text-secondary); | |
| font-size: 1.1rem; | |
| } | |
| /* Upload Zone */ | |
| .upload-zone { | |
| background: var(--bg-card); | |
| border: 2px dashed var(--border); | |
| border-radius: 16px; | |
| padding: 4rem 2rem; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| margin-bottom: 2rem; | |
| } | |
| .upload-zone:hover, .upload-zone.dragover { | |
| border-color: var(--accent); | |
| background: var(--bg-hover); | |
| box-shadow: 0 0 30px var(--accent-glow); | |
| } | |
| .upload-zone svg { | |
| width: 64px; | |
| height: 64px; | |
| margin-bottom: 1rem; | |
| color: var(--accent-light); | |
| } | |
| .upload-zone h3 { | |
| font-size: 1.3rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .upload-zone p { | |
| color: var(--text-secondary); | |
| font-size: 0.95rem; | |
| } | |
| #file-input { | |
| display: none; | |
| } | |
| /* Font Name Input */ | |
| .font-name-section { | |
| background: var(--bg-card); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .font-name-section label { | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| } | |
| .font-name-section input { | |
| width: 100%; | |
| padding: 0.8rem 1rem; | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-size: 1rem; | |
| outline: none; | |
| transition: border-color 0.3s; | |
| } | |
| .font-name-section input:focus { | |
| border-color: var(--accent); | |
| } | |
| /* Processing Status */ | |
| .status-card { | |
| background: var(--bg-card); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| display: none; | |
| } | |
| .status-card.visible { | |
| display: block; | |
| } | |
| .status-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| } | |
| .status-icon { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| } | |
| .status-icon.processing { | |
| background: var(--accent-glow); | |
| animation: pulse 2s infinite; | |
| } | |
| .status-icon.completed { | |
| background: rgba(16, 185, 129, 0.2); | |
| } | |
| .status-icon.failed { | |
| background: rgba(239, 68, 68, 0.2); | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); opacity: 1; } | |
| 50% { transform: scale(1.05); opacity: 0.8; } | |
| } | |
| .progress-bar { | |
| height: 8px; | |
| background: var(--bg-dark); | |
| border-radius: 4px; | |
| overflow: hidden; | |
| margin: 1rem 0; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent) 0%, var(--accent-light) 100%); | |
| border-radius: 4px; | |
| transition: width 0.5s ease; | |
| } | |
| /* Preview Section */ | |
| .preview-section { | |
| background: var(--bg-card); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| margin-bottom: 2rem; | |
| display: none; | |
| } | |
| .preview-section.visible { | |
| display: block; | |
| } | |
| .preview-section h3 { | |
| margin-bottom: 1rem; | |
| } | |
| .preview-image { | |
| width: 100%; | |
| max-height: 400px; | |
| object-fit: contain; | |
| border-radius: 8px; | |
| background: var(--bg-dark); | |
| } | |
| .characters-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| } | |
| .char-badge { | |
| padding: 0.3rem 0.8rem; | |
| background: var(--bg-hover); | |
| border-radius: 6px; | |
| font-family: monospace; | |
| font-size: 1rem; | |
| } | |
| /* Download Button */ | |
| .download-btn { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 1rem 2rem; | |
| background: linear-gradient(135deg, var(--accent) 0%, #6d28d9 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-decoration: none; | |
| } | |
| .download-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 30px var(--accent-glow); | |
| } | |
| .download-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| /* Test Font Section */ | |
| .test-font-section { | |
| background: var(--bg-card); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| margin-top: 2rem; | |
| display: none; | |
| } | |
| .test-font-section.visible { | |
| display: block; | |
| } | |
| .test-font-section h3 { | |
| margin-bottom: 1rem; | |
| } | |
| .font-preview { | |
| padding: 1.5rem; | |
| background: white; | |
| color: black; | |
| border-radius: 8px; | |
| font-size: 1.5rem; | |
| line-height: 1.8; | |
| } | |
| /* Footer */ | |
| footer { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>Type.ai</h1> | |
| <p>Convert your handwriting into a custom font</p> | |
| </header> | |
| <!-- Upload Zone --> | |
| <div class="upload-zone" id="upload-zone"> | |
| <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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"/> | |
| </svg> | |
| <h3>Drop your handwriting image here</h3> | |
| <p>or click to browse • JPG, PNG, BMP supported</p> | |
| <input type="file" id="file-input" accept=".jpg,.jpeg,.png,.bmp,.tiff"> | |
| </div> | |
| <!-- Font Name --> | |
| <div class="font-name-section"> | |
| <label for="font-name">Font Name</label> | |
| <input type="text" id="font-name" value="MyHandwriting" placeholder="Enter font name"> | |
| </div> | |
| <!-- Status Card --> | |
| <div class="status-card" id="status-card"> | |
| <div class="status-header"> | |
| <div class="status-icon processing" id="status-icon">⏳</div> | |
| <div> | |
| <h3 id="status-title">Processing...</h3> | |
| <p id="status-message" style="color: var(--text-secondary)">Analyzing your handwriting</p> | |
| </div> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progress-fill" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Preview Section --> | |
| <div class="preview-section" id="preview-section"> | |
| <h3>Detected Characters</h3> | |
| <img src="" alt="Segmentation preview" class="preview-image" id="preview-image"> | |
| <div class="characters-grid" id="characters-grid"></div> | |
| </div> | |
| <!-- Download Section --> | |
| <div style="text-align: center; margin: 2rem 0;"> | |
| <a href="#" class="download-btn" id="download-btn" style="display: none;"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/> | |
| </svg> | |
| Download Font | |
| </a> | |
| </div> | |
| <!-- Test Font Section --> | |
| <div class="test-font-section" id="test-font-section"> | |
| <h3>Preview Your Font</h3> | |
| <div class="font-preview" id="font-preview"> | |
| The quick brown fox jumps over the lazy dog. | |
| ABCDEFGHIJKLMNOPQRSTUVWXYZ | |
| abcdefghijklmnopqrstuvwxyz | |
| 0123456789 | |
| </div> | |
| </div> | |
| <footer> | |
| <p>Type.ai • AI-Powered Handwriting to Font Converter</p> | |
| </footer> | |
| </div> | |
| <script> | |
| const uploadZone = document.getElementById('upload-zone'); | |
| const fileInput = document.getElementById('file-input'); | |
| const fontNameInput = document.getElementById('font-name'); | |
| const statusCard = document.getElementById('status-card'); | |
| const statusIcon = document.getElementById('status-icon'); | |
| const statusTitle = document.getElementById('status-title'); | |
| const statusMessage = document.getElementById('status-message'); | |
| const progressFill = document.getElementById('progress-fill'); | |
| const previewSection = document.getElementById('preview-section'); | |
| const previewImage = document.getElementById('preview-image'); | |
| const charactersGrid = document.getElementById('characters-grid'); | |
| const downloadBtn = document.getElementById('download-btn'); | |
| const testFontSection = document.getElementById('test-font-section'); | |
| const fontPreview = document.getElementById('font-preview'); | |
| let currentJobId = null; | |
| let pollInterval = null; | |
| // Drag and drop | |
| uploadZone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.add('dragover'); | |
| }); | |
| uploadZone.addEventListener('dragleave', () => { | |
| uploadZone.classList.remove('dragover'); | |
| }); | |
| uploadZone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| uploadZone.classList.remove('dragover'); | |
| const file = e.dataTransfer.files[0]; | |
| if (file) uploadFile(file); | |
| }); | |
| uploadZone.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) uploadFile(file); | |
| }); | |
| async function uploadFile(file) { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| formData.append('font_name', fontNameInput.value || 'MyHandwriting'); | |
| // Show status | |
| statusCard.classList.add('visible'); | |
| statusIcon.className = 'status-icon processing'; | |
| statusIcon.textContent = '⏳'; | |
| statusTitle.textContent = 'Uploading...'; | |
| statusMessage.textContent = 'Sending your handwriting to the AI'; | |
| progressFill.style.width = '5%'; | |
| try { | |
| const response = await fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| currentJobId = data.job_id; | |
| pollStatus(); | |
| } else { | |
| showError(data.detail || 'Upload failed'); | |
| } | |
| } catch (error) { | |
| showError('Connection error: ' + error.message); | |
| } | |
| } | |
| function pollStatus() { | |
| if (pollInterval) clearInterval(pollInterval); | |
| pollInterval = setInterval(async () => { | |
| try { | |
| const response = await fetch(`/api/status/${currentJobId}`); | |
| const data = await response.json(); | |
| updateStatus(data); | |
| if (data.status === 'completed' || data.status === 'failed') { | |
| clearInterval(pollInterval); | |
| } | |
| } catch (error) { | |
| console.error('Poll error:', error); | |
| } | |
| }, 1000); | |
| } | |
| function updateStatus(data) { | |
| progressFill.style.width = data.progress + '%'; | |
| statusMessage.textContent = data.message; | |
| if (data.status === 'processing') { | |
| statusIcon.className = 'status-icon processing'; | |
| statusIcon.textContent = '⏳'; | |
| statusTitle.textContent = 'Processing...'; | |
| } else if (data.status === 'completed') { | |
| statusIcon.className = 'status-icon completed'; | |
| statusIcon.textContent = '✓'; | |
| statusTitle.textContent = 'Complete!'; | |
| // Show preview | |
| if (data.preview_url) { | |
| previewSection.classList.add('visible'); | |
| previewImage.src = data.preview_url; | |
| } | |
| // Show characters | |
| if (data.characters && data.characters.length > 0) { | |
| charactersGrid.innerHTML = data.characters | |
| .map(c => `<span class="char-badge">${c}</span>`) | |
| .join(''); | |
| } | |
| // Show download button | |
| downloadBtn.style.display = 'inline-flex'; | |
| downloadBtn.href = data.font_url; | |
| // Load and preview font | |
| loadFont(data.font_url, fontNameInput.value); | |
| } else if (data.status === 'failed') { | |
| showError(data.message); | |
| } | |
| } | |
| function showError(message) { | |
| statusIcon.className = 'status-icon failed'; | |
| statusIcon.textContent = '✗'; | |
| statusTitle.textContent = 'Error'; | |
| statusMessage.textContent = message; | |
| progressFill.style.width = '0%'; | |
| } | |
| async function loadFont(url, fontName) { | |
| try { | |
| const font = new FontFace(fontName, `url(${url})`); | |
| await font.load(); | |
| document.fonts.add(font); | |
| fontPreview.style.fontFamily = `"${fontName}", sans-serif`; | |
| testFontSection.classList.add('visible'); | |
| } catch (error) { | |
| console.error('Font load error:', error); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |