VirtualOasis commited on
Commit
9b64245
·
verified ·
1 Parent(s): 6ffa77a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -310
app.py CHANGED
@@ -1,10 +1,10 @@
1
- import gradio as gr
2
  import os
 
3
  import tempfile
4
- import traceback
5
- import gc
6
  from PIL import Image, ImageDraw, ImageFont
7
 
 
 
8
  def draw_horizontal_lines(draw, width, height, spacing=60, color=(230, 230, 230)):
9
  """Draws horizontal lines on the image."""
10
  for y in range(0, height, spacing):
@@ -21,330 +21,191 @@ def draw_lattice_grid(draw, width, height, spacing=100, color=(235, 235, 235)):
21
  # Draw vertical lines
22
  for x in range(0, width, spacing):
23
  draw.line([(x, 0), (x, height)], fill=color, width=2)
24
- # Draw horizontal lines
25
  for y in range(0, height, spacing):
26
  draw.line([(0, y), (width, y)], fill=color, width=2)
27
 
28
- def text_to_images_mcp(text_content, style='lines', font_path=None):
 
29
  """
30
- Converts text to images and returns a list of image objects.
31
-
 
32
  Args:
33
  text_content (str): The text to be converted.
34
- style (str): The background style ('plain', 'lines', 'dots', 'grid').
35
  font_path (str, optional): The path to a .ttf font file.
36
-
37
  Returns:
38
- list: List of PIL Image objects for display in Gradio.
39
  """
40
- try:
41
- # Input validation
42
- if not text_content or not text_content.strip():
43
- return []
44
-
45
- # Limit text length to prevent memory issues
46
- if len(text_content) > 10000: # 10k character limit
47
- text_content = text_content[:10000] + "\n\n[Text truncated due to length limit]"
48
-
49
- # --- Configuration ---
50
- IMG_WIDTH = 1080
51
- IMG_HEIGHT = 1080
52
- BACKGROUND_COLOR = (255, 255, 255)
53
- TEXT_COLOR = (10, 10, 10)
54
- STYLE_COLOR = (225, 225, 225)
55
-
56
- PADDING_X = 80
57
- PADDING_Y = 80
58
- FONT_SIZE = 48
59
- LINE_SPACING = 20
60
-
61
- # Limit maximum number of pages to prevent memory issues
62
- MAX_PAGES = 10
63
-
64
- # --- Font Loading ---
65
- font = None
66
- try:
67
- if font_path and os.path.exists(font_path):
68
- font = ImageFont.truetype(font_path, FONT_SIZE)
69
- else:
70
- font_paths_to_try = [
71
- "Arial.ttf", "arial.ttf",
72
- "/System/Library/Fonts/Supplemental/Arial.ttf",
73
- "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
74
- ]
75
- for f_path in font_paths_to_try:
76
- try:
77
- font = ImageFont.truetype(f_path, FONT_SIZE)
78
- break
79
- except (IOError, OSError):
80
- continue
81
- if not font:
82
- font = ImageFont.load_default()
83
- except Exception as e:
84
- print(f"Font loading error: {e}")
85
- font = ImageFont.load_default()
86
-
87
- # --- Text Wrapping Logic ---
88
- drawable_width = IMG_WIDTH - 2 * PADDING_X
89
- paragraphs = [p.strip() for p in text_content.strip().split('\n') if p.strip()]
90
 
91
- if not paragraphs:
92
- return []
 
 
 
93
 
94
- all_lines_and_breaks = []
95
- for i, paragraph in enumerate(paragraphs):
96
- words = paragraph.split()
97
- current_line = ""
98
- for word in words:
 
 
 
 
 
 
 
 
 
 
99
  try:
