3morrrrr commited on
Commit
ae17bcc
·
verified ·
1 Parent(s): 87bfba4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -221
app.py CHANGED
@@ -1,7 +1,6 @@
1
  import os, re, io
2
  import xml.etree.ElementTree as ET
3
  import gradio as gr
4
-
5
  from hand import Hand # your model
6
 
7
  # -----------------------------------------------------------------------------
@@ -11,14 +10,50 @@ os.makedirs("img", exist_ok=True)
11
  hand = Hand()
12
 
13
  # -----------------------------------------------------------------------------
14
- # Small helpers
15
  # -----------------------------------------------------------------------------
16
  def _parse_viewbox(root):
 
 
 
 
 
 
17
  vb = root.get("viewBox")
18
- if not vb:
19
- return (0.0, 0.0, 1200.0, 400.0)
20
- x, y, w, h = map(float, vb.split())
21
- return (x, y, w, h)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  def _extract_paths(elem):
24
  return [e for e in elem.iter() if e.tag.endswith("path")]
@@ -27,15 +62,16 @@ def _translate_group(elem, dx, dy):
27
  prev = elem.get("transform", "")
28
  elem.set("transform", (prev + f" translate({dx},{dy})").strip())
29
 
30
- # Tokenize a line into runs of text / spaces / underscores (order matters)
 
 
31
  def _tokenize_line(line):
32
- tokens = []
33
- i, n = 0, len(line)
34
  while i < n:
35
  ch = line[i]
36
- if ch == '_':
37
  j = i
38
- while j < n and line[j] == '_': j += 1
39
  tokens.append(("sep_underscore", line[i:j]))
40
  i = j
41
  elif ch.isspace():
@@ -45,46 +81,35 @@ def _tokenize_line(line):
45
  i = j
46
  else:
47
  j = i
48
- while j < n and (line[j] != '_' and not line[j].isspace()):
49
  j += 1
50
  tokens.append(("text", line[i:j]))
51
  i = j
52
  return tokens
53
 
54
- # Build the text that the model will actually render (underscores & spaces -> space)
55
  def _display_text_from_tokens(tokens):
56
- out = []
57
- for t, v in tokens:
58
- if t == "text":
59
- out.append(v)
60
- else:
61
- out.append(" ") # normalize separators
62
- return "".join(out)
63
 
64
- # Rasterize an SVG string to RGBA PIL.Image and return (img, vw, vh, vx, vy)
 
 
65
  def _rasterize_svg(svg_str, scale=2.0):
66
  import cairosvg
67
  from PIL import Image
68
  png = cairosvg.svg2png(bytestring=svg_str.encode("utf-8"), scale=scale, background_color="none")
69
- img = Image.open(io.BytesIO(png)).convert("RGBA")
70
- return img
71
 
72
- # From alpha image, find left-to-right blobs & gaps within the content bbox
73
  def _find_blobs_and_gaps(alpha_img):
74
  w, h = alpha_img.size
75
  bbox = alpha_img.getbbox()
76
  if not bbox:
77
- return [], [], (0, 0, w, h) # nothing
78
  left, top, right, bottom = bbox
79
-
80
- # columns with any ink
81
  def col_has_ink(x):
82
- # check a cropped band to speed up
83
  for y in range(top, bottom):
84
  if alpha_img.getpixel((x, y)) > 0:
85
  return True
86
  return False
87
-
88
  blobs, gaps = [], []
89
  x = left
90
  in_blob = col_has_ink(x)
@@ -92,56 +117,29 @@ def _find_blobs_and_gaps(alpha_img):
92
  while x < right:
93
  has = col_has_ink(x)
94
  if has != in_blob:
95
- if in_blob:
96
- blobs.append((start, x)) # [start, x)
97
- else:
98
- gaps.append((start, x))
99
- start = x
100
- in_blob = has
101
  x += 1
102
- # close last run
103
- if in_blob:
104
- blobs.append((start, right))
105
- else:
106
- gaps.append((start, right))
107
-
108
- # We only want gaps **between** blobs, not leading/trailing margins:
109
- core_gaps = []
110
- for i in range(len(blobs) - 1):
111
- core_gaps.append((blobs[i][1], blobs[i + 1][0]))
112
-
113
  return blobs, core_gaps, (left, top, right, bottom)
