Spaces:
Build error
Build error
| # -*- coding: utf-8 -*- | |
| """ | |
| SunoDoc HuggingFace Space | |
| pytesseract OCR + Qwen2.5-7B (Hyperbolic) + edge-tts | |
| """ | |
| import gradio as gr | |
| import tempfile | |
| import os | |
| import asyncio | |
| import edge_tts | |
| from PIL import Image | |
| import pytesseract | |
| from huggingface_hub import InferenceClient | |
| HINDI_VOICE = "hi-IN-SwaraNeural" | |
| HF_TOKEN = os.environ.get("sunodoc", None) | |
| VL_MODEL = "Qwen/Qwen2.5-7B-Instruct" | |
| client = InferenceClient(provider="hyperbolic", api_key=HF_TOKEN) | |
| EXPLAIN_PROMPT = """{ocr_text} | |
| Oopar diya gaya document padhkar exactly 3 lines mein simple Hindi mein samjhao: | |
| Line 1: \u092f\u0939 \u0915\u094d\u092f\u093e \u0939\u0948 - (ek vakya mein batao yah document kya hai) | |
| Line 2: \u0906\u092a\u0915\u094b \u0915\u094d\u092f\u093e \u0915\u0930\u0928\u093e \u0939\u0948 - (ek vakya mein action batao) | |
| Line 3: \u0915\u092c \u0924\u0915 \u092f\u093e \u0915\u093f\u0924\u0928\u093e - (deadline ya amount, ya "\u0915\u094b\u0908 \u0938\u092e\u092f \u0938\u0940\u092e\u093e \u0928\u0939\u0940\u0902" agar koi nahi) | |
| Rules: simple Hindi, 20 words se kam per line, repeat mat karo, Line 3 ke baad ruko.""" | |
| FOLLOWUP_PROMPT = """Document summary: {context} | |
| Question: {question} | |
| 2-3 sentences mein simple Hindi mein jawab do.""" | |
| def ocr_image(image): | |
| try: | |
| text = pytesseract.image_to_string(image, lang="hin+eng") | |
| if text.strip(): | |
| return text.strip() | |
| except Exception: | |
| pass | |
| try: | |
| return pytesseract.image_to_string(image, lang="eng").strip() | |
| except Exception as e: | |
| return "" | |
| def text_to_speech(text): | |
| out = tempfile.mktemp(suffix=".mp3") | |
| clean = text.replace("*", "").strip() | |
| async def _speak(): | |
| await edge_tts.Communicate(clean, HINDI_VOICE).save(out) | |
| asyncio.run(_speak()) | |
| return out | |
| def explain_document(image): | |
| if image is None: | |
| return "\u0915\u094b\u0908 \u0926\u0938\u094d\u0924\u093e\u0935\u0947\u091c\u093c \u0928\u0939\u0940\u0902 \u092e\u093f\u0932\u093e\u0964", None | |
| ocr_text = ocr_image(image) | |
| if not ocr_text or len(ocr_text) < 20: | |
| return "\u0926\u0938\u094d\u0924\u093e\u0935\u0947\u091c\u093c \u092a\u0922\u093c \u0928\u0939\u0940\u0902 \u092a\u093e\u090f\u0964 \u0938\u093e\u092b\u093c \u092b\u094b\u091f\u094b \u0932\u0947\u0902\u0964", None | |
| prompt = EXPLAIN_PROMPT.format(ocr_text=ocr_text[:2000]) | |
| response = client.chat.completions.create( | |
| model=VL_MODEL, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=200, temperature=0.1, | |
| ) | |
| hindi_text = response.choices[0].message.content.strip() | |
| return hindi_text, text_to_speech(hindi_text) | |
| def answer_followup(question, context): | |
| if not question or not question.strip(): | |
| return "\u0915\u0943\u092a\u092f\u093e \u092a\u094d\u0930\u0936\u094d\u0928 \u0932\u093f\u0916\u0947\u0902\u0964", None | |
| if not context: | |
| return "\u092a\u0939\u0932\u0947 \u0926\u0938\u094d\u0924\u093e\u0935\u0947\u091c\u093c \u0938\u092e\u091d\u093e\u090f\u0902\u0964", None | |
| prompt = FOLLOWUP_PROMPT.format(context=context, question=question) | |
| response = client.chat.completions.create( | |
| model=VL_MODEL, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=150, temperature=0.1, | |
| ) | |
| answer = response.choices[0].message.content.strip() | |
| return answer, text_to_speech(answer) | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;500;600;700&family=Sora:wght@300;400;500;600;700;800&display=swap'); | |
| *,*::before,*::after{box-sizing:border-box} | |
| body,.gradio-container{background:#080808!important;font-family:'Sora',sans-serif!important;color:#F0EBE3!important;min-height:100vh;overflow-x:hidden} | |
| .hero{position:relative;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 24px 60px;text-align:center;overflow:hidden} | |
| .hero-glow{position:absolute;top:-20%;left:50%;transform:translateX(-50%);width:800px;height:600px;background:radial-gradient(ellipse,rgba(255,90,0,0.18) 0%,transparent 65%);pointer-events:none;animation:pulse 4s ease-in-out infinite} | |
| .hero-glow-2{position:absolute;bottom:10%;right:-10%;width:400px;height:400px;background:radial-gradient(ellipse,rgba(255,50,0,0.08) 0%,transparent 65%);pointer-events:none} | |
| @keyframes pulse{0%,100%{opacity:.8;transform:translateX(-50%) scale(1)}50%{opacity:1;transform:translateX(-50%) scale(1.05)}} | |
| .hero-eyebrow{display:inline-flex;align-items:center;gap:8px;border:1px solid rgba(255,107,0,.35);background:rgba(255,107,0,.08);border-radius:100px;padding:6px 18px;font-size:.7rem;letter-spacing:2.5px;text-transform:uppercase;color:#FF7A2B;margin-bottom:36px;position:relative;z-index:1} | |
| .hero-wordmark{font-size:clamp(5rem,14vw,9rem);font-weight:800;line-height:.9;letter-spacing:-5px;margin-bottom:20px;position:relative;z-index:1} | |
| .hero-wordmark .orange{color:#FF5500}.hero-wordmark .white{color:#F0EBE3} | |
| .hero-tagline{font-family:'Noto Sans Devanagari',sans-serif;font-size:clamp(1.1rem,2.5vw,1.6rem);color:rgba(240,235,227,.5);margin-bottom:32px;position:relative;z-index:1} | |
| .hero-story{max-width:480px;font-size:1rem;line-height:1.75;color:rgba(240,235,227,.45);margin-bottom:16px;position:relative;z-index:1} | |
| .hero-story strong{color:#F0EBE3;font-weight:600} | |
| .hero-stats{display:flex;gap:56px;margin:32px 0 48px;position:relative;z-index:1} | |
| .stat{text-align:center} | |
| .stat-val{font-size:2.2rem;font-weight:800;color:#FF5500;line-height:1} | |
| .stat-key{font-size:.68rem;letter-spacing:2px;text-transform:uppercase;color:rgba(240,235,227,.3);margin-top:6px} | |
| .hero-pills{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;position:relative;z-index:1} | |
| .pill{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:100px;padding:7px 18px;font-size:.76rem;color:rgba(240,235,227,.6)} | |
| .scroll-hint{position:absolute;bottom:32px;left:50%;transform:translateX(-50%);color:rgba(240,235,227,.2);font-size:.7rem;letter-spacing:2px;text-transform:uppercase;animation:bounce 2s ease-in-out infinite;z-index:1} | |
| @keyframes bounce{0%,100%{transform:translateX(-50%) translateY(0)}50%{transform:translateX(-50%) translateY(6px)}} | |
| .line{border:none;border-top:1px solid rgba(255,255,255,.06);margin:0} | |
| .hiw{padding:100px 40px;max-width:960px;margin:0 auto} | |
| .eyebrow{font-size:.68rem;letter-spacing:3px;text-transform:uppercase;color:#FF5500;margin-bottom:14px} | |
| .section-h{font-size:clamp(2rem,4vw,3rem);font-weight:700;letter-spacing:-1.5px;line-height:1.1;margin-bottom:56px} | |
| .section-h .muted{color:rgba(240,235,227,.3);font-weight:300} | |
| .steps{display:grid;grid-template-columns:repeat(3,1fr);gap:20px} | |
| .step{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.09);border-radius:24px;padding:32px 28px;transition:all .3s} | |
| .step:hover{border-color:rgba(255,85,0,.25);transform:translateY(-6px)} | |
| .step-n{font-size:4rem;font-weight:800;color:rgba(255,85,0,.18);line-height:1;margin-bottom:20px} | |
| .step-ico{font-size:2rem;margin-bottom:14px;display:block} | |
| .step-h{font-family:'Noto Sans Devanagari','Sora',sans-serif;font-size:1.1rem;font-weight:600;margin-bottom:10px} | |
| .step-p{font-size:.82rem;color:rgba(240,235,227,.42);line-height:1.65} | |
| .app-zone{padding:80px 40px;max-width:1060px;margin:0 auto} | |
| .card-tag{display:inline-flex;align-items:center;gap:8px;background:rgba(255,85,0,.12);border:1px solid rgba(255,85,0,.22);border-radius:100px;padding:5px 16px;font-size:.68rem;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#FF7A2B;margin-bottom:24px} | |
| textarea,.gr-textbox textarea{background:rgba(255,255,255,.05)!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:16px!important;color:#F0EBE3!important;font-family:'Noto Sans Devanagari','Sora',sans-serif!important;font-size:1.05rem!important;line-height:1.85!important;padding:16px!important} | |
| .privacy{background:rgba(20,80,40,.15);border:1px solid rgba(40,160,80,.18);border-radius:16px;padding:16px 24px;font-size:.78rem;color:rgba(100,210,130,.75);text-align:center;margin-top:32px;line-height:1.6} | |
| .footer{border-top:1px solid rgba(255,255,255,.05);padding:48px 24px;text-align:center;color:rgba(240,235,227,.2);font-size:.75rem;letter-spacing:.5px;line-height:2} | |
| .footer a{color:rgba(255,85,0,.6);text-decoration:none} | |
| @media(max-width:680px){.steps{grid-template-columns:1fr}.hero-stats{gap:28px}.hiw,.app-zone{padding:60px 20px}} | |
| """ | |
| LANDING = ( | |
| '<div class="hero">' | |
| '<div class="hero-glow"></div><div class="hero-glow-2"></div>' | |
| '<div class="hero-eyebrow">✦ Gradio × HuggingFace · Build Small 2026 ✦</div>' | |
| '<div class="hero-wordmark"><span class="orange">Suno</span><span class="white">Doc</span></div>' | |
| '<div class="hero-tagline">सुनो, समञो — आपका दस्तावेज़ हिंदी में बोलेगा</div>' | |
| '<div class="hero-story">Your parent receives a bank letter in English.<br>They can\'t read it. You\'re not there.<br><br>' | |
| '<strong>SunoDoc reads it, explains it in 3 simple Hindi lines,<br>and speaks it aloud.</strong></div>' | |
| '<div class="hero-stats">' | |
| '<div class="stat"><div class="stat-val">7B</div><div class="stat-key">Parameters</div></div>' | |
| '<div class="stat"><div class="stat-val">₹0</div><div class="stat-key">API Cost</div></div>' | |
| '<div class="stat"><div class="stat-val">हिंदी</div><div class="stat-key">Language</div></div>' | |
| '<div class="stat"><div class="stat-val">🔒</div><div class="stat-key">Private</div></div>' | |
| '</div>' | |
| '<div class="hero-pills">' | |
| '<div class="pill">🔌 Off the Grid</div>' | |
| '<div class="pill">🡙 Llama Champion</div>' | |
| '<div class="pill">🎨 Off-Brand</div>' | |
| '<div class="pill">📓 Field Notes</div>' | |
| '</div>' | |
| '<div class="scroll-hint">↓ scroll</div>' | |
| '</div>' | |
| '<hr class="line">' | |
| '<div class="hiw">' | |
| '<div class="eyebrow">How it works</div>' | |
| '<h2 class="section-h">तीन आसान कदम<br><span class="muted">Three simple steps</span></h2>' | |
| '<div class="steps">' | |
| '<div class="step"><div class="step-n">01</div><span class="step-ico">📷</span>' | |
| '<div class="step-h">फोटो लें</div>' | |
| '<div class="step-p">Take a photo of any English document — bank letter, electricity bill, government notice.</div></div>' | |
| '<div class="step"><div class="step-n">02</div><span class="step-ico">🧠</span>' | |
| '<div class="step-h">AI समञेगा</div>' | |
| '<div class="step-p">OCR extracts text, then Qwen2.5-7B explains it in 3 simple Hindi sentences.</div></div>' | |
| '<div class="step"><div class="step-n">03</div><span class="step-ico">🔊</span>' | |
| '<div class="step-h">सुनिए</div>' | |
| '<div class="step-p">A clear Hindi voice reads the explanation aloud. Ask follow-up questions by typing.</div></div>' | |
| '</div></div>' | |
| '<hr class="line">' | |
| '<div class="app-zone">' | |
| '<div class="eyebrow">Try it now</div>' | |
| '<h2 class="section-h">अपना दस्तावेज़ यहाँ लाएं<br><span class="muted">Drop your document below</span></h2>' | |
| '</div>' | |
| ) | |
| with gr.Blocks(css=CSS, title="SunoDoc") as demo: | |
| explanation_state = gr.State("") | |
| gr.HTML(LANDING) | |
| with gr.Group(): | |
| gr.HTML('<div class="card-tag">📄 Step 1 — दस्तावेज़ की फोटो लें</div>') | |
| with gr.Row(equal_height=True): | |
| with gr.Column(scale=1): | |
| doc_image = gr.Image(label="Upload or capture document photo", type="pil", sources=["upload","webcam"], height=320) | |
| with gr.Column(scale=1): | |
| explain_btn = gr.Button("समञाও -- Explain in Hindi", variant="primary", size="lg") | |
| hindi_text = gr.Textbox(label="हिंदी में समञाया गया / Explanation", lines=6, interactive=False, placeholder="यहाँ हिंदी में समञाया जाएगा...") | |
| audio_output = gr.Audio(label="Listen to explanation", type="filepath", interactive=False, autoplay=True) | |
| with gr.Group(): | |
| gr.HTML('<div class="card-tag">💬 Step 2 — प्रश्न पूछें · Optional</div>') | |
| with gr.Row(equal_height=True): | |
| with gr.Column(scale=1): | |
| text_question = gr.Textbox(label="हिंदी में प्रश्न लिखें / Type question", lines=3, placeholder="जैसे: मुञे क्या भरना है?") | |
| ask_btn = gr.Button("Ask in Hindi", size="sm") | |
| with gr.Column(scale=1): | |
| followup_text = gr.Textbox(label="उत्तर / Answer", lines=4, interactive=False) | |
| followup_audio = gr.Audio(label="Hear answer", type="filepath", interactive=False, autoplay=True) | |
| gr.HTML( | |
| '<div style="max-width:1060px;margin:0 auto;padding:0 40px 80px">' | |
| '<div class="privacy">🔒 No data stored · Qwen2.5-7B · pytesseract · edge-tts · Gradio · कोई डेटा सेव नहीं होता</div>' | |
| '<div class="footer">Built with ♡ for parents in Jhansi, UP · ' | |
| '<a href="https://github.com/ashish-doing/sunodoc">GitHub</a> · ' | |
| 'Gradio × HuggingFace Build Small Hackathon 2026 · ' | |
| '<span style="color:rgba(255,85,0,0.5)">Track: Backyard AI 🏠</span></div>' | |
| '</div>' | |
| ) | |
| explain_btn.click(fn=explain_document, inputs=[doc_image], outputs=[hindi_text, audio_output]).then(fn=lambda t: t, inputs=[hindi_text], outputs=[explanation_state]) | |
| ask_btn.click(fn=answer_followup, inputs=[text_question, explanation_state], outputs=[followup_text, followup_audio]) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) |