100
- # Handle very long words
101
- if hasattr(font, 'getlength') and font.getlength(word) > drawable_width:
102
- temp_word = ""
103
- for char in word:
104
- if font.getlength(temp_word + char) > drawable_width:
105
- all_lines_and_breaks.append(temp_word)
106
- temp_word = char
107
- else:
108
- temp_word += char
109
- word = temp_word
110
-
111
- # Check line length
112
- test_line = current_line + " " + word if current_line else word
113
- if hasattr(font, 'getlength'):
114
- line_width = font.getlength(test_line)
115
- else:
116
- line_width = len(test_line) * 12 # Rough estimate
117
-
118
- if line_width <= drawable_width:
119
- current_line = test_line
120
- else:
121
- if current_line:
122
- all_lines_and_breaks.append(current_line.strip())
123
- current_line = word
124
- except Exception as e:
125
- print(f"Text wrapping error: {e}")
126
- # Fallback to simple character limit
127
- if len(current_line + " " + word) <= 80: # Rough character limit
128
- current_line += " " + word
129
- else:
130
- all_lines_and_breaks.append(current_line.strip())
131
- current_line = word
132
-
133
- if current_line:
134
- all_lines_and_breaks.append(current_line.strip())
135
- if i < len(paragraphs) - 1:
136
- all_lines_and_breaks.append(None) # Paragraph break
137
-
138
- # --- Image Generation ---
139
- img_count = 0
140
- page_content = []
141
- y_text = PADDING_Y
142
-
143
- try:
144
- if hasattr(font, 'getbbox'):
145
- line_height = font.getbbox("A")[3] - font.getbbox("A")[1]
146
- else:
147
- line_height = FONT_SIZE
148
- except Exception:
149
- line_height = FONT_SIZE
150
-
151
- PARAGRAPH_SPACING = line_height
152
- generated_images = []
153
-
154
- def create_image_page(content, page_num):
155
- """Helper function to create and return a single image page."""
156
- try:
157
- img = Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), color=BACKGROUND_COLOR)
158
- draw = ImageDraw.Draw(img)
159
-
160
- # Draw the selected background style first
161
- if style == 'lines':
162
- line_style_spacing = line_height + LINE_SPACING
163
- draw_horizontal_lines(draw, IMG_WIDTH, IMG_HEIGHT, spacing=line_style_spacing, color=STYLE_COLOR)
164
- elif style == 'dots':
165
- draw_dot_grid(draw, IMG_WIDTH, IMG_HEIGHT, color=STYLE_COLOR)
166
- elif style == 'grid':
167
- draw_lattice_grid(draw, IMG_WIDTH, IMG_HEIGHT, color=STYLE_COLOR)
168
 
169
- # Draw the text on top of the background
170
- current_y = PADDING_Y
171
- for page_item in content:
172
- if page_item is not None:
173
- try:
174
- draw.text((PADDING_X, current_y), page_item, font=font, fill=TEXT_COLOR)
175
- current_y += line_height + LINE_SPACING
176
- except Exception as e:
177
- print(f"Text drawing error: {e}")
178
- current_y += line_height + LINE_SPACING
 
 
 
 
 
 
179
  else:
180
- current_y += PARAGRAPH_SPACING
181
-
182
- # Optimize image for web delivery
183
- img = img.convert('RGB')
184
- generated_images.append(img)
185
- return img
186
- except Exception as e:
187
- print(f"Image creation error: {e}")
188
- # Return a simple error image
189
- error_img = Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), color=(255, 255, 255))
190
- error_draw = ImageDraw.Draw(error_img)
191
- try:
192
- error_draw.text((PADDING_X, PADDING_Y), f"Error creating page {page_num}", font=font, fill=(255, 0, 0))
193
- except:
194
- pass
195
- return error_img
196
-
197
- # Process content with page limit
198
- for item in all_lines_and_breaks:
199
- if img_count >= MAX_PAGES:
200
- break
201
-
202
- is_break = item is None
203
- item_height = PARAGRAPH_SPACING if is_break else line_height
204
-
205
- if y_text + item_height > IMG_HEIGHT - PADDING_Y:
206
- img_count += 1
207
- create_image_page(page_content, img_count)
208
 
209
- page_content = [item]
210
- y_text = PADDING_Y + item_height + (0 if is_break else LINE_SPACING)
211
  else:
212
- page_content.append(item)
213
- y_text += item_height + (0 if is_break else LINE_SPACING)
214
-
215
- if page_content and img_count < MAX_PAGES:
216
- img_count += 1
217
- create_image_page(page_content, img_count)
218
-
219
- # Memory cleanup
220
- gc.collect()
221
-
222
- return generated_images[:MAX_PAGES] # Ensure we don't exceed limit
223
 
