ChevalierJoseph commited on
Commit
b9e817a
·
verified ·
1 Parent(s): db165ec

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +112 -169
app.py CHANGED
@@ -1,221 +1,156 @@
1
- import spaces
2
- from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
3
- import gradio as gr
4
- import torch
5
- from threading import Thread
6
  import re
7
- import io
8
- import zipfile
9
- import tempfile
10
  import os
 
11
  import glob
12
- from fontmake.font_project import FontProject
13
- from defcon import Font, Glyph, Point
14
- from fontTools.pens.svgPathPen import SVGPathPen
15
- from svgpathtools import parse_path, Path, Line, CubicBezier, QuadraticBezier, Arc
16
- from svgpathtools.path import Path as SvgPath
17
- from svgpathtools.path import BoundingBox # Importation de BoundingBox pour gérer les limites
18
-
19
- # ------------------------
20
- # MODELE
21
- # ------------------------
22
  def load_model():
23
  tokenizer = AutoTokenizer.from_pretrained("ChevalierJoseph/typtop")
24
  model = AutoModelForCausalLM.from_pretrained("ChevalierJoseph/typtop")
25
  return tokenizer, model
26
 
27
- # ------------------------
28
- # SVG + GLYPH EXTRACTION
29
- # ------------------------
30
- def generate_svg(path_data, width=50, height=50):
31
- svg_template = f"""
32
- <svg width="{width}" height="{height}" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg">
33
- <path d="{path_data}" fill="black"/>
34
- </svg>
35
- """
36
- return svg_template
37
-
38
  def extract_glyphs(text):
39
  pattern = r"Glyph\s+([A-Z])\s+([MmZzLlHhVvCcSsQqTtAa0-9,\s\.\-]+?)(?=\s*Glyph\s+[A-Z]|\s*$)"
40
  glyphs = re.findall(pattern, text)
41
  return glyphs
42
 
43
- def generate_glyphs_html(glyphs, cols=5, width=100, height=100):
44
- html_parts = []
45
- for lettre, path in glyphs:
46
- svg_content = f"""
47
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -800 900 900" width="{width}" height="{height}">
48
- <g transform="translate(0, 0)">
49
- <path d="{path.strip()}" fill="black"/>
50
- </g>
51
- </svg>
52
- """
53
- html_parts.append(f"<div style='display: inline-block; margin: 10px; text-align: center;'><h3>{lettre}</h3>{svg_content}</div>")
54
- grid_style = f"display: grid; grid-template-columns: repeat({cols}, 1fr); gap: 20px;"
55
- return f'<div style="{grid_style}">{"".join(html_parts)}</div>'
56
-
57
- def generate_svg_files(glyphs, width=100, height=100):
58
- svg_files = {}
59
- for lettre, path in glyphs:
60
- svg_content = f"""
61
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -800 900 900" width="{width}" height="{height}">
62
- <g transform="translate(0, 0)">
63
- <path d="{path.strip()}" fill="black"/>
64
- </g>
65
- </svg>
66
- """
67
- svg_files[f"{lettre}.svg"] = svg_content
68
- return svg_files
69
-
70
- def create_zip(svg_files):
71
- with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp_file:
72
- zip_path = tmp_file.name
73
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zip_file:
74
- for filename, content in svg_files.items():
75
- zip_file.writestr(filename, content)
76
- return zip_path
77
-
78
- # ------------------------
79
- # UFO / FONTMAKE
80
- # ------------------------
81
- def validate_svg_path(path_data):
82
- try:
83
- parse_path(path_data)
84
- return True
85
- except Exception as e:
86
- print(f"Invalid SVG path: {e}")
87
- return False
88
-
89
  def build_ufo_from_glyphs(glyphs):
90
  font = Font()
91
  font.info.familyName = "TipTopType"
92
  font.info.styleName = "Regular"
93
  font.info.unitsPerEm = 1000
94
  font.info.ascender = 800
95
- font.info.descender = -200
96
  font.info.capHeight = 700
97
  font.info.xHeight = 500
 
98
  for letter, path_data in glyphs:
99
- if not validate_svg_path(path_data):
100
- print(f"Skipping glyph {letter} due to invalid SVG path.")
101
- continue
102
- glyph = Glyph()
103
- glyph.name = letter
104
- glyph.unicode = ord(letter)
105
- pen = glyph.getPen()
106
  try:
107
  path = parse_path(path_data)
108
- normalized_path = []
 
 
 
 
 
 
 
 
109
  for segment in path:
110
- if segment.length() == 0:
111
- continue
112
- normalized_path.append(segment)
113
- if normalized_path:
114
- # Calculer les limites du chemin
115
- bounds = None
116
- for segment in normalized_path:
117
- segment_bounds = BoundingBox([segment.start, segment.end])
118
- if isinstance(segment, (CubicBezier, QuadraticBezier)):
119
- segment_bounds |= BoundingBox([segment.control1, segment.control2])
120
- if bounds is None:
121
- bounds = segment_bounds
122
- else:
123
- bounds |= segment_bounds
124
- if bounds and (bounds.width > 0) and (bounds.height > 0):
125
- glyph_width = 800
126
- glyph_height = 1000
127
- scale_factor = glyph_height * 0.8 / bounds.height
128
- offset_x = (glyph_width - bounds.width * scale_factor) / 2
129
- offset_y = -bounds.bottom * scale_factor
130
- def transform_point(point):
131
- x = point.real
132
- y = point.imag
133
- new_x = x * scale_factor + offset_x
134
- new_y = y * scale_factor + offset_y
135
- return Point((new_x, -new_y))
136
- if normalized_path:
137
- start_segment = normalized_path[0]
138
- start_point = transform_point(start_segment.start)
139
- pen.moveTo(start_point)
140
- for segment in normalized_path:
141
- if isinstance(segment, Line):
142
- end_point = transform_point(segment.end)
143
- pen.lineTo(end_point)
144
- elif isinstance(segment, CubicBezier):
145
- control1 = transform_point(segment.control1)
146
- control2 = transform_point(segment.control2)
147
- end_point = transform_point(segment.end)
148
- pen.curveTo(control1, control2, end_point)
149
- elif isinstance(segment, QuadraticBezier):
150
- control = transform_point(segment.control)
151
- end_point = transform_point(segment.end)
152
- # Approximer la courbe quadratique avec une courbe cubique
153
- start_point = transform_point(segment.start)
154
- control1 = start_point + (control - start_point) * 2/3
155
- control2 = end_point + (control - end_point) * 2/3
156
- pen.curveTo(control1, control2, end_point)
157
- if normalized_path and normalized_path[-1].end == normalized_path[0].start:
158
- pen.closePath()
159
- glyph.width = glyph_width if 'glyph_width' in locals() else 1000
160
- print(f"Glyph {letter} created with SVG path.")
161
- except Exception as e:
162
- print(f"Error injecting SVG for {letter}: {e}")
163
- pen.moveTo(Point((100, 0)))
164
- pen.lineTo(Point((900, 0)))
165
- pen.lineTo(Point((900, 1000)))
166
- pen.lineTo(Point((100, 1000)))
167
- pen.closePath()
168
  glyph.width = 1000
169
- font.insertGlyph(glyph)
170
- print(f"Total glyphs in font: {len(font)}")
 
 
 
 
 
 
171
  return font
172
 
173
  def save_otf_font(glyphs, font_name="TipTopType-Regular.otf"):
174
- if not glyphs:
175
- print("No glyphs provided to generate OTF font.")
176
- return None
177
  try:
178
  with tempfile.TemporaryDirectory() as tmpdir:
179
  ufo_path = os.path.join(tmpdir, "font.ufo")
180
  font = build_ufo_from_glyphs(glyphs)
181
  font.save(ufo_path)
182
- print(f"UFO file saved at: {ufo_path}")
 
183
  output_dir = os.path.join(tmpdir, "out")
184
  os.makedirs(output_dir, exist_ok=True)
185
- project = FontProject()
186
- project.run_from_ufos([ufo_path], output=["otf"], output_dir=output_dir)
187
- print("Fontmake ran successfully.")
188
- otf_files = glob.glob(os.path.join(output_dir, "**/*.otf"), recursive=True)
 
 
 
 
 
 
189
  if not otf_files:
190
- print("No OTF files generated by fontmake.")
191
  return None
192
- generated_path = otf_files[0]
193
- print(f"OTF file generated at: {generated_path}")
194
  final_path = os.path.join(tempfile.gettempdir(), font_name)
195
- os.replace(generated_path, final_path)
196
- print(f"OTF file moved to: {final_path}")
197
  return final_path
 
198
  except Exception as e:
199
- print(f"Error in save_otf_font: {e}")
200
  return None
201
 
202
- # ------------------------
203
- # GENERATION
204
- # ------------------------
205
- @spaces.GPU(duration=180)
206
- def respond(message: str, system_message: str, max_tokens: int, temperature: float, top_p: float):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  tokenizer, model = load_model()
208
  if torch.cuda.is_available():
