3morrrrr commited on
Commit
5b17b59
·
verified ·
1 Parent(s): a1c2125

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +242 -285
app.py CHANGED
@@ -1,246 +1,257 @@
1
- import gradio as gr
2
  import os
3
- import threading
4
- import subprocess
5
- import time
6
  import re
7
  import xml.etree.ElementTree as ET
8
- from huggingface_hub import hf_hub_download
9
- from handwriting_api import InputData, validate_input
10
- from hand import Hand
11
 
12
- # Create img directory if it doesn't exist
13
- os.makedirs("img", exist_ok=True)
 
14
 
15
- # Initialize the handwriting model
 
 
 
16
  hand = Hand()
17
 
18
- def insert_font_underscores(svg_content, original_lines, underscore_positions, color, stroke_width):
 
 
 
 
 
 
19
  """
20
- Insert font-based underscores into the SVG at the tracked positions
 
21
  """
22
- try:
23
- # Parse the SVG
24
- root = ET.fromstring(svg_content)
25
-
26
- # Get SVG dimensions
27
- viewbox = root.get('viewBox', '0 0 600 200')
28
- vb_parts = viewbox.split()
29
- svg_width = float(vb_parts[2]) if len(vb_parts) > 2 else 600
30
- svg_height = float(vb_parts[3]) if len(vb_parts) > 3 else 200
31
-
32
- # Better character width estimation based on actual text length
33
- max_chars = max(len(line) for line in original_lines) if original_lines else 20
34
- char_width = (svg_width - 40) / max_chars if max_chars > 0 else 30 # Leave 20px margin on each side
35
-
36
- # Process each line
37
- for line_idx, (original_line, underscore_pos_list) in enumerate(zip(original_lines, underscore_positions)):
38
- if not underscore_pos_list:
39
- continue
40
-
41
- # Calculate Y position for this line - better alignment with handwriting
42
- # Position underscores slightly below the baseline of the text
43
- line_y = 80 + (line_idx * 80) # Start at 80px, 80px between lines
44
-
45
- # Insert underscores at tracked positions
46
- for underscore_pos in underscore_pos_list:
47
- # Calculate X position with better spacing
48
- x_pos = 20 + (underscore_pos * char_width)
49
-
50
- # Create a path element to draw the underscore as a line
51
- # This is more reliable than text elements
52
- underscore_width = char_width * 0.8 # Make underscore slightly shorter than character width
53
- underscore_height = 2 # Thin line
54
-
55
- # Create path data for a horizontal line (underscore)
56
- path_data = f"M{x_pos},{line_y} L{x_pos + underscore_width},{line_y}"
57
-
58
- path_elem = ET.Element('path')
59
- path_elem.set('d', path_data)
60
- path_elem.set('stroke', color)
61
- path_elem.set('stroke-width', str(max(1, stroke_width)))
62
- path_elem.set('fill', 'none')
63
- path_elem.set('stroke-linecap', 'round')
64
-
65
- # Add to the SVG
66
- root.append(path_elem)
67
-
68
- # Convert back to string
69
- return ET.tostring(root, encoding='unicode')
70
-
71
- except Exception as e:
72
- print(f"Error inserting font underscores: {str(e)}")
73
- return svg_content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # Create a function to generate handwriting
76
  def generate_handwriting(
77
- text,
78
- style,
79
- bias=0.75,
80
  color="#000000",
81
  stroke_width=2,
82
- multiline=True,
83
- transparent_background=True
84
  ):
85
- """Generate handwritten text using the model"""
 
 
86
  try:
87
- # Process the text
88
- if multiline:
89
- lines = text.split('\n')
90
- else:
91
- lines = [text]
92
-
93
- # Create arrays for parameters
94
- stroke_colors = [color] * len(lines)
95
- stroke_widths = [stroke_width] * len(lines)
96
- biases = [bias] * len(lines)
97
- styles = [style] * len(lines)
98
-
99
- # Process each line to replace unsupported characters with placeholders
100
- sanitized_lines = []
101
- underscore_positions = [] # Track where underscores were replaced
102
-
103
- for line_num, line in enumerate(lines):
104
- if len(line) > 75:
105
- return f"Error: Line {line_num+1} is too long (max 75 characters)"
106
-
107
- # Replace slashes with dashes and underscores with a placeholder
108
- sanitized_line = line.replace('/', '-').replace('\\', '-')
109
-
110
- # Track underscore positions for later replacement
111
- line_underscores = []
112
- for i, char in enumerate(sanitized_line):
113
- if char == '_':
114
- line_underscores.append(i)
115
-
116
- # Replace underscores with a space (will be replaced with font underscore later)
117
- sanitized_line = sanitized_line.replace('_', ' ')
118
- sanitized_lines.append(sanitized_line)
119
- underscore_positions.append(line_underscores)
120
-
121
- data = InputData(
122
- text='\n'.join(sanitized_lines),
123
- style=style,
124
- bias=bias,
125
- stroke_colors=stroke_colors,
126
- stroke_widths=stroke_widths
127
- )
128
-
129
- try:
130
- validate_input(data)
131
- except ValueError as e:
132
- return f"Error: {str(e)}"
133
-
134
- # Generate the handwriting with sanitized lines
135
- hand.write(
136
- filename='img/output.svg',
137
- lines=sanitized_lines,
138
- biases=biases,
139
- styles=styles,
140
- stroke_colors=stroke_colors,
141
- stroke_widths=stroke_widths
142
- )
143
-
144
- # Read the generated SVG
145
- with open("img/output.svg", "r") as f:
146
- svg_content = f.read()
147
-
148
- # Insert font-based underscores at tracked positions
149
- svg_content = insert_font_underscores(svg_content, lines, underscore_positions, color, stroke_width)
150
-
151
- # If transparent background is requested, modify the SVG
152
- if transparent_background:
153
- # Remove the background rectangle or make it transparent
154
- pattern = r'<rect[^>]*?fill="white"[^>]*?>'
155
- if re.search(pattern, svg_content):
156
- svg_content = re.sub(pattern, '', svg_content)
157
-
158
- # Write the modified SVG back
159
- with open("img/output.svg", "w") as f:
160
- f.write(svg_content)
161
-
162
  return svg_content
 
163
  except Exception as e:
164
  return f"Error: {str(e)}"
165
 
 
 
166
  def export_to_png(svg_content):
167
- """Convert SVG to transparent PNG using CairoSVG and Pillow for robust transparency"""
168
  try:
169
  import cairosvg
170
  from PIL import Image
171
-
172
  if not svg_content or svg_content.startswith("Error:"):
173
  return None
174
-
175
- # Modify the SVG to ensure the background is transparent
176
- # Remove any white background rectangle
177
- pattern = r'<rect[^>]*?fill="white"[^>]*?>'
178
- if re.search(pattern, svg_content):
179
- svg_content = re.sub(pattern, '', svg_content)
180
-
181
- # Save the modified SVG to a temporary file
182
- with open("img/temp.svg", "w") as f:
183
  f.write(svg_content)
184
-
185
- # Convert SVG to PNG with transparency using CairoSVG
186
  cairosvg.svg2png(
187
- url="img/temp.svg",
188
- write_to="img/output_temp.png",
189
  scale=2.0,
190
- background_color="none" # This ensures transparency
191
  )
192
-
193
- # Additional processing with Pillow to ensure transparency
194
  img = Image.open("img/output_temp.png")
195
-
196
- # Convert to RGBA if not already
197
  if img.mode != 'RGBA':
198
  img = img.convert('RGBA')
199
-
200
- # Create a transparent canvas
201
- transparent_img = Image.new('RGBA', img.size, (0, 0, 0, 0))
202
-
203
- # Process the image data to ensure white is transparent
204
  datas = img.getdata()
205
  new_data = []
206
-
207
  for item in datas:
208
- # If pixel is white or near-white, make it transparent
209
  if item[0] > 240 and item[1] > 240 and item[2] > 240:
210
- new_data.append((255, 255, 255, 0)) # Transparent
211
  else:
212
- new_data.append(item) # Keep original color
213
-
214
- transparent_img.putdata(new_data)
215
- transparent_img.save("img/output.png", "PNG")
216
-
217
- # Clean up the temporary file
 
218
  try:
219
  os.remove("img/output_temp.png")
220
  except:
221
  pass
222
-
223
- return "img/output.png"
 
224
  except Exception as e:
225
  print(f"Error converting to PNG: {str(e)}")
226
  return None
227
 
228
- def generate_lyrics_sample():
229
- """Generate a sample using lyrics"""
230
- from lyrics import all_star
231
- return all_star.split("\n")[0:4]
232
-
233
- def generate_handwriting_wrapper(
234
- text,
235
- style,
236
- bias,
237
- color,
238
- stroke_width,
239
- multiline=True
240
- ):
241
  svg = generate_handwriting(text, style, bias, color, stroke_width, multiline)
242
  png_path = export_to_png(svg)
243
- return svg, png_path
 
244
 
245
  css = """
246
  .container {max-width: 900px; margin: auto;}
@@ -250,134 +261,80 @@ css = """
250
  """
251
 
252
  with gr.Blocks(css=css) as demo:
253
- gr.Markdown("# 🖋️ Handwriting Synthesis")
254
- gr.Markdown("Generate realistic handwritten text using neural networks.")
255
-
256
  with gr.Row():
257
  with gr.Column(scale=2):
258
  text_input = gr.Textbox(
259
  label="Text Input",
260
- placeholder="Enter text to convert to handwriting...",
261
  lines=5,
262
  max_lines=10,
263
  )
264
-
265
  with gr.Row():
266
  with gr.Column(scale=1):
267
- style_select = gr.Slider(
268
- minimum=0,
269
- maximum=12,
270
- step=1,
271
- value=9,
272
- label="Handwriting Style"
273
- )
274
  with gr.Column(scale=1):
275
- bias_slider = gr.Slider(
276
- minimum=0.5,
277
- maximum=1.0,
278
- step=0.05,
279
- value=0.75,
280
- label="Neatness (Higher = Neater)"
281
- )
282
-
283
  with gr.Row():
284
  with gr.Column(scale=1):
285
- color_picker = gr.ColorPicker(
286
- label="Ink Color",
287
- value="#000000"
288
- )
289
  with gr.Column(scale=1):
290
- stroke_width = gr.Slider(
291
- minimum=1,
292
- maximum=4,
293
- step=0.5,
294
- value=2,
295
- label="Stroke Width"
296
- )
297
-
298
  with gr.Row():
299
  generate_btn = gr.Button("Generate Handwriting", variant="primary")
300
  clear_btn = gr.Button("Clear")
301
-
302
- with gr.Accordion("Examples", open=False):
303
- sample_btn = gr.Button("Insert Sample Text")
304
-
305
  with gr.Column(scale=3):
306
  output_svg = gr.HTML(label="Generated Handwriting (SVG)", elem_classes=["output-container"])
307
  output_png = gr.Image(type="filepath", label="Generated Handwriting (PNG)", elem_classes=["output-container"])
308
-
309
  with gr.Row():
310
- download_svg_btn = gr.Button("Download SVG")
311
- download_png_btn = gr.Button("Download PNG")
312
-
313
- gr.Markdown("""
314
- ### Tips:
315
- - Try different styles (0-12) to get various handwriting appearances
316
- - Adjust the neatness slider to make writing more or less tidy
317
- - Each line should be 75 characters or less
318
- - The model works best for English text
319
- - Forward slashes (/) and backslashes (\\) will be replaced with dashes (-)
320
- - Underscores (_) will be rendered using font characters for better appearance
321
- - PNG output has transparency for easy integration into other documents
322
- """)
323
-
324
  gr.Markdown("""