224
- except Exception as e:
225
- print(f"Critical error in text_to_images_mcp: {e}")
226
- print(traceback.format_exc())
227
- # Return a simple error image
228
- try:
229
- error_img = Image.new('RGB', (1080, 1080), color=(255, 255, 255))
230
- error_draw = ImageDraw.Draw(error_img)
231
- error_draw.text((80, 80), f"Error processing text: {str(e)[:100]}", fill=(255, 0, 0))
232
- return [error_img]
233
- except:
234
- return []
235
-
236
- # Sample text for demonstration
237
- sample_text = """In the heart of a bustling city, there lived a clockmaker named Alistair. His shop, a quaint corner of tranquility amidst the urban chaos, was filled with the gentle ticking of countless timepieces. Each clock was a masterpiece, a testament to his dedication and skill. But Alistair held a secret. One of his clocks, an old grandfather clock in the corner, did not just tell time. It told stories.
238
-
239
- Every midnight, as the city slept, the clock would chime, not with bells, but with whispers of forgotten tales. Stories of ancient kings, lost love, and adventures in lands woven from starlight and dreams. Alistair would sit by the fire, listening, his heart filled with the magic of the past. He was the keeper of time, and in turn, time had made him its confidant.
240
-
241
- One day, a young girl with eyes as curious as a cat's wandered into his shop. She wasn't interested in the shiny new watches but was drawn to the grandfather clock. "What's its story?" she asked, her voice soft. Alistair smiled, for he knew he had found the next guardian of the stories. The legacy of the whispering clock would live on."""
242
-
243
- def create_app():
244
- """Create and configure the Gradio app with better error handling."""
245
 
246
- # Create the demo with enhanced error handling
247
- demo = gr.Interface(
248
- fn=text_to_images_mcp,
249
- inputs=[
250
- gr.Textbox(
251
- value=sample_text,
252
- lines=10,
253
- max_lines=20,
254
- label="Text Content",
255
- placeholder="Enter your long-form text here (max 10,000 characters)..."
256
- ),
257
- gr.Dropdown(
258
- choices=['plain', 'lines', 'dots', 'grid'],
259
- value='lines',
260
- label="Background Style",
261
- info="Choose the background style for your images"
262
- ),
263
- gr.Textbox(
264
- value="",
265
- label="Custom Font Path (Optional)",
266
- placeholder="/path/to/your/font.ttf",
267
- info="Leave empty to use system default font"
268
- )
269
- ],
270
- outputs=[
271
- gr.Gallery(
272
- label="Generated Images (Max 10 pages)",
273
- show_label=True,
274
- elem_id="gallery",
275
- columns=2,
276
- rows=2,
277
- object_fit="contain",
278
- height="auto",
279
- show_download_button=True
280
- )
281
- ],
282
- title="📖 Text to Images Converter (MCP Compatible)",
283
- description="Transform long-form text into a series of attractive, readable images. Simply paste your text, choose a background style, and preview the generated images. You can download individual images by clicking on them. Limited to 10,000 characters and 10 pages for optimal performance. This app functions as an MCP server for LLM integration.",
284
- examples=[
285
- [sample_text, 'lines', ''],
286
- [sample_text, 'dots', ''],
287
- [sample_text, 'grid', ''],
288
- [sample_text, 'plain', '']
289
- ],
290
- cache_examples=False,
291
- show_api=True
292
- )
293
 
294
- return demo
295
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  if __name__ == "__main__":
297
- # Set up environment for MCP server
298
- os.environ["GRADIO_MCP_SERVER"] = "True"
299
-
300
- # Create the app
301
- demo = create_app()
302
-
303
- # Launch with multiple fallback strategies
304
- launch_configs = [
305
- # Strategy 1: Full MCP server with explicit settings
306
- {
307
- "mcp_server": True,
308
- "server_name": "0.0.0.0",
309
- "server_port": 7860,
310
- "share": False,
311
- "debug": False,
312
- "show_error": False,
313
- "quiet": True
314
- },
315
- # Strategy 2: Environment variable only
316
- {
317
- "server_name": "0.0.0.0",
318
- "server_port": 7860,
319
- "share": False,
320
- "debug": False,
321
- "show_error": False,
322
- "quiet": True
323
- },
324
- # Strategy 3: Minimal configuration
325
- {
326
- "server_name": "0.0.0.0",
327
- "server_port": 7860,
328
- "share": False
329
- },
330
- # Strategy 4: Default configuration
331
- {}
332
- ]
333
-
334
- launched = False
335
- for i, config in enumerate(launch_configs):
336
- try:
337
- print(f"Attempting launch strategy {i+1}...")
338
- demo.launch(**config)
339
- launched = True
340
- print(f"Successfully launched with strategy {i+1}")
341
- break
342
- except Exception as e:
343
- print(f"Launch strategy {i+1} failed: {e}")
344
- if i < len(launch_configs) - 1:
345
- print("Trying next strategy...")
346
- continue
347
-
348
- if not launched:
349
- print("All launch strategies failed. Please check your environment and dependencies.")
350
- print("You may need to install gradio[mcp] or update your gradio version.")
 
 
1
  import os
