# -*- 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 = ( '
' '
' '
✦ Gradio × HuggingFace · Build Small 2026 ✦
' '
SunoDoc
' '
सुनो, समञो — आपका दस्तावेज़ हिंदी में बोलेगा
' '
Your parent receives a bank letter in English.
They can\'t read it. You\'re not there.

' 'SunoDoc reads it, explains it in 3 simple Hindi lines,
and speaks it aloud.
' '
' '
7B
Parameters
' '
₹0
API Cost
' '
हिंदी
Language
' '
🔒
Private
' '
' '
' '
🔌 Off the Grid
' '
🡙 Llama Champion
' '
🎨 Off-Brand
' '
📓 Field Notes
' '
' '
↓ scroll
' '
' '
' '
' '
How it works
' '

तीन आसान कदम
Three simple steps

' '
' '
01
📷' '
फोटो लें
' '
Take a photo of any English document — bank letter, electricity bill, government notice.
' '
02
🧠' '
AI समञेगा
' '
OCR extracts text, then Qwen2.5-7B explains it in 3 simple Hindi sentences.
' '
03
🔊' '
सुनिए
' '
A clear Hindi voice reads the explanation aloud. Ask follow-up questions by typing.
' '
' '
' '
' '
Try it now
' '

अपना दस्तावेज़ यहाँ लाएं
Drop your document below

' '
' ) with gr.Blocks(css=CSS, title="SunoDoc") as demo: explanation_state = gr.State("") gr.HTML(LANDING) with gr.Group(): gr.HTML('
📄 Step 1 — दस्तावेज़ की फोटो लें
') 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('
💬 Step 2 — प्रश्न पूछें · Optional
') 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( '
' '
🔒 No data stored · Qwen2.5-7B · pytesseract · edge-tts · Gradio · कोई डेटा सेव नहीं होता
' '' '
' ) 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)