325
- <div class="footer">
326
- Created with Gradio
327
- </div>
 
 
328
  """)
329
-
330
- # Define interactions
331
  generate_btn.click(
332
  fn=generate_handwriting_wrapper,
333
  inputs=[text_input, style_select, bias_slider, color_picker, stroke_width],
334
- outputs=[output_svg, output_png]
 
 
 
 
335
  )
336
-
337
  clear_btn.click(
338
  fn=lambda: ("", 9, 0.75, "#000000", 2),
339
  inputs=None,
340
  outputs=[text_input, style_select, bias_slider, color_picker, stroke_width]
341
  )
342
-
343
- sample_btn.click(
344
- fn=lambda: ("\n".join(generate_lyrics_sample())),
345
- inputs=None,
346
- outputs=[text_input]
347
- )
348
-
349
- download_svg_btn.click(
350
- fn=lambda x: x,
351
- inputs=[output_svg],
352
- outputs=[gr.File(label="Download SVG", file_count="single", file_types=[".svg"])]
353
- )
354
-
355
- download_png_btn.click(
356
- fn=lambda x: x,
357
- inputs=[output_png],
358
- outputs=[gr.File(label="Download PNG", file_count="single", file_types=[".png"])]
359
- )
360
 
361
  if __name__ == "__main__":
362
- # Set port based on environment variable or default to 7860
363
  port = int(os.environ.get("PORT", 7860))
364
-
365
- # Check if required packages are installed
366
  missing_packages = []
367
  try:
368
- import cairosvg
369
  except ImportError:
370
  missing_packages.append("cairosvg")
371
-
372
  try:
373
- from PIL import Image
374
  except ImportError:
375
  missing_packages.append("pillow")
376
-
377
  if missing_packages:
378
- print(f"WARNING: The following packages are missing and required for transparent PNG export: {', '.join(missing_packages)}")
379
- print("Please install them using: pip install " + " ".join(missing_packages))
380
- else:
381
- print("All required packages are installed and ready for transparent PNG export")
382
-
383
- demo.launch(server_name="0.0.0.0", server_port=port)
 
 
1
  import os
 
 
 
2
  import re
3
  import xml.etree.ElementTree as ET
4
+ import gradio as gr
 
 
5
 
6
+ # If you use these in your env, keep them. Otherwise they're optional.
7
+ # from huggingface_hub import hf_hub_download
8
+ # from handwriting_api import InputData, validate_input
9
 
10
+ from hand import Hand # your handwriting model wrapper
11
+
12
+ # --- Setup --------------------------------------------------------------------
13
+ os.makedirs("img", exist_ok=True)
14
  hand = Hand()
15
 
16
+ # --- SVG helpers ---------------------------------------------------------------
17
+
18
+ def _extract_paths(svg_root_or_group):
19
+ """Return a list of <path> elements (namespace-agnostic)."""
20
+ return [el for el in svg_root_or_group.iter() if el.tag.endswith('path')]
21
+
22
+ def _bbox_of_paths(paths):
23
  """