2
+ import gradio as gr
3
  import tempfile
 
 
4
  from PIL import Image, ImageDraw, ImageFont
5
 
6
+ # --- Image Generation Logic (from coverter.py) ---
7
+
8
  def draw_horizontal_lines(draw, width, height, spacing=60, color=(230, 230, 230)):
9
  """Draws horizontal lines on the image."""
10
  for y in range(0, height, spacing):
 
21
  # Draw vertical lines
22
  for x in range(0, width, spacing):
23
  draw.line([(x, 0), (x, height)], fill=color, width=2)
24
+ # Draw horizontal lines
25
  for y in range(0, height, spacing):
26
  draw.line([(0, y), (width, y)], fill=color, width=2)
27
 
28
+
29
+ def text_to_images_generator(text_content, style='lines', font_path=None):
30
  """
31
+ Converts a given string of text into a series of images and returns their file paths.
32
+ This version is adapted for Gradio to return image paths from a temporary directory.
33
+
34
  Args:
35
  text_content (str): The text to be converted.
36
+ style (str, optional): The background style ('plain', 'lines', 'dots', 'grid').
37
  font_path (str, optional): The path to a .ttf font file.
38
+
39
  Returns:
40
+ list: A list of file paths for the generated images.
41
  """
42
+ if not text_content or not text_content.strip():
43
+ # Return empty list and show a warning if there is no text
44
+ gr.Warning("Input text is empty. Please enter some text to generate images.")
45
+ return []
46
+
47
+ # --- Configuration ---
48
+ IMG_WIDTH = 1080
49
+ IMG_HEIGHT = 1080
50
+ BACKGROUND_COLOR = (255, 255, 255)
51
+ TEXT_COLOR = (10, 10, 10)
52
+ STYLE_COLOR = (225, 225, 225) # Color for lines/dots/grid
53
+
54
+ PADDING_X = 80
55
+ PADDING_Y = 80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ FONT_SIZE = 48
58
+ LINE_SPACING = 20
59
+
60
+ # Create a temporary directory to save images for the Gradio gallery
61
+ output_dir = tempfile.mkdtemp()
62
 
63
+ # --- Font Loading ---
64
+ font = None
65
+ try:
66
+ # Prioritize provided font path if it exists
67
+ if font_path and os.path.exists(font_path):
68
+ font = ImageFont.truetype(font_path, FONT_SIZE)
69
+ else:
70
+ # Otherwise, search for common system fonts
71
+ font_paths_to_try = [
72
+ "Arial.ttf", "arial.ttf", "DejaVuSans.ttf",
73
+ "/System/Library/Fonts/Supplemental/Arial.ttf",
74
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
75
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
76
+ ]
77
+ for f_path in font_paths_to_try:
78
  try:
79
+ font = ImageFont.truetype(f_path, FONT_SIZE)
80
+ break
81
+ except IOError:
82
+ continue
83
+ # Fallback to default if no font is found
84
+ if not font:
85
+ gr.Warning("Could not find a standard .ttf font. Falling back to the basic default font. Text may look pixelated.")
86
+ font = ImageFont.load_default()
87
+ except Exception as e:
88
+ print(f"An unexpected error occurred during font loading: {e}")
89
+ font = ImageFont.load_default()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ # --- Text Wrapping Logic ---
92
+ drawable_width = IMG_WIDTH - 2 * PADDING_X
93
+ paragraphs = [p.strip() for p in text_content.strip().split('\n') if p.strip()]
94
+
95
+ all_lines_and_breaks = []
96
+ for i, paragraph in enumerate(paragraphs):
97
+ words = paragraph.split()
98
+ current_line = ""
99
+ for word in words:
100
+ # Handle very long words by breaking them up
101
+ if font.getlength(word) > drawable_width:
102
+ temp_word = ""
103
+ for char in word:
104
+ if font.getlength(temp_word + char) > drawable_width:
105
+ all_lines_and_breaks.append(temp_word)
106
+ temp_word = char
107
  else:
108
+ temp_word += char
109
+ word = temp_word
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ if font.getlength(current_line + " " + word) <= drawable_width:
112
+ current_line += " " + word
113
  else:
114
+ all_lines_and_breaks.append(current_line.strip())
115
+ current_line = word
116
+ all_lines_and_breaks.append(current_line.strip())
 
 
 
 
 
 
 
 
117
 