114
 
115
- # Map pixel x/y to SVG coords via viewBox
116
- def _px_to_svg_x(x_px, img_w, vb):
117
- vx, vy, vw, vh = vb
118
- return vx + (x_px / float(img_w)) * vw
119
-
120
- def _px_to_svg_y(y_px, img_h, vb):
121
- vx, vy, vw, vh = vb
122
- return vy + (y_px / float(img_h)) * vh
123
-
124
- # Draw N underscores filling a gap (pixel coords -> converted to SVG coords)
125
  def _draw_underscores_in_gap(root, gap_px, baseline_px, img_w, img_h, vb,
126
  color, stroke_width, n, pad_px=3, between_px=4):
127
  gap_w = max(0, gap_px[1] - gap_px[0] - 2 * pad_px)
128
- if gap_w <= 6 or n <= 0:
129
- return
130
- # fit underscores nicely within the gap
131
- # give each underscore ~85% of its slot
132
  slot = gap_w / n
133
  line_len = max(8, int(slot * 0.85) - between_px)
134
  offset = (slot - line_len) / 2.0
135
-
136
  for i in range(n):
137
  x0_px = gap_px[0] + pad_px + i * slot + offset
138
  x1_px = x0_px + line_len
139
  y_px = baseline_px
140
-
141
  x0 = _px_to_svg_x(x0_px, img_w, vb)
142
  x1 = _px_to_svg_x(x1_px, img_w, vb)
143
- y = _px_to_svg_y(y_px, img_h, vb)
144
-
145
  p = ET.Element("path")
146
  p.set("d", f"M{x0},{y} L{x1},{y}")
147
  p.set("stroke", color)
