Milhaud commited on
Commit
e1e1f64
·
1 Parent(s): aac83c6

fix: Fix typos and improve security with XSS protection

Browse files

- Fix typos in variable names and UI text
- Add HTML escaping to prevent XSS attacks
- Standardize function return types and error handling
- Refactor HTML generation into separate functions

Files changed (1) hide show
  1. app.py +95 -78
app.py CHANGED
@@ -7,6 +7,7 @@ from dotenv import load_dotenv
7
  from utils import svg_to_png_base64
8
  import pathlib
9
  from logger import setup_logger
 
10
 
11
  load_dotenv(override=True)
12
  logger = setup_logger()
@@ -93,7 +94,8 @@ class SVGAnimationGenerator:
93
  decomposed_svg_match = re.search(
94
  r"<decomposed_svg>(.*?)</decomposed_svg>", response_text, re.DOTALL
95
  )
96
- animation_suggenstions_match = re.search(
 
97
  r"<animation_suggestions>(.*?)</animation_suggestions>",
98
  response_text,
99
  re.DOTALL,
@@ -101,9 +103,9 @@ class SVGAnimationGenerator:
101
  print(
102
  "[SVG Decompose] Decomposed SVG found", decomposed_svg_match is not None
103
  )
104
- if decomposed_svg_match and animation_suggenstions_match:
105
  decomposed_svg_text = decomposed_svg_match.group(1).strip()
106
- animation_suggestions = animation_suggenstions_match.group(1).strip()
107
  print("[SVG Decompose] Animation suggestions found")
108
  return {
109
  "svg_content": decomposed_svg_text,
@@ -119,11 +121,11 @@ class SVGAnimationGenerator:
119
  def feedback_decompose_group(self, svg_content: str, feedback: str) -> tuple:
120
  try:
121
  # Parse the SVG content first
122
- parsed_svg = self.parse_svg(svg_content) # Remove ["svg_content"] access
123
  if "error" in parsed_svg:
124
  error_message = parsed_svg["error"]
125
  error_html = create_error_html(error_message)
126
- return "", "", error_html # Return tuple of 3 values
127
 
128
  prompt = self.feedback_decompose_group_prompt.format(
129
  parsed_svg=parsed_svg, feedback=feedback
@@ -148,38 +150,31 @@ class SVGAnimationGenerator:
148
  decomposed_svg_match = re.search(
149
  r"<decomposed_svg>(.*?)</decomposed_svg>", response_text, re.DOTALL
150
  )
151
- animation_suggenstions_match = re.search(
 
152
  r"<animation_suggestions>(.*?)</animation_suggestions>",
153
  response_text,
154
  re.DOTALL,
155
  )
156
 
157
- if decomposed_svg_match and animation_suggenstions_match:
158
  decomposed_svg_text = decomposed_svg_match.group(1).strip()
159
- animation_suggestions = animation_suggenstions_match.group(1).strip()
160
 
161
- # Create viewer HTML
162
- viewer_html = f"""
163
- <div style='padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: block;'>
164
- <div style='display: block; align-items: center; margin-bottom: 10px;'>
165
- </div>
166
- <div id='animation-container' style='min-height: 300px; display: block; justify-content: center; align-items: center; background: #fafafa; border-radius: 4px; padding: 20px;'>
167
- {decomposed_svg_text}
168
- </div>
169
- </div>
170
- """
171
 
172
- return decomposed_svg_text, animation_suggestions, viewer_html # Return tuple of 3 values
173
  else:
174
  error_message = "Decomposed SVG and Animation Suggestion not found in response."
175
  error_html = create_error_html(error_message)
176
- return "", "", error_html # Return tuple of 3 values
177
  except Exception as e:
178
  error_message = f"Error during MLLM feedback prediction: {e}"
179
  error_html = create_error_html(error_message)
180
- return "", "", error_html # Return tuple of 3 values
181
 
182
- def generate_animation(self, proposed_animation: str, svg_content: str) -> str:
183
  try:
184
  prompt = self.generate_animation_prompt.format(
185
  svg_content=svg_content, proposed_animation=proposed_animation
@@ -194,15 +189,54 @@ class SVGAnimationGenerator:
194
  )
195
  response_text = response.content[0].text
196
  logger.info(f"Model Response:\n{response_text}")
197
- return response_text
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
  except Exception as e:
200
- return f"<html><body><h3>Error generating animation: {e}</h3></body></html>"
 
201
 
202
 
203
  generator = SVGAnimationGenerator()
204
 
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  def process_svg(svg_file):
207
  if svg_file is None:
208
  return "Please upload an SVG file"
@@ -222,7 +256,7 @@ def process_svg(svg_file):
222
  def predict_decompose_group(svg_file, svg_text, object_name):
223
  if not object_name.strip():
224
  error_msg = "Please enter a valid object name for the SVG"
225
- error_html = (error_msg)
226
  return "", error_msg, "", error_html
227
 
228
  if svg_file is not None:
@@ -251,45 +285,32 @@ def predict_decompose_group(svg_file, svg_text, object_name):
251
  animation_suggestions = decompose_result["animation_suggestions"]
252
 
253
  # Create viewer HTML
254
- decomposed_svg_viewer = f"""
255
- <div style='padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: block;'>
256
- <div style='display: block; align-items: center; margin-bottom: 10px;'>
257
- </div>
258
- <div id='animation-container' style='min-height: 300px; display: block; justify-content: center; align-items: center; background: #fafafa; border-radius: 4px; padding: 20px;'>
259
- {decomposed_svg}
260
- </div>
261
- </div>
262
- """
263
 
264
  return (
265
  decomposed_svg, # For svg_content_hidden
266
- decomposed_svg, # For groups_summary (분석 결과 표시)
267
  animation_suggestions, # For animation_suggestion
268
  decomposed_svg_viewer, # For decomposed_svg_viewer
269
  )
270
 
271
- def create_animation_preview(animation_desc: str, svg_content: str) -> str:
 
272
  """Create animation preview from description and SVG content."""
273
  if not svg_content.strip():
274
- return create_error_html("⚠️ Please process SVG first")
 
275
 
276
  if not animation_desc.strip():
277
- return create_error_html("⚠️ Please describe the animation you want")
 
278
 
279
  try:
280
- animation_html = generator.generate_animation(animation_desc, svg_content)
281
- if not animation_html:
282
- return create_error_html("❌ Failed to generate animation")
283
-
284
- # Extract HTML content from Claude's response
285
- html_match = re.search(
286
- r"<html_output>(.*?)</html_output>", animation_html, re.DOTALL
287
- )
288
- if not html_match:
289
- return create_error_html("❌ Invalid animation HTML format")
290
-
291
- # Get the actual HTML content
292
- html_content = html_match.group(1).strip()
293
 
294
  # Save the HTML content to the output directory
295
  output_dir = "output"
@@ -299,35 +320,27 @@ def create_animation_preview(animation_desc: str, svg_content: str) -> str:
299
  f.write(html_content)
300
  print(f"Animation preview saved to: {html_path}")
301
 
302
- html_path = pathlib.Path("output/animation_preview.html").read_text(
303
- encoding="utf-8"
304
- )
305
-
306
- # Wrap in a container with preview styling
307
- return f"""
308
  <div style='padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: block;'>
309
  <div style='display: block; align-items: center; margin-bottom: 10px;'>
310
  </div>
311
  <div id='animation-container' style='min-height: 300px; display: block; justify-content: center; align-items: center; background: #fafafa; border-radius: 4px; padding: 20px;'>
312
- <iframe srcdoc="{html_path.replace('"', '&quot;')}"
313
- width="100%" height="600px"
314
- style="border:none; overflow:hidden;"
315
- sandbox="allow-scripts allow-same-origin">
316
- </iframe>
317
  </div>
318
  </div>
319
  """
 
 
 
320
  except Exception as e:
321
- return create_error_html(f"❌ Error creating animation: {str(e)}")
322
-
323
-
324
- def create_error_html(message: str) -> str:
325
- """Create formatted error message HTML."""
326
- return f"""
327
- <div style='padding: 40px; text-align: center; color: #666; border: 2px dashed #ddd; border-radius: 10px;'>
328
- <h3>{message}</h3>
329
- </div>
330
- """
331
 
332
 
333
  # Define examples with proper path handling and categories
@@ -448,7 +461,7 @@ with demo:
448
  value="""
449
  <div style='padding: 40px; text-align: center; color: #666; border: 2px dashed #ddd; border-radius: 10px;'>
450
  <div id='animation-container' style='min-height: 150px; display: flex; justify-content: center; align-items: center; border-radius: 4px; padding: 10px;'>
451
- <div style='color: #999; text-align: center;'>Amination preview will appear here</div>
452
  </div>
453
  </div>
454
  """,
@@ -461,10 +474,11 @@ with demo:
461
  output_html = gr.Textbox(
462
  label="Output HTML",
463
  lines=10,
464
- placeholder="Generated HTML will be saved here.",
465
  )
466
 
467
- process_btn.click(
 
468
  fn=predict_decompose_group,
469
  inputs=[svg_file, svg_text, object_name],
470
  outputs=[
@@ -474,6 +488,7 @@ with demo:
474
  decomposed_svg_viewer, # Show SVG preview
475
  ],
476
  )
 
477
  groups_feedback_btn.click(
478
  fn=generator.feedback_decompose_group,
479
  inputs=[
@@ -486,6 +501,7 @@ with demo:
486
  decomposed_svg_viewer, # Update SVG preview
487
  ],
488
  )
 
489
  animate_btn.click(
490
  fn=create_animation_preview,
491
  inputs=[
@@ -493,9 +509,10 @@ with demo:
493
  svg_content_hidden,
494
  ],
495
  outputs=[
496
- animation_preview,
497
- output_html
498
  ],
499
  )
 
500
  if __name__ == "__main__":
501
- demo.launch(share=True)
 
7
  from utils import svg_to_png_base64
8
  import pathlib
9
  from logger import setup_logger
10
+ import html
11
 
12
  load_dotenv(override=True)
13
  logger = setup_logger()
 
94
  decomposed_svg_match = re.search(
95
  r"<decomposed_svg>(.*?)</decomposed_svg>", response_text, re.DOTALL
96
  )
97
+ # 오타 수정: suggenstions → suggestions
98
+ animation_suggestions_match = re.search(
99
  r"<animation_suggestions>(.*?)</animation_suggestions>",
100
  response_text,
101
  re.DOTALL,
 
103
  print(
104
  "[SVG Decompose] Decomposed SVG found", decomposed_svg_match is not None
105
  )
106
+ if decomposed_svg_match and animation_suggestions_match:
107
  decomposed_svg_text = decomposed_svg_match.group(1).strip()
108
+ animation_suggestions = animation_suggestions_match.group(1).strip()
109
  print("[SVG Decompose] Animation suggestions found")
110
  return {
111
  "svg_content": decomposed_svg_text,
 
121
  def feedback_decompose_group(self, svg_content: str, feedback: str) -> tuple:
122
  try:
123
  # Parse the SVG content first
124
+ parsed_svg = self.parse_svg(svg_content)
125
  if "error" in parsed_svg:
126
  error_message = parsed_svg["error"]
127
  error_html = create_error_html(error_message)
128
+ return "", "", error_html
129
 
130
  prompt = self.feedback_decompose_group_prompt.format(
131
  parsed_svg=parsed_svg, feedback=feedback
 
150
  decomposed_svg_match = re.search(
151
  r"<decomposed_svg>(.*?)</decomposed_svg>", response_text, re.DOTALL
152
  )
153
+ # 오타 수정: suggenstions → suggestions
154
+ animation_suggestions_match = re.search(
155
  r"<animation_suggestions>(.*?)</animation_suggestions>",
156
  response_text,
157
  re.DOTALL,
158
  )
159
 
160
+ if decomposed_svg_match and animation_suggestions_match:
161
  decomposed_svg_text = decomposed_svg_match.group(1).strip()
162
+ animation_suggestions = animation_suggestions_match.group(1).strip()
163
 
164
+ # Create viewer HTML with XSS protection
165
+ viewer_html = create_svg_viewer_html(decomposed_svg_text)
 
 
 
 
 
 
 
 
166
 
167
+ return decomposed_svg_text, animation_suggestions, viewer_html
168
  else:
169
  error_message = "Decomposed SVG and Animation Suggestion not found in response."
170
  error_html = create_error_html(error_message)
171
+ return "", "", error_html
172
  except Exception as e:
173
  error_message = f"Error during MLLM feedback prediction: {e}"
174
  error_html = create_error_html(error_message)
175
+ return "", "", error_html
176
 
177
+ def generate_animation(self, proposed_animation: str, svg_content: str) -> tuple:
178
  try:
179
  prompt = self.generate_animation_prompt.format(
180
  svg_content=svg_content, proposed_animation=proposed_animation
 
189
  )
190
  response_text = response.content[0].text
191
  logger.info(f"Model Response:\n{response_text}")
192
+
193
+ # Extract HTML content from Claude's response
194
+ html_match = re.search(
195
+ r"<html_output>(.*?)</html_output>", response_text, re.DOTALL
196
+ )
197
+ if html_match:
198
+ html_content = html_match.group(1).strip()
199
+ return response_text, html_content
200
+ else:
201
+ return response_text, ""
202
+ else:
203
+ error_msg = "Anthropic API client not initialized"
204
+ return f"<html><body><h3>Error: {error_msg}</h3></body></html>", ""
205
 
206
  except Exception as e:
207
+ error_msg = f"Error generating animation: {e}"
208
+ return f"<html><body><h3>{error_msg}</h3></body></html>", ""
209
 
210
 
211
  generator = SVGAnimationGenerator()
212
 
213
 
214
+ def create_error_html(message: str) -> str:
215
+ """Create formatted error message HTML with XSS protection."""
216
+ safe_message = html.escape(str(message))
217
+ return f"""
218
+ <div style='padding: 40px; text-align: center; color: #666; border: 2px dashed #ddd; border-radius: 10px;'>
219
+ <h3>{safe_message}</h3>
220
+ </div>
221
+ """
222
+
223
+
224
+ def create_svg_viewer_html(svg_content: str) -> str:
225
+ """Create SVG viewer HTML with XSS protection."""
226
+ # Basic sanitization - in production, use a proper SVG sanitizer
227
+ safe_svg = html.escape(svg_content).replace('&lt;', '<').replace('&gt;', '>')
228
+
229
+ return f"""
230
+ <div style='padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: block;'>
231
+ <div style='display: block; align-items: center; margin-bottom: 10px;'>
232
+ </div>
233
+ <div id='animation-container' style='min-height: 300px; display: block; justify-content: center; align-items: center; background: #fafafa; border-radius: 4px; padding: 20px;'>
234
+ {safe_svg}
235
+ </div>
236
+ </div>
237
+ """
238
+
239
+
240
  def process_svg(svg_file):
241
  if svg_file is None:
242
  return "Please upload an SVG file"
 
256
  def predict_decompose_group(svg_file, svg_text, object_name):
257
  if not object_name.strip():
258
  error_msg = "Please enter a valid object name for the SVG"
259
+ error_html = create_error_html(error_msg)
260
  return "", error_msg, "", error_html
261
 
262
  if svg_file is not None:
 
285
  animation_suggestions = decompose_result["animation_suggestions"]
286
 
287
  # Create viewer HTML
288
+ decomposed_svg_viewer = create_svg_viewer_html(decomposed_svg)
 
 
 
 
 
 
 
 
289
 
290
  return (
291
  decomposed_svg, # For svg_content_hidden
292
+ decomposed_svg, # For groups_summary (분석 결과 표시)
293
  animation_suggestions, # For animation_suggestion
294
  decomposed_svg_viewer, # For decomposed_svg_viewer
295
  )
296
 
297
+
298
+ def create_animation_preview(animation_desc: str, svg_content: str) -> tuple:
299
  """Create animation preview from description and SVG content."""
300
  if not svg_content.strip():
301
+ error_html = create_error_html("⚠️ Please process SVG first")
302
+ return error_html, ""
303
 
304
  if not animation_desc.strip():
305
+ error_html = create_error_html("⚠️ Please describe the animation you want")
306
+ return error_html, ""
307
 
308
  try:
309
+ animation_response, html_content = generator.generate_animation(animation_desc, svg_content)
310
+
311
+ if not html_content:
312
+ error_html = create_error_html("❌ Failed to generate animation HTML")
313
+ return error_html, animation_response
 
 
 
 
 
 
 
 
314
 
315
  # Save the HTML content to the output directory
316
  output_dir = "output"
 
320
  f.write(html_content)
321
  print(f"Animation preview saved to: {html_path}")
322
 
323
+ # Create iframe HTML with XSS protection
324
+ safe_html_content = html.escape(html_content).replace('"', '&quot;')
325
+ preview_html = f"""
 
 
 
326
  <div style='padding: 20px; background: #fff; border: 1px solid #eee; border-radius: 8px; display: block;'>
327
  <div style='display: block; align-items: center; margin-bottom: 10px;'>
328
  </div>
329
  <div id='animation-container' style='min-height: 300px; display: block; justify-content: center; align-items: center; background: #fafafa; border-radius: 4px; padding: 20px;'>
330
+ <iframe srcdoc="{safe_html_content}"
331
+ width="100%" height="600px"
332
+ style="border:none; overflow:hidden;"
333
+ sandbox="allow-scripts allow-same-origin">
334
+ </iframe>
335
  </div>
336
  </div>
337
  """
338
+
339
+ return preview_html, html_content
340
+
341
  except Exception as e:
342
+ error_html = create_error_html(f"❌ Error creating animation: {str(e)}")
343
+ return error_html, ""
 
 
 
 
 
 
 
 
344
 
345
 
346
  # Define examples with proper path handling and categories
 
461
  value="""
462
  <div style='padding: 40px; text-align: center; color: #666; border: 2px dashed #ddd; border-radius: 10px;'>
463
  <div id='animation-container' style='min-height: 150px; display: flex; justify-content: center; align-items: center; border-radius: 4px; padding: 10px;'>
464
+ <div style='color: #999; text-align: center;'>Animation preview will appear here</div>
465
  </div>
466
  </div>
467
  """,
 
474
  output_html = gr.Textbox(
475
  label="Output HTML",
476
  lines=10,
477
+ placeholder="Generated HTML will appear here.",
478
  )
479
 
480
+ # Event handlers
481
+ process_btn.click(
482
  fn=predict_decompose_group,
483
  inputs=[svg_file, svg_text, object_name],
484
  outputs=[
 
488
  decomposed_svg_viewer, # Show SVG preview
489
  ],
490
  )
491
+
492
  groups_feedback_btn.click(
493
  fn=generator.feedback_decompose_group,
494
  inputs=[
 
501
  decomposed_svg_viewer, # Update SVG preview
502
  ],
503
  )
504
+
505
  animate_btn.click(
506
  fn=create_animation_preview,
507
  inputs=[
 
509
  svg_content_hidden,
510
  ],
511
  outputs=[
512
+ animation_preview, # Animation preview HTML
513
+ output_html, # Raw HTML output
514
  ],
515
  )
516
+
517
  if __name__ == "__main__":
518
+ demo.launch(share=True)