209
  model = model.to('cuda')
210
  model_device = next(model.parameters()).device
 
211
  messages = [{"role": "system", "content": system_message}]
212
  messages.append({"role": "user", "content": message})
 
213
  inputs = tokenizer.apply_chat_template(
214
  messages,
215
  tokenize=True,
216
  add_generation_prompt=True,
217
  return_tensors="pt",
218
  ).to(model_device)
 
219
  streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
220
  generation_kwargs = {
221
  "input_ids": inputs,
@@ -226,12 +161,15 @@ def respond(message: str, system_message: str, max_tokens: int, temperature: flo
226
  "do_sample": True,
227
  "use_cache": True,
228
  }
 
229
  if temperature <= 0.01:
230
  generation_kwargs["do_sample"] = False
231
  generation_kwargs.pop("temperature", None)
232
  generation_kwargs.pop("top_p", None)
 
233
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
234
  thread.start()
 
235
  partial_response = ""
236
  for new_text in streamer:
237
  partial_response += new_text
@@ -239,17 +177,15 @@ def respond(message: str, system_message: str, max_tokens: int, temperature: flo
239
  yield partial_response, glyphs
240
  thread.join()
241
 
242
- # ------------------------
243
- # GRADIO APP
244
- # ------------------------
245
  def create_demo():
246
  with gr.Blocks() as demo:
247
  gr.Markdown("# TypTopType")
248
  glyphs_state = gr.State([])
249
  message_history = gr.State([])
 
250
  with gr.Row():
251
  with gr.Column(scale=1):
252
- msg = gr.Textbox(label="input box, type here")
253
  system_message = gr.Textbox(
254
  value="Based on the following text, give me the svgpath of the glyphs from A to Z.",
255
  visible=False
@@ -260,15 +196,17 @@ def create_demo():
260
  cols = gr.Slider(minimum=1, maximum=10, value=5, step=1, visible=False)
261
  width = gr.Slider(minimum=50, maximum=200, value=100, step=10, visible=False)
262
  height = gr.Slider(minimum=50, maximum=200, value=100, step=10, visible=False)
263
- download_btn = gr.Button("Download svg file")
264
  download_otf_btn = gr.Button("Download OTF font")
265
  with gr.Column(scale=3):
266
- gr.Markdown("## preview")
267
  svg_preview = gr.HTML(label="SVG Preview")
268
  download_output = gr.File(label="Download ZIP")
269
  download_otf_output = gr.File(label="Download OTF")
 
270
  def user(user_message, history):
271
  return "", history + [[user_message, None]]
 
272
  def bot(history, system_message, max_tokens, temperature, top_p, cols, width, height):
273
  message = history[-1][0]
274
  response_generator = respond(message, system_message, max_tokens, temperature, top_p)
@@ -279,25 +217,30 @@ def create_demo():
279
  glyphs_list = glyphs
280
  svg_html = generate_glyphs_html(glyphs_list, cols=cols, width=width, height=height) if glyphs_list else "No glyphs found."
281
  yield svg_html, glyphs_list
 
282
  def download_svg(glyphs, width, height):
283
  if not glyphs:
284
  return None
285
  svg_files = generate_svg_files(glyphs, width=width, height=height)
286
  zip_path = create_zip(svg_files)
287
  return zip_path
 
288
  def download_otf(glyphs):
289
  if not glyphs:
290
  return None
291
  return save_otf_font(glyphs)
 
292
  msg.submit(user, [msg, message_history], [msg, message_history], queue=False).then(
293
  bot, [message_history, system_message, max_tokens, temperature, top_p, cols, width, height], [svg_preview, glyphs_state]
294
  )
 
295
  download_btn.click(
296
  download_svg, inputs=[glyphs_state, width, height], outputs=download_output
297
  )
298
  download_otf_btn.click(
299
  download_otf, inputs=[glyphs_state], outputs=download_otf_output
300
  )
 
301
  return demo
302
 
303
  demo = create_demo()
 
 
 
 
 
 
1
  import re
 
 
 
2
  import os
3
+ import tempfile
4
  import glob
5
+ import subprocess
6
+ import zipfile
7
+ from defcon import Font, Glyph
8
+ from svgpathtools import parse_path, Path, Line, CubicBezier, Move
9
+ import gradio as gr
10
+ from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer
11
+ import torch
12
+ from threading import Thread
13
+
 
14
  def load_model():
15
  tokenizer = AutoTokenizer.from_pretrained("ChevalierJoseph/typtop")
16
  model = AutoModelForCausalLM.from_pretrained("ChevalierJoseph/typtop")
17
  return tokenizer, model
18
 
 
 
 
 
 
 
 
 
 
 
 
19
  def extract_glyphs(text):
20
  pattern = r"Glyph\s+([A-Z])\s+([MmZzLlHhVvCcSsQqTtAa0-9,\s\.\-]+?)(?=\s*Glyph\s+[A-Z]|\s*$)"
21
  glyphs = re.findall(pattern, text)
22
  return glyphs
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  def build_ufo_from_glyphs(glyphs):
25
  font = Font()
26
  font.info.familyName = "TipTopType"
27
  font.info.styleName = "Regular"
28
  font.info.unitsPerEm = 1000
29
  font.info.ascender = 800
30
+ font.info.descender = -300
31
  font.info.capHeight = 700
32
  font.info.xHeight = 500
33
+
34
  for letter, path_data in glyphs:
35
+ print(f"\n--- Processing glyph: {letter} ---")
 
 
 
 
 
 
36
  try:
37
  path = parse_path(path_data)
38
+ if not path:
39
+ print(f"❌ Skipping {letter}: could not parse path")
40
+ continue
41
+
42
+ glyph = Glyph()
43
+ glyph.name = letter
44
+ glyph.unicode = ord(letter)
45
+ pen = glyph.getPen()
46
+
47
  for segment in path:
48
+ if isinstance(segment, Move):
49
+ pen.moveTo((segment.end.real, -segment.end.imag))
50
+ elif isinstance(segment, Line):
51
+ pen.lineTo((segment.end.real, -segment.end.imag))
52
+ elif isinstance(segment, CubicBezier):
53
+ pen.curveTo(
54
+ (segment.control1.real, -segment.control1.imag),
55
+ (segment.control2.real, -segment.control2.imag),
56
+ (segment.end.real, -segment.end.imag)
57
+ )
58
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  glyph.width = 1000
60
+ font.insertGlyph(glyph)
61
+ print(f" Glyph {letter} added successfully")
62
+
63
+ except Exception as e:
64
+ print(f"❌ Error for {letter}: {str(e)}")
65
+ continue
66
+
67
+ print(f"\nTotal glyphs in UFO: {len(font)}")
68
  return font
69
 
70
  def save_otf_font(glyphs, font_name="TipTopType-Regular.otf"):
 
 
 
71
  try:
72
  with tempfile.TemporaryDirectory() as tmpdir:
73
  ufo_path = os.path.join(tmpdir, "font.ufo")
74
  font = build_ufo_from_glyphs(glyphs)
75
  font.save(ufo_path)
76
+ print(f"UFO saved to: {ufo_path}")
77
+
78
  output_dir = os.path.join(tmpdir, "out")
79
  os.makedirs(output_dir, exist_ok=True)
80
+
81
+ result = subprocess.run(
82
+ ["fontmake", "-o", "otf", ufo_path, "--output-dir", output_dir],
83
+ capture_output=True,
84
+ text=True
85
+ )
86
+ print("FontMake output:", result.stdout)
87
+ print("FontMake errors:", result.stderr)
88
+
89
+ otf_files = glob.glob(os.path.join(output_dir, "*.otf"))
90
  if not otf_files:
91
+ print("No OTF generated")
92
  return None
93
+
 
94
  final_path = os.path.join(tempfile.gettempdir(), font_name)
95
+ os.replace(otf_files[0], final_path)
96
+ print(f"OTF saved to: {final_path}")
97
  return final_path
98
+
99
  except Exception as e:
100
+ print(f"Error in save_otf_font: {str(e)}")
101
  return None
102
 
103
+ def generate_svg_files(glyphs, width=100, height=100):
104
+ svg_files = {}
105
+ for lettre, path in glyphs:
106
+ svg_content = f"""
107
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -800 900 900" width="{width}" height="{height}">
108
+ <g transform="translate(0, 0)">
109
+ <path d="{path.strip()}" fill="black"/>
110
+ </g>
111
+ </svg>
112
+ """
113
+ svg_files[f"{lettre}.svg"] = svg_content
114
+ return svg_files
115
+
116
+ def create_zip(svg_files):
117
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp_file:
118
+ zip_path = tmp_file.name
119
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zip_file:
120
+ for filename, content in svg_files.items():
121
+ zip_file.writestr(filename, content)
122
+ return zip_path
123
+
124
+ def generate_glyphs_html(glyphs, cols=5, width=100, height=100):
125
+ html_parts = []
126
+ for lettre, path in glyphs:
127
+ svg_content = f"""
128
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -800 900 900" width="{width}" height="{height}">
129
+ <g transform="translate(0, 0)">
130
+ <path d="{path.strip()}" fill="black"/>
131
+ </g>
132
+ </svg>
133
+ """
134
+ html_parts.append(f"<div style='display: inline-block; margin: 10px; text-align: center;'><h3>{lettre}</h3>{svg_content}</div>")
135
+ grid_style = f"display: grid; grid-template-columns: repeat({cols}, 1fr); gap: 20px;"
136
+ return f'<div style="{grid_style}">{"".join(html_parts)}</div>'
137
+
138
+ def respond(message, system_message, max_tokens, temperature, top_p):
139
  tokenizer, model = load_model()
140
  if torch.cuda.is_available():
141
  model = model.to('cuda')
142
  model_device = next(model.parameters()).device
143
+
144
  messages = [{"role": "system", "content": system_message}]
145
  messages.append({"role": "user", "content": message})
146
+
147
  inputs = tokenizer.apply_chat_template(
148
  messages,
149
  tokenize=True,
150
  add_generation_prompt=True,
151
  return_tensors="pt",
152
  ).to(model_device)
153
+
154
  streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
155
  generation_kwargs = {
156
  "input_ids": inputs,
 
161
  "do_sample": True,
162
  "use_cache": True,
163
  }
164
+
165
  if temperature <= 0.01:
166
  generation_kwargs["do_sample"] = False
167
  generation_kwargs.pop("temperature", None)
168
  generation_kwargs.pop("top_p", None)
169
+
170
  thread = Thread(target=model.generate, kwargs=generation_kwargs)
171
  thread.start()
172
+
173
  partial_response = ""
174
  for new_text in streamer:
175
  partial_response += new_text
 
177
  yield partial_response, glyphs
178
  thread.join()
179
 
 
 
 
180
  def create_demo():
181
  with gr.Blocks() as demo:
182
  gr.Markdown("# TypTopType")
183
  glyphs_state = gr.State([])
184
  message_history = gr.State([])
185
+
186
  with gr.Row():
187
  with gr.Column(scale=1):
188
+ msg = gr.Textbox(label="Input description of the typography")
189
  system_message = gr.Textbox(
190
  value="Based on the following text, give me the svgpath of the glyphs from A to Z.",
191
  visible=False
 
196
  cols = gr.Slider(minimum=1, maximum=10, value=5, step=1, visible=False)
197
  width = gr.Slider(minimum=50, maximum=200, value=100, step=10, visible=False)
198
  height = gr.Slider(minimum=50, maximum=200, value=100, step=10, visible=False)
199
+ download_btn = gr.Button("Download SVG files")
200
  download_otf_btn = gr.Button("Download OTF font")
201
  with gr.Column(scale=3):
202
+ gr.Markdown("## Preview")
203
  svg_preview = gr.HTML(label="SVG Preview")
204
  download_output = gr.File(label="Download ZIP")
205
  download_otf_output = gr.File(label="Download OTF")
206
+
207
  def user(user_message, history):
208
  return "", history + [[user_message, None]]
209
+
210
  def bot(history, system_message, max_tokens, temperature, top_p, cols, width, height):
211
  message = history[-1][0]
212
  response_generator = respond(message, system_message, max_tokens, temperature, top_p)
 
217
  glyphs_list = glyphs
218
  svg_html = generate_glyphs_html(glyphs_list, cols=cols, width=width, height=height) if glyphs_list else "No glyphs found."
219
  yield svg_html, glyphs_list
220
+
221
  def download_svg(glyphs, width, height):
222
  if not glyphs:
223
  return None
224
  svg_files = generate_svg_files(glyphs, width=width, height=height)
225
  zip_path = create_zip(svg_files)
226
  return zip_path
227
+
228
  def download_otf(glyphs):
229
  if not glyphs:
230
  return None
231
  return save_otf_font(glyphs)
232
+
233
  msg.submit(user, [msg, message_history], [msg, message_history], queue=False).then(
234
  bot, [message_history, system_message, max_tokens, temperature, top_p, cols, width, height], [svg_preview, glyphs_state]
235
  )
236
+
237
  download_btn.click(
238
  download_svg, inputs=[glyphs_state, width, height], outputs=download_output
239
  )
240
  download_otf_btn.click(
241
  download_otf, inputs=[glyphs_state], outputs=download_otf_output
242
  )
243
+
244
  return demo
245
 
246
  demo = create_demo()