@@ -151,65 +149,43 @@ def _draw_underscores_in_gap(root, gap_px, baseline_px, img_w, img_h, vb,
151
  root.append(p)
152
 
153
  # -----------------------------------------------------------------------------
154
- # Render ONE line with correct underscores by analyzing the full-line mask
155
  # -----------------------------------------------------------------------------
156
  def render_line_svg_with_underscores(line, style, bias, color, stroke_width):
157
- """
158
- Returns (line_group, width_estimate). The group contains model strokes + our underscores.
159
- """
160
- # 1) Tokenize and build the display line (underscores & spaces -> spaces)
161
  tokens = _tokenize_line(line)
162
  display_line = _display_text_from_tokens(tokens).replace("/", "-").replace("\\", "-")
163
-
164
- # 2) Ask the model to render this single line to a temp SVG
165
  hand.write(
166
  filename="img/line.tmp.svg",
167
  lines=[display_line if display_line.strip() else " "],
168
- biases=[bias],
169
- styles=[style],
170
- stroke_colors=[color],
171
- stroke_widths=[stroke_width]
172
  )
173
  root = ET.parse("img/line.tmp.svg").getroot()
174
  vb = _parse_viewbox(root)
175
-
176
- # 3) Rasterize the exact SVG we will augment, then find blobs/gaps
177
- from PIL import Image
178
  img = _rasterize_svg(ET.tostring(root, encoding="unicode"))
179
  alpha = img.split()[-1]
180
  blobs, gaps, content_bbox = _find_blobs_and_gaps(alpha)
181
  img_w, img_h = img.size
182
  left, top, right, bottom = content_bbox
183
  line_h_px = max(20, bottom - top)
184
- baseline_px = bottom - int(0.18 * line_h_px) # tuck slightly above bottom
185
-
186
- # 4) Build a <g> that contains all original paths
187
  g = ET.Element("g")
188
- for p in _extract_paths(root):
189
- g.append(p)
190
-
191
- # 5) Determine which gaps correspond to underscores (not regular spaces)
192
- # Walk tokens: whenever we have TEXT then SEPs then TEXT, consume one gap.
193
  gap_idx = 0
194
- i = 0
195
  text_run_count = sum(1 for t, v in tokens if t == "text" and len(v) > 0)
196
- # Defensive: if model merged two words, blobs may be fewer—clamp at min
197
- max_gaps_we_can_use = max(0, min(len(gaps), max(0, text_run_count - 1)))
198
-
199
  while i < len(tokens):
200
  t, v = tokens[i]
201
  if t == "text" and len(v) > 0:
202
- # Count any following separators as one logical gap between this and the next text
203
  j = i + 1
204
- underscore_count = 0
205
- saw_sep = False
206
  while j < len(tokens) and tokens[j][0].startswith("sep_"):
207
  saw_sep = True
208
  if tokens[j][0] == "sep_underscore":
209
  underscore_count += len(tokens[j][1])
210
  j += 1
211
-
212
- if saw_sep and gap_idx < max_gaps_we_can_use:
213
  if underscore_count > 0:
214
  _draw_underscores_in_gap(
215
  g, gaps[gap_idx], baseline_px, img_w, img_h, vb,
@@ -219,99 +195,56 @@ def render_line_svg_with_underscores(line, style, bias, color, stroke_width):
219
  i = j
220
  else:
221
  i += 1
222
-
223
- # 6) Estimate width (use content bbox)
224
- width_estimate = right - left
225
- if width_estimate <= 0:
226
- width_estimate = img_w // 2
227
-
228
  return g, width_estimate
229
 
230
  # -----------------------------------------------------------------------------
231
- # Generate full SVG (multi-line stacking)
232
  # -----------------------------------------------------------------------------
233
- def generate_handwriting(
234
- text,
235
- style,
236
- bias=0.75,
237
- color="#000000",
238
- stroke_width=2,
239
- multiline=True
240
- ):
241
  try:
242
  lines = text.split("\n") if multiline else [text]
243
-
244
  for idx, ln in enumerate(lines):
245
  if len(ln) > 75:
246
- return f"Error: Line {idx+1} is too long (max 75 characters)"
247
-
248
- # Compose final SVG
249
- svg_root = ET.Element("svg", {
250
- "xmlns": "http://www.w3.org/2000/svg",
251
- "viewBox": "0 0 1200 800"
252
- })
253
-
254
- y0 = 80.0
255
- line_gap = 110.0
256
- max_right = 0.0
257
-
258
  for i, line in enumerate(lines):
259
- g, w = render_line_svg_with_underscores(
260
- line, style, bias, color, stroke_width
261
- )
262
- # place this line group lower on the page
263
  _translate_group(g, dx=40, dy=y0 + i * line_gap)
264
  svg_root.append(g)
265
  max_right = max(max_right, 40 + w)
266
-
267
  height = int(y0 + len(lines) * line_gap + 80)
268
  width = max(300, int(max_right + 40))
269
  svg_root.set("viewBox", f"0 0 {width} {height}")
270
-
271
  svg_content = ET.tostring(svg_root, encoding="unicode")
272
- with open("img/output.svg", "w", encoding="utf-8") as f:
273
- f.write(svg_content)
274
  return svg_content
275
-
276
  except Exception as e:
277
  return f"Error: {str(e)}"
278
 
279
  # -----------------------------------------------------------------------------
280
- # PNG export (transparent)
281
  # -----------------------------------------------------------------------------
282
  def export_to_png(svg_content):
283
  try:
284
  import cairosvg
285
  from PIL import Image
286
-
287
  if not svg_content or svg_content.startswith("Error:"):
288
  return None
289
-
290
  tmp_svg = "img/temp.svg"
291
- with open(tmp_svg, "w", encoding="utf-8") as f:
292
- f.write(svg_content)
293
-
294
  cairosvg.svg2png(url=tmp_svg, write_to="img/output_temp.png", scale=2.0, background_color="none")
295
-
296
  img = Image.open("img/output_temp.png")
297
- if img.mode != "RGBA":
298
- img = img.convert("RGBA")
299
-
300
- # Ensure any near-white is transparent (safety)
301
  datas = img.getdata()
302
- img.putdata([(255, 255, 255, 0) if r > 240 and g > 240 and b > 240 else (r, g, b, a)
303
- for (r, g, b, a) in datas])
304
-
305
  out_path = "img/output.png"
306
  img.save(out_path, "PNG")
307
-
308
- try:
309
- os.remove("img/output_temp.png")
310
- except:
311
- pass
312
-
313
  return out_path
314
-
315
  except Exception as e:
316
  print(f"Error converting to PNG: {str(e)}")
317
  return None
@@ -324,81 +257,27 @@ def generate_handwriting_wrapper(text, style, bias, color, stroke_width, multili
324
  png = export_to_png(svg)
325
  return svg, png, "img/output.svg"
326
 
327
- css = """
328
- .container {max-width: 900px; margin: auto;}
329
- .output-container {min-height: 300px;}
330
- .gr-box {border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);}
331
- .footer {text-align: center; margin-top: 20px; font-size: 0.8em; color: #666;}
332
- """
333
 
334
  with gr.Blocks(css=css) as demo:
335
  gr.Markdown("# 🖋️ Handwriting Synthesis (Underscore-aware)")
336
- gr.Markdown("Underscores are placed only where typed, aligned to real gaps from the model output.")
337
-
338
  with gr.Row():
339
  with gr.Column(scale=2):
340
- text_input = gr.Textbox(
341
- label="Text Input",
342
- placeholder="Try: user_name, zeb_3asba, or zeb aasba (no underscore should be drawn)",
343
- lines=5, max_lines=10,
344
- )
345
- with gr.Row():
346
- with gr.Column(scale=1):
347
- style_select = gr.Slider(0, 12, step=1, value=9, label="Handwriting Style")
348
- with gr.Column(scale=1):
349
- bias_slider = gr.Slider(0.5, 1.0, step=0.05, value=0.75, label="Neatness (Higher = Neater)")
350
- with gr.Row():
351
- with gr.Column(scale=1):
352
- color_picker = gr.ColorPicker(label="Ink Color", value="#000000")
353
- with gr.Column(scale=1):
354
- stroke_width = gr.Slider(1, 4, step=0.5, value=2, label="Stroke Width")
355
- with gr.Row():
356
- generate_btn = gr.Button("Generate Handwriting", variant="primary")
357
- clear_btn = gr.Button("Clear")
358
  with gr.Column(scale=3):
359
  output_svg = gr.HTML(label="Generated Handwriting (SVG)", elem_classes=["output-container"])
360
  output_png = gr.Image(type="filepath", label="Generated Handwriting (PNG)", elem_classes=["output-container"])
361
- with gr.Row():
362
- download_svg_file = gr.File(label="Download SVG")
363
- download_png_file = gr.File(label="Download PNG")
 
 
 
364
 
365
- gr.Markdown("""
366
- ### Notes
367
- - Only underscores you type are drawn; normal spaces remain spaces.
368
- - Alignment uses the *actual gaps* between model-drawn word blobs.
369
- - Lines ≤ 75 chars. Slashes (/ and \\) normalized to dashes (-).
370
- """)
371
-
372
- generate_btn.click(
373
- fn=generate_handwriting_wrapper,
374
- inputs=[text_input, style_select, bias_slider, color_picker, stroke_width],
375
- outputs=[output_svg, output_png, download_svg_file]
376
- ).then(
377
- fn=lambda p: p,
378
- inputs=[output_png],
379
- outputs=[download_png_file]
380
- )
381
-
382
- clear_btn.click(
383
- fn=lambda: ("", 9, 0.75, "#000000", 2),
384
- inputs=None,
385
- outputs=[text_input, style_select, bias_slider, color_picker, stroke_width]
386
- )
387
-
388
- # -----------------------------------------------------------------------------
389
- # Main
390
- # -----------------------------------------------------------------------------
391
  if __name__ == "__main__":
392
- port = int(os.environ.get("PORT", 7860))
393
- missing = []
394
- try:
395
- import cairosvg # noqa
396
- except ImportError:
397
- missing.append("cairosvg")
398
- try:
399
- from PIL import Image # noqa
400
- except ImportError:
401
- missing.append("pillow")
402
- if missing:
403
- print("Install:", " ".join(missing))
404
- demo.launch(server_name="0.0.0.0", server_port=port)
 
1
  import os, re, io
2
  import xml.etree.ElementTree as ET
3
  import gradio as gr
 
4
  from hand import Hand # your model
5
 
6
  # -----------------------------------------------------------------------------
 
10
  hand = Hand()
11
 
12
  # -----------------------------------------------------------------------------
13
+ # Helpers
14
  # -----------------------------------------------------------------------------
15
  def _parse_viewbox(root):
16
+ """
17
+ Parse SVG viewBox robustly:
18
+ - accepts "0 0 600 160" or "0,0,600,160"
19
+ - falls back to width/height attributes
20
+ - strips 'px'
21
+ """
22
  vb = root.get("viewBox")
23
+ if vb:
24
+ s = re.sub(r"[,\s]+", " ", vb.strip())
25
+ parts = [p for p in s.split(" ") if p]
26
+ if len(parts) == 4:
27
+ try:
28
+ x, y, w, h = map(float, parts)
29
+ if w <= 0: w = 1200.0
30
+ if h <= 0: h = 400.0
31
+ return (x, y, w, h)
32
+ except ValueError:
33
+ pass
34
+ # fallback: width/height attributes
35
+ def _num(v, default):
36
+ if not v:
37
+ return float(default)
38
+ v = v.strip().lower().replace("px", "")
39
+ v = v.replace(",", ".")
40
+ try:
41
+ return float(v)
42
+ except ValueError:
43
+ return float(default)
44
+ w = _num(root.get("width"), 1200.0)
45
+ h = _num(root.get("height"), 400.0)
46
+ return (0.0, 0.0, w, h)
47
+
48
+ def _px_to_svg_x(x_px, img_w, vb):
49
+ vx, vy, vw, vh = vb
50
+ if img_w <= 0: return vx
51
+ return vx + (x_px / float(img_w)) * vw
52
+
53
+ def _px_to_svg_y(y_px, img_h, vb):
54
+ vx, vy, vw, vh = vb
55
+ if img_h <= 0: return vy
56
+ return vy + (y_px / float(img_h)) * vh
57
 
58
  def _extract_paths(elem):
59
  return [e for e in elem.iter() if e.tag.endswith("path")]
 
62
  prev = elem.get("transform", "")
63
  elem.set("transform", (prev + f" translate({dx},{dy})").strip())
64
 
65
+ # -----------------------------------------------------------------------------
66
+ # Tokenizer
67
+ # -----------------------------------------------------------------------------
68
  def _tokenize_line(line):
69
+ tokens, i, n = [], 0, len(line)
 
70
  while i < n:
71
  ch = line[i]
72
+ if ch == "_":
73
  j = i
74
+ while j < n and line[j] == "_": j += 1
75
  tokens.append(("sep_underscore", line[i:j]))
76
  i = j
77
  elif ch.isspace():
 
81
  i = j
82
  else:
83
  j = i
84
+ while j < n and (line[j] != "_" and not line[j].isspace()):
85
  j += 1
86
  tokens.append(("text", line[i:j]))
87
  i = j
88
  return tokens
89
 
 
90
  def _display_text_from_tokens(tokens):
91
+ return "".join([v if t == "text" else " " for t, v in tokens])
 
 
 
 
 
 
92
 
93
+ # -----------------------------------------------------------------------------
94
+ # Rasterize and analyze gaps
95
+ # -----------------------------------------------------------------------------
96
  def _rasterize_svg(svg_str, scale=2.0):
97
  import cairosvg
98
  from PIL import Image
99
  png = cairosvg.svg2png(bytestring=svg_str.encode("utf-8"), scale=scale, background_color="none")
100
+ return Image.open(io.BytesIO(png)).convert("RGBA")
 
101
 
 
102
  def _find_blobs_and_gaps(alpha_img):
103
  w, h = alpha_img.size
104
  bbox = alpha_img.getbbox()
105
  if not bbox:
106
+ return [], [], (0, 0, w, h)
107
  left, top, right, bottom = bbox
 
 
108
  def col_has_ink(x):
 
109
  for y in range(top, bottom):
110
  if alpha_img.getpixel((x, y)) > 0:
111
  return True
112
  return False
 
113
  blobs, gaps = [], []
114
  x = left
115
  in_blob = col_has_ink(x)
 
117
  while x < right:
118
  has = col_has_ink(x)
119
  if has != in_blob:
120
+ if in_blob: blobs.append((start, x))
121
+ else: gaps.append((start, x))
122
+ start, in_blob = x, has
 
 
 
123
  x += 1
124
+ if in_blob: blobs.append((start, right))
125
+ else: gaps.append((start, right))
126
+ core_gaps = [(blobs[i][1], blobs[i + 1][0]) for i in range(len(blobs) - 1)]
 
 
 
 
 
 
 
 
127
  return blobs, core_gaps, (left, top, right, bottom)
128
 
 
 
 
 
 
 
 
 
 
 
129
  def _draw_underscores_in_gap(root, gap_px, baseline_px, img_w, img_h, vb,
130
  color, stroke_width, n, pad_px=3, between_px=4):
131
  gap_w = max(0, gap_px[1] - gap_px[0] - 2 * pad_px)
132
+ if gap_w <= 6 or n <= 0: return
 
 
 
133
  slot = gap_w / n
134
  line_len = max(8, int(slot * 0.85) - between_px)
135
  offset = (slot - line_len) / 2.0
 
136
  for i in range(n):
137
  x0_px = gap_px[0] + pad_px + i * slot + offset
138
  x1_px = x0_px + line_len
139
  y_px = baseline_px
 
140
  x0 = _px_to_svg_x(x0_px, img_w, vb)
141
  x1 = _px_to_svg_x(x1_px, img_w, vb)
142
+ y = _px_to_svg_y(y_px, img_h, vb)
 
143
  p = ET.Element("path")
144
  p.set("d", f"M{x0},{y} L{x1},{y}")
145
  p.set("stroke", color)
 
149
  root.append(p)
150
 
151
  # -----------------------------------------------------------------------------
152
+ # Line renderer with underscores
153
  # -----------------------------------------------------------------------------
154
  def render_line_svg_with_underscores(line, style, bias, color, stroke_width):
 
 
 
 
155
  tokens = _tokenize_line(line)
156
  display_line = _display_text_from_tokens(tokens).replace("/", "-").replace("\\", "-")
 
 
157
  hand.write(
158
  filename="img/line.tmp.svg",
159
  lines=[display_line if display_line.strip() else " "],
160
+ biases=[bias], styles=[style],
161
+ stroke_colors=[color], stroke_widths=[stroke_width]
 
 
162
  )
163
  root = ET.parse("img/line.tmp.svg").getroot()
164
  vb = _parse_viewbox(root)
 
 
 
165
  img = _rasterize_svg(ET.tostring(root, encoding="unicode"))
166
  alpha = img.split()[-1]
167
  blobs, gaps, content_bbox = _find_blobs_and_gaps(alpha)
168
  img_w, img_h = img.size
169
  left, top, right, bottom = content_bbox
170
  line_h_px = max(20, bottom - top)
171
+ baseline_px = bottom - int(0.18 * line_h_px)
 
 
172
  g = ET.Element("g")
173
+ for p in _extract_paths(root): g.append(p)
 
 
 
 
174
  gap_idx = 0
 
175
  text_run_count = sum(1 for t, v in tokens if t == "text" and len(v) > 0)
176
+ max_gaps = max(0, min(len(gaps), text_run_count - 1))
177
+ i = 0
 
178
  while i < len(tokens):
179
  t, v = tokens[i]
180
  if t == "text" and len(v) > 0:
 
181
  j = i + 1
182
+ underscore_count, saw_sep = 0, False
 
183
  while j < len(tokens) and tokens[j][0].startswith("sep_"):
184
  saw_sep = True
185
  if tokens[j][0] == "sep_underscore":
186
  underscore_count += len(tokens[j][1])
187
  j += 1
188
+ if saw_sep and gap_idx < max_gaps:
 
189
  if underscore_count > 0:
190
  _draw_underscores_in_gap(
191
  g, gaps[gap_idx], baseline_px, img_w, img_h, vb,
 
195
  i = j
196
  else:
197
  i += 1
198
+ width_estimate = right - left if right > left else img_w // 2
 
 
 
 
 
199
  return g, width_estimate
200
 
201
  # -----------------------------------------------------------------------------
202
+ # Generate handwriting (multi-line)
203
  # -----------------------------------------------------------------------------
204
+ def generate_handwriting(text, style, bias=0.75, color="#000000", stroke_width=2, multiline=True):
 
 
 
 
 
 
 
205
  try:
206
  lines = text.split("\n") if multiline else [text]
 
207
  for idx, ln in enumerate(lines):
208
  if len(ln) > 75:
209
+ return f"Error: Line {idx+1} too long (max 75 chars)"
210
+ svg_root = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg","viewBox": "0 0 1200 800"})
211
+ y0, line_gap, max_right = 80.0, 110.0, 0.0
 
 
 
 
 
 
 
 
 
212
  for i, line in enumerate(lines):
213
+ g, w = render_line_svg_with_underscores(line, style, bias, color, stroke_width)
 
 
 
214
  _translate_group(g, dx=40, dy=y0 + i * line_gap)
215
  svg_root.append(g)
216
  max_right = max(max_right, 40 + w)
 
217
  height = int(y0 + len(lines) * line_gap + 80)
218
  width = max(300, int(max_right + 40))
219
  svg_root.set("viewBox", f"0 0 {width} {height}")
 
220
  svg_content = ET.tostring(svg_root, encoding="unicode")
221
+ with open("img/output.svg", "w", encoding="utf-8") as f: f.write(svg_content)
 
222
  return svg_content
 
223
  except Exception as e:
224
  return f"Error: {str(e)}"
225
 
226
  # -----------------------------------------------------------------------------
227
+ # Export to PNG
228
  # -----------------------------------------------------------------------------
229
  def export_to_png(svg_content):
230
  try:
231
  import cairosvg
232
  from PIL import Image
 
233
  if not svg_content or svg_content.startswith("Error:"):
234
  return None
 
235
  tmp_svg = "img/temp.svg"
236
+ with open(tmp_svg, "w", encoding="utf-8") as f: f.write(svg_content)
 
 
237
  cairosvg.svg2png(url=tmp_svg, write_to="img/output_temp.png", scale=2.0, background_color="none")
 
238
  img = Image.open("img/output_temp.png")
239
+ if img.mode != "RGBA": img = img.convert("RGBA")
 
 
 
240
  datas = img.getdata()
241
+ img.putdata([(255,255,255,0) if r>240 and g>240 and b>240 else (r,g,b,a)
242
+ for (r,g,b,a) in datas])
 
243
  out_path = "img/output.png"
244
  img.save(out_path, "PNG")
245
+ try: os.remove("img/output_temp.png")
246
+ except: pass
 
 
 
 
247
  return out_path
 
248
  except Exception as e:
249
  print(f"Error converting to PNG: {str(e)}")
250
  return None
 
257
  png = export_to_png(svg)
258
  return svg, png, "img/output.svg"
259
 
260
+ css = ".container {max-width:900px;margin:auto;} .output-container{min-height:300px;}"
 
 
 
 
 
261
 
262
  with gr.Blocks(css=css) as demo:
263
  gr.Markdown("# 🖋️ Handwriting Synthesis (Underscore-aware)")
 
 
264
  with gr.Row():
265
  with gr.Column(scale=2):
266
+ text_input = gr.Textbox(label="Text Input", placeholder="Try: user_name vs zeb aasba", lines=5)
267
+ style_select = gr.Slider(0, 12, step=1, value=9, label="Handwriting Style")
268
+ bias_slider = gr.Slider(0.5, 1.0, step=0.05, value=0.75, label="Neatness")
269
+ color_picker = gr.ColorPicker(label="Ink Color", value="#000000")
270
+ stroke_width = gr.Slider(1, 4, step=0.5, value=2, label="Stroke Width")
271
+ generate_btn = gr.Button("Generate Handwriting", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
272
  with gr.Column(scale=3):
273
  output_svg = gr.HTML(label="Generated Handwriting (SVG)", elem_classes=["output-container"])
274
  output_png = gr.Image(type="filepath", label="Generated Handwriting (PNG)", elem_classes=["output-container"])
275
+ download_svg_file = gr.File(label="Download SVG")
276
+ download_png_file = gr.File(label="Download PNG")
277
+ generate_btn.click(fn=generate_handwriting_wrapper,
278
+ inputs=[text_input, style_select, bias_slider, color_picker, stroke_width],
279
+ outputs=[output_svg, output_png, download_svg_file]
280
+ ).then(fn=lambda p: p, inputs=[output_png], outputs=[download_png_file])
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  if __name__ == "__main__":
283
+ demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))