24
+ Conservative bbox by scanning numeric coords inside each path 'd'.
25
+ Not mathematically perfect for curve extrema, but close enough for layout.
26
  """
27
+ xs, ys = [], []
28
+ num_re = re.compile(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?')
29
+ for p in paths:
30
+ d = p.get('d', '')
31
+ nums = list(map(float, num_re.findall(d)))
32
+ for i in range(0, len(nums) - 1, 2):
33
+ xs.append(nums[i])
34
+ ys.append(nums[i + 1])
35
+ if not xs or not ys:
36
+ return (0.0, 0.0, 0.0, 0.0)
37
+ return (min(xs), min(ys), max(xs), max(ys))
38
+
39
+ def _translate_group(elem, dx, dy):
40
+ prev = elem.get('transform', '')
41
+ t = f"translate({dx},{dy})"
42
+ elem.set('transform', (prev + " " + t).strip())
43
+
44
+ # --- Segment compositor (the key fix) -----------------------------------------
45
+
46
+ def _render_part_group(text_part, style, bias, color, stroke_width):
47
+ """
48
+ Render a single text 'part' (no underscores) using the Hand model to a temp SVG,
49
+ pull out its paths, wrap them in a <g>, and return (group, bbox).
50
+ """
51
+ # Avoid empty part causing errors: render a single space
52
+ lines = [text_part if text_part else " "]
53
+ hand.write(
54
+ filename='img/part.tmp.svg',
55
+ lines=lines,
56
+ biases=[bias],
57
+ styles=[style],
58
+ stroke_colors=[color],
59
+ stroke_widths=[stroke_width]
60
+ )
61
+ svg = ET.parse('img/part.tmp.svg').getroot()
62
+ g = ET.Element('g')
63
+ for p in _extract_paths(svg):
64
+ g.append(p)
65
+ bbox = _bbox_of_paths(_extract_paths(g))
66
+ return g, bbox
67
+
68
+ def render_line_with_underscores(
69
+ line,
70
+ style,
71
+ bias,
72
+ color,
73
+ stroke_width,
74
+ line_y,
75
+ x_start=40,
76
+ gap_left=8,
77
+ gap_right=8,
78
+ underscore_len=22
79
+ ):
80
+ """
81
+ Compose a line that may include underscores by rendering surrounding segments,
82
+ measuring their true widths, and drawing the underscore at the baseline.
83
+ Returns (list_of_elements, rightmost_x).
84
+ """
85
+ parts = line.split('_')
86
+
87
+ # Render every part and measure bboxes
88
+ part_groups, part_bboxes = [], []
89
+ for part in parts:
90
+ g, bbox = _render_part_group(part, style, bias, color, stroke_width)
91
+ part_groups.append(g)
92
+ part_bboxes.append(bbox)
93
+
94
+ # Estimate line metrics from first non-empty part (fallbacks if needed)
95
+ line_height = 40.0
96
+ for bbox in part_bboxes:
97
+ minx, miny, maxx, maxy = bbox
98
+ if maxy - miny > 0.0:
99
+ line_height = max(20.0, maxy - miny)
100
+ break
101
+ base_y = line_y + 0.8 * line_height
102
+
103
+ # Compose into positioned elements
104
+ composed = []
105
+ cursor_x = x_start
106
+
107
+ for i, (g, bbox) in enumerate(zip(part_groups, part_bboxes)):
108
+ minx, _, maxx, _ = bbox
109
+ width = max(0.0, maxx - minx)
110
+
111
+ # Place this segment at current cursor_x
112
+ _translate_group(g, dx=(cursor_x - minx), dy=line_y)
113
+ composed.append(g)
114
+ cursor_x += width
115
+
116
+ # If there is an underscore after this part, draw it now
117
+ if i < len(part_groups) - 1:
118
+ x0 = cursor_x + gap_left
119
+ x1 = x0 + underscore_len
120
+ underscore = ET.Element('path')
121
+ underscore.set('d', f"M{x0},{base_y} L{x1},{base_y}")
122
+ underscore.set('stroke', color)
123
+ underscore.set('stroke-width', str(max(1, stroke_width)))
124
+ underscore.set('fill', 'none')
125
+ underscore.set('stroke-linecap', 'round')
126
+ composed.append(underscore)
127
+ cursor_x = x1 + gap_right # advance cursor
128
+
129
+ return composed, cursor_x
130
+
131
+ # --- Handwriting generation (multi-line + composition) ------------------------
132
 
 
133
  def generate_handwriting(
134
+ text,
135
+ style,
136
+ bias=0.75,
137
  color="#000000",
138
  stroke_width=2,
139
+ multiline=True
 
140
  ):
141
+ """
142
+ Generate a composed SVG that places underscores geometrically between segments.
143
+ """
144
  try:
145
+ lines = text.split('\n') if multiline else [text]
146
+
147
+ # Light validation and slash normalization (your old behavior)
148
+ for idx, ln in enumerate(lines):
149
+ if len(ln) > 75:
150
+ return f"Error: Line {idx + 1} is too long (max 75 characters)"
151
+ lines[idx] = ln.replace('/', '-').replace('\\', '-') # keep underscores intact
152
+
153
+ # Create a fresh SVG root we control (no background rect -> transparent)
154
+ svg_root = ET.Element('svg', {
155
+ 'xmlns': 'http://www.w3.org/2000/svg',
156
+ 'viewBox': '0 0 1200 800'
157
+ })
158
+
159
+ y0 = 80.0
160
+ line_gap = 100.0
161
+ max_x = 0.0
162
+
163
+ for i, original_line in enumerate(lines):
164
+ line_y = y0 + i * line_gap
165
+ elems, right_x = render_line_with_underscores(
166
+ original_line,
167
+ style,
168
+ bias,
169
+ color,
170
+ stroke_width,
171
+ line_y,
172
+ x_start=40,
173
+ gap_left=8,
174
+ gap_right=8,
175
+ underscore_len=22
176
+ )
177
+ for el in elems:
178
+ svg_root.append(el)
179
+ max_x = max(max_x, right_x)
180
+
181
+ # Tighten viewBox to content width + margin
182
+ width = max(300, int(max_x + 40))
183
+ height = int(y0 + len(lines) * line_gap)
184
+ svg_root.set('viewBox', f"0 0 {width} {height}")
185
+
186
+ svg_content = ET.tostring(svg_root, encoding='unicode')
187
+ # Persist for download
188
+ with open("img/output.svg", "w", encoding="utf-8") as f:
189
+ f.write(svg_content)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  return svg_content
191
+
192
  except Exception as e:
193
  return f"Error: {str(e)}"
194
 
195
+ # --- PNG export (transparent) --------------------------------------------------
196
+
197
  def export_to_png(svg_content):
198
+ """Convert SVG to transparent PNG using CairoSVG and Pillow."""
199
  try:
200
  import cairosvg
201
  from PIL import Image
202
+
203
  if not svg_content or svg_content.startswith("Error:"):
204
  return None
205
+
206
+ # Ensure we write the current svg to disk (CairoSVG can read from file)
207
+ tmp_svg = "img/temp.svg"
208
+ with open(tmp_svg, "w", encoding="utf-8") as f:
 
 
 
 
 
209
  f.write(svg_content)
210
+
211
+ # Render at higher scale for crisp strokes
212
  cairosvg.svg2png(
213
+ url=tmp_svg,
214
+ write_to="img/output_temp.png",
215
  scale=2.0,
216
+ background_color="none"
217
  )
218
+
 
219
  img = Image.open("img/output_temp.png")
 
 
220
  if img.mode != 'RGBA':
221
  img = img.convert('RGBA')
222
+
223
+ # Optional: force near-white background fully transparent (safety)
 
 
 
224
  datas = img.getdata()
225
  new_data = []
 
226
  for item in datas:
 
227
  if item[0] > 240 and item[1] > 240 and item[2] > 240:
228
+ new_data.append((255, 255, 255, 0))
229
  else:
230
+ new_data.append(item)
231
+ img.putdata(new_data)
232
+
233
+ out_path = "img/output.png"
234
+ img.save(out_path, "PNG")
235
+
236
+ # cleanup
237
  try:
238
  os.remove("img/output_temp.png")
239
  except:
240
  pass
241
+
242
+ return out_path
243
+
244
  except Exception as e:
245
  print(f"Error converting to PNG: {str(e)}")
246
  return None
247
 
248
+ # --- Gradio UI ----------------------------------------------------------------
249
+
250
+ def generate_handwriting_wrapper(text, style, bias, color, stroke_width, multiline=True):
 
 
 
 
 
 
 
 
 
 
251
  svg = generate_handwriting(text, style, bias, color, stroke_width, multiline)
252
  png_path = export_to_png(svg)
253
+ # Display SVG inline; return file path for PNG
254
+ return svg, png_path, "img/output.svg"
255
 
256
  css = """