118
+ # Add a placeholder for paragraph breaks
119
+ if i < len(paragraphs) - 1:
120
+ all_lines_and_breaks.append(None)
121
+
122
+ # --- Image Generation ---
123
+ generated_files = []
124
+ img_count = 0
125
+ page_content = []
126
+ y_text = PADDING_Y
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ try:
129
+ # Get line height from font metrics if possible
130
+ line_height = font.getbbox("A")[3] - font.getbbox("A")[1]
131
+ except AttributeError:
132
+ # Fallback for default font
133
+ line_height = 12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
+ PARAGRAPH_SPACING = line_height
136
+
137
+ def create_image_page(content, page_num):
138
+ """Helper function to create and save a single image page."""
139
+ img = Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), color=BACKGROUND_COLOR)
140
+ draw = ImageDraw.Draw(img)
141
+
142
+ # Draw the selected background style first
143
+ if style == 'lines':
144
+ line_style_spacing = line_height + LINE_SPACING
145
+ draw_horizontal_lines(draw, IMG_WIDTH, IMG_HEIGHT, spacing=line_style_spacing, color=STYLE_COLOR)
146
+ elif style == 'dots':
147
+ draw_dot_grid(draw, IMG_WIDTH, IMG_HEIGHT, color=STYLE_COLOR)
148
+ elif style == 'grid':
149
+ draw_lattice_grid(draw, IMG_WIDTH, IMG_HEIGHT, color=STYLE_COLOR)
150
+
151
+ # Draw the text on top of the background
152
+ current_y = PADDING_Y
153
+ for page_item in content:
154
+ if page_item is not None:
155
+ draw.text((PADDING_X, current_y), page_item, font=font, fill=TEXT_COLOR)
156
+ current_y += line_height + LINE_SPACING
157
+ else: # This is a paragraph break
158
+ current_y += PARAGRAPH_SPACING
159
+
160
+ filename = os.path.join(output_dir, f"page_{page_num}.png")
161
+ img.save(filename)
162
+ generated_files.append(filename)
163
+
164
+ # Iterate through all lines and create pages
165
+ for item in all_lines_and_breaks:
166
+ is_break = item is None
167
+ item_height = PARAGRAPH_SPACING if is_break else line_height
168
+
169
+ # If adding the next line/break exceeds page height, create the current page
170
+ if y_text + item_height > IMG_HEIGHT - PADDING_Y:
171
+ img_count += 1
172
+ create_image_page(page_content, img_count)
173
+ # Start a new page
174
+ page_content = [item]
175
+ y_text = PADDING_Y + item_height + (0 if is_break else LINE_SPACING)
176
+ else:
177
+ page_content.append(item)
178
+ y_text += item_height + (0 if is_break else LINE_SPACING)
179
+
180
+ # Save the last page if it has content
181
+ if page_content:
182
+ img_count += 1
183
+ create_image_page(page_content, img_count)
184
+
185
+ return generated_files
186
+
187
+ # --- Gradio Interface ---
188
+
189
+ # Example text to pre-fill the textbox
190
+ example_text = """In the heart of a bustling city, there lived a clockmaker named Alistair. His shop, a quaint corner of tranquility amidst the urban chaos, was filled with the gentle ticking of countless timepieces. Each clock was a masterpiece, a testament to his dedication and skill.
191
+
192
+ One day, a young girl with eyes as curious as a cat's wandered into his shop. She wasn't interested in the shiny new watches but was drawn to the grandfather clock in the corner. "What's its story?" she asked, her voice soft. Alistair smiled, for he knew he had found the next guardian of the stories. The legacy of the whispering clock would live on."""
193
+
194
+ # Define the Gradio interface
195
+ demo = gr.Interface(
196
+ fn=text_to_images_generator,
197
+ inputs=[
198
+ gr.Textbox(lines=15, label="Text Content", placeholder="Paste your long-form text here...", value=example_text),
199
+ gr.Radio(['lines', 'dots', 'grid', 'plain'], label="Background Style", value='lines')
200
+ ],
201
+ outputs=gr.Gallery(label="Generated Images", show_label=True, preview=True),
202
+ title="Text-to-Image Converter",
203
+ description="Transforms long-form text into a series of attractive, readable images. Paste your text, choose a style, and click 'Submit'. You can download the images from the gallery below.",
204
+ allow_flagging="never"
205
+ )
206
+
207
+ # --- Main Execution ---
208
  if __name__ == "__main__":
209
+ # Launch the Gradio app as an MCP server
210
+ # The MCP server allows other applications to call this Gradio app as an API
211
+ demo.launch(mcp_server=True)