Hypernova823 commited on
Commit
5a2d714
·
verified ·
1 Parent(s): e2f6b5c

Upload src/app.py

Browse files
Files changed (1) hide show
  1. src/app.py +174 -0
src/app.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import torch
3
+ from transformers import TrOCRProcessor, VisionEncoderDecoderModel
4
+ from PIL import Image, UnidentifiedImageError
5
+ import time
6
+ import io
7
+ import platform
8
+ import sys
9
+ import cv2
10
+ import numpy as np
11
+ import os
12
+
13
+ # --- 1. SYSTEM CONFIG ---
14
+ st.set_page_config(page_title="Handwriting Analysis", layout="wide", initial_sidebar_state="collapsed")
15
+
16
+ if 'debug_log' not in st.session_state:
17
+ st.session_state.debug_log = [f"[{time.strftime('%H:%M:%S')}] === SYSTEM BOOT ({platform.system()}) ==="]
18
+ if 'img_buffer' not in st.session_state:
19
+ st.session_state.img_buffer = None
20
+
21
+ def log(msg):
22
+ st.session_state.debug_log.append(f"[{time.strftime('%H:%M:%S')}] {msg}")
23
+
24
+ # CSS: Dark loading spinner and UI
25
+ st.markdown("""
26
+ <style>
27
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;500;700&family=Manrope:wght@300;400;600&display=swap');
28
+ @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap');
29
+
30
+ .stApp { background-color: #0c0e12; color: #f6f6fc; font-family: 'Manrope', sans-serif; }
31
+ .block-container { padding-top: 1rem !important; padding-bottom: 0rem !important; max-width: 95% !important; }
32
+ .hero-title { font-family: 'Space Grotesk'; font-size: clamp(34px, 4vw, 60px); font-weight: 300; line-height: 0.9; margin-bottom: 25px; }
33
+ .hero-accent { color: #8ff5ff; font-weight: 700; font-style: italic; text-shadow: 0 0 20px rgba(143, 245, 255, 0.4); }
34
+ .strike { text-decoration: line-through; color: #46484d; opacity: 0.4; }
35
+
36
+ .uploader-wrapper { position: relative; width: 100%; max-width: 400px; margin: 0 auto; }
37
+ .ingest-card { height: 250px; background: #171a1f; border: 1px solid rgba(143, 245, 255, 0.1); border-radius: 4px; padding: 20px; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; }
38
+ .dashed-border { border: 1px dashed rgba(143, 245, 255, 0.15); border-radius: 2px; padding: 15px; margin-bottom: 12px; }
39
+ .cyan-btn { background-color: #8ff5ff; color: #003f43; font-family: 'Space Grotesk'; font-weight: 700; text-transform: uppercase; padding: 8px 25px; border-radius: 2px; font-size: 10px; letter-spacing: 1px; }
40
+
41
+ div[data-testid="stFileUploader"] { margin-top: -250px !important; height: 250px !important; opacity: 0.01 !important; z-index: 99 !important; cursor: pointer !important; }
42
+ div[data-testid="stFileUploader"] section { height: 100% !important; padding: 0 !important; }
43
+
44
+ .stat-card { background: #000; padding: 15px; border-radius: 2px; text-align: center; }
45
+ .stat-val { color: #8ff5ff; font-size: 24px; font-weight: 700; font-family: 'Space Grotesk'; }
46
+ .stat-lbl { font-size: 9px; color: #46484d; text-transform: uppercase; letter-spacing: 1.5px; }
47
+ .output-box { border-left: 3px solid #8ff5ff; background: #171a1f; padding: 20px; font-family: 'Space Grotesk'; font-size: clamp(16px, 1.5vw, 20px); color: #f6f6fc; margin-top: 15px; line-height: 1.5; min-height: 100px; white-space: pre-wrap; }
48
+
49
+ .stButton>button { background-color: transparent !important; border: 1px solid #8ff5ff !important; color: #8ff5ff !important; width: 100%; border-radius: 2px; font-family: 'Space Grotesk'; text-transform: uppercase; font-size: 10px; margin-top: 10px; }
50
+
51
+ /* Dark Spinner fix */
52
+ [data-testid="stStatus"] { background-color: transparent !important; }
53
+ [data-testid="stSpinner"] { background-color: #171a1f !important; color: #8ff5ff !important; border: 1px solid rgba(143, 245, 255, 0.2); padding: 15px; border-radius: 4px; }
54
+ [data-testid="stSpinner"] > div { border-top-color: #8ff5ff !important; }
55
+
56
+ [data-testid="stHeader"], footer, .stFileUploader label { visibility: hidden; display: none; }
57
+ </style>
58
+ """, unsafe_allow_html=True)
59
+
60
+ # --- 2. ENGINE ---
61
+ @st.cache_resource
62
+ def load_engine():
63
+ local_path = "./final_handwriting_model"
64
+ cloud_path = "Hypernova823/ReadAI"
65
+ path = local_path if os.path.exists(local_path) else cloud_path
66
+ log(f"ENGINE: Loading from {path}")
67
+
68
+ device = "cuda" if torch.cuda.is_available() else "cpu"
69
+ try:
70
+ proc = TrOCRProcessor.from_pretrained(path)
71
+ model = VisionEncoderDecoderModel.from_pretrained(path).to(device)
72
+ return proc, model, device
73
+ except Exception as e:
74
+ log(f"CRASH: {e}")
75
+ return None, None, None
76
+
77
+ def segment_handwriting(pil_image):
78
+ img_cv = np.array(pil_image.convert('RGB'))[:, :, ::-1].copy()
79
+ gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
80
+ blur = cv2.GaussianBlur(gray, (7,7), 0)
81
+ thresh = cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 21, 10)
82
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (60, 5))
83
+ dilated = cv2.dilate(thresh, kernel, iterations=1)
84
+ contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
85
+ boxes = [cv2.boundingRect(c) for c in contours]
86
+ boxes = [b for b in boxes if b[2] > 50 and b[3] > 15]
87
+ boxes.sort(key=lambda b: b[1])
88
+
89
+ lines = []
90
+ for x, y, w, h in boxes:
91
+ pad_x, pad_y = 15, 15
92
+ x1, y1 = max(0, x - pad_x), max(0, y - pad_y)
93
+ x2, y2 = min(img_cv.shape[1], x + w + pad_x), min(img_cv.shape[0], y + h + pad_y)
94
+ lines.append(pil_image.crop((x1, y1, x2, y2)))
95
+ return lines if lines else [pil_image]
96
+
97
+ def render_tts_button(text, voice_choice, speed, volume):
98
+ safe_text = text.replace("'", "\\'").replace('\n', ' ')
99
+ html_code = f"""
100
+ <div style="padding: 2px;">
101
+ <button onclick="playTTS()" style="box-sizing: border-box; background-color: transparent; border: 1px solid #8ff5ff; color: #8ff5ff; width: 100%; border-radius: 2px; font-family: 'Space Grotesk', sans-serif; text-transform: uppercase; font-size: 10px; padding: 12px 0; cursor: pointer; transition: 0.3s;" onmouseover="this.style.backgroundColor='#8ff5ff'; this.style.color='#000';" onmouseout="this.style.backgroundColor='transparent'; this.style.color='#8ff5ff';"><span style="vertical-align: middle;">Generate Audio Readout</span></button>
102
+ </div>
103
+ <script>
104
+ var systemVoices = [];
105
+ function loadVoices() {{ systemVoices = window.speechSynthesis.getVoices(); }}
106
+ window.speechSynthesis.onvoiceschanged = loadVoices; loadVoices();
107
+ function playTTS() {{
108
+ window.speechSynthesis.cancel();
109
+ var msg = new SpeechSynthesisUtterance('{safe_text}');
110
+ msg.rate = {speed}; msg.volume = {volume};
111
+ var targetVoice = '{voice_choice}';
112
+ var selectedVoice = null;
113
+ if (targetVoice === 'Aria (Neural)') {{ selectedVoice = systemVoices.find(v => v.name.toLowerCase().includes('female') || v.name.toLowerCase().includes('zira') || v.name.toLowerCase().includes('aria') || v.name.toLowerCase().includes('samantha')); }}
114
+ else {{ selectedVoice = systemVoices.find(v => v.name.toLowerCase().includes('male') || v.name.toLowerCase().includes('david') || v.name.toLowerCase().includes('julian') || v.name.toLowerCase().includes('mark')); }}
115
+ if (selectedVoice) {{ msg.voice = selectedVoice; }}
116
+ window.speechSynthesis.speak(msg);
117
+ }}
118
+ </script>
119
+ """
120
+ st.components.v1.html(html_code, height=70)
121
+
122
+ # --- 3. UI ---
123
+ st.markdown('<div class="hero-title"><div class="strike">Handwronging</div><div class="hero-accent">Handwriting</div></div>', unsafe_allow_html=True)
124
+ left, right = st.columns([1, 1], gap="large")
125
+
126
+ with left:
127
+ if st.session_state.img_buffer is None:
128
+ st.markdown('<div class="uploader-wrapper"><div class="ingest-card"><div class="dashed-border"><span class="material-symbols-outlined" style="font-size:28px; color:#8ff5ff;">add_a_photo</span></div><div class="cyan-btn">Browse Local Storage</div></div>', unsafe_allow_html=True)
129
+ # Bypassing HF File Drop issues with explicit rerun
130
+ img_file = st.file_uploader(" ", type=["jpg", "png", "jpeg", "jfif", "webp"], label_visibility="collapsed")
131
+ st.markdown('</div>', unsafe_allow_html=True)
132
+ if img_file:
133
+ st.session_state.img_buffer = img_file.getvalue()
134
+ st.rerun()
135
+ else:
136
+ st.image(Image.open(io.BytesIO(st.session_state.img_buffer)), width=300, caption="Source Input")
137
+ if st.button("UPLOAD ANOTHER PHOTO"):
138
+ st.session_state.img_buffer = None
139
+ st.rerun()
140
+
141
+ with right:
142
+ if st.session_state.img_buffer is not None:
143
+ proc, model, dev = load_engine()
144
+ if model:
145
+ stats_ph = st.empty()
146
+ text_ph = st.empty()
147
+
148
+ line_images = segment_handwriting(Image.open(io.BytesIO(st.session_state.img_buffer)).convert("RGB"))
149
+ full_text = ""
150
+ t_start = time.perf_counter()
151
+
152
+ with st.spinner(f"Decoding {len(line_images)} lines..."):
153
+ for idx, line_img in enumerate(line_images):
154
+ pix = proc(line_img, return_tensors="pt").pixel_values.to(dev)
155
+ # REVERTED: Original generate logic
156
+ out = model.generate(pix)
157
+ pred = proc.batch_decode(out, skip_special_tokens=True)[0]
158
+ full_text += pred + " "
159
+ text_ph.markdown(f'<div class="output-box">{full_text}</div>', unsafe_allow_html=True)
160
+
161
+ latency = time.perf_counter() - t_start
162
+ with stats_ph.container():
163
+ s1, s2 = st.columns(2)
164
+ with s1: st.markdown(f'<div class="stat-card"><div class="stat-val">SYNC</div><div class="stat-lbl">Neural Status</div></div>', unsafe_allow_html=True)
165
+ with s2: st.markdown(f'<div class="stat-card"><div class="stat-val">{latency:.2f}s</div><div class="stat-lbl">Latency</div></div>', unsafe_allow_html=True)
166
+
167
+ c1, c2, c3 = st.columns(3)
168
+ with c1: voice = st.selectbox("Voice", ["Aria (Neural)", "Julian (Natural)"], label_visibility="collapsed")
169
+ with c2: speed = st.slider("Rate", 0.5, 2.0, 1.0, 0.1)
170
+ with c3: vol = st.slider("Vol", 0.1, 1.0, 1.0, 0.1)
171
+ render_tts_button(full_text, voice, speed, vol)
172
+
173
+ log_html = f"<div class='debug-terminal'>{''.join([f'<div>{e}</div>' for e in st.session_state.debug_log])}</div>"
174
+ st.markdown(log_html, unsafe_allow_html=True)