257
  .container {max-width: 900px; margin: auto;}
 
261
  """
262
 
263
  with gr.Blocks(css=css) as demo:
264
+ gr.Markdown("# 🖋️ Handwriting Synthesis (Underscore-safe)")
265
+ gr.Markdown("Generate realistic handwritten text with **true** underscore alignment—no retraining required.")
266
+
267
  with gr.Row():
268
  with gr.Column(scale=2):
269
  text_input = gr.Textbox(
270
  label="Text Input",
271
+ placeholder="Try: zeb_3asba or user_name underscores render perfectly",
272
  lines=5,
273
  max_lines=10,
274
  )
 
275
  with gr.Row():
276
  with gr.Column(scale=1):
277
+ style_select = gr.Slider(minimum=0, maximum=12, step=1, value=9, label="Handwriting Style")
 
 
 
 
 
 
278
  with gr.Column(scale=1):
279
+ bias_slider = gr.Slider(minimum=0.5, maximum=1.0, step=0.05, value=0.75, label="Neatness (Higher = Neater)")
 
 
 
 
 
 
 
280
  with gr.Row():
281
  with gr.Column(scale=1):
282
+ color_picker = gr.ColorPicker(label="Ink Color", value="#000000")
 
 
 
283
  with gr.Column(scale=1):
284
+ stroke_width = gr.Slider(minimum=1, maximum=4, step=0.5, value=2, label="Stroke Width")
 
 
 
 
 
 
 
285
  with gr.Row():
286
  generate_btn = gr.Button("Generate Handwriting", variant="primary")
287
  clear_btn = gr.Button("Clear")
288
+
 
 
 
289
  with gr.Column(scale=3):
290
  output_svg = gr.HTML(label="Generated Handwriting (SVG)", elem_classes=["output-container"])
291
  output_png = gr.Image(type="filepath", label="Generated Handwriting (PNG)", elem_classes=["output-container"])
 
292
  with gr.Row():
293
+ download_svg_file = gr.File(label="Download SVG")
294
+ download_png_file = gr.File(label="Download PNG")
295
+
 
 
 
 
 
 
 
 
 
 
 
296
  gr.Markdown("""
297
+ ### Notes
298
+ - Underscores are drawn **between** segments using real geometry → perfect alignment.
299
+ - Slashes (/, \\) are normalized to dashes (-) for model stability.
300
+ - Each line ≤ 75 characters.
301
+ - Transparent PNG export included.
302
  """)
303
+
 
304
  generate_btn.click(
305
  fn=generate_handwriting_wrapper,
306
  inputs=[text_input, style_select, bias_slider, color_picker, stroke_width],
307
+ outputs=[output_svg, output_png, download_svg_file]
308
+ ).then(
309
+ fn=lambda p: p,
310
+ inputs=[output_png],
311
+ outputs=[download_png_file]
312
  )
313
+
314
  clear_btn.click(
315
  fn=lambda: ("", 9, 0.75, "#000000", 2),
316
  inputs=None,
317
  outputs=[text_input, style_select, bias_slider, color_picker, stroke_width]
318
  )
319
+
320
+ # --- Main ---------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
  if __name__ == "__main__":
 
323
  port = int(os.environ.get("PORT", 7860))
324
+
325
+ # Soft check for optional deps
326
  missing_packages = []
327
  try:
328
+ import cairosvg # noqa
329
  except ImportError:
330
  missing_packages.append("cairosvg")
 
331
  try:
332
+ from PIL import Image # noqa
333
  except ImportError:
334
  missing_packages.append("pillow")
335
+
336
  if missing_packages:
337
+ print(f"WARNING: Missing packages for transparent PNG export: {', '.join(missing_packages)}")
338
+ print("Install with: pip install " + " ".join(missing_packages))
339
+
340
+ demo.launch(server_name="0.0.0.0", server_port=port)