ysharma HF Staff commited on
Commit
dfd203c
Β·
verified Β·
1 Parent(s): e175c55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -134
app.py CHANGED
@@ -5,6 +5,8 @@ from huggingface_hub import login
5
  from sentence_transformers import SentenceTransformer, util
6
 
7
  # --- CONFIGURATION ---
 
 
8
  class Config:
9
  """Configuration settings for the application."""
10
  EMBEDDING_MODEL_ID = "google/embeddinggemma-300M"
@@ -12,7 +14,10 @@ class Config:
12
  TOP_K = 5
13
  HF_TOKEN = os.getenv('HF_TOKEN')
14
 
 
15
  # --- FONT DATA ---
 
 
16
  FONT_DATA = [
17
  {
18
  "name": "Playfair Display",
@@ -208,7 +213,10 @@ FONT_DATA = [
208
  }
209
  ]
210
 
 
211
  # --- CORE LOGIC ---
 
 
212
  class FontMoodGenerator:
213
  """Handles model loading, embedding generation, and font palette creation."""
214
 
@@ -227,6 +235,7 @@ class FontMoodGenerator:
227
  login(token=self.config.HF_TOKEN)
228
  else:
229
  print("HF_TOKEN not found. Proceeding without login.")
 
230
 
231
  def _load_model(self) -> SentenceTransformer:
232
  """Loads the Sentence Transformer model."""
@@ -251,32 +260,12 @@ class FontMoodGenerator:
251
  print("Embeddings computed successfully.")
252
  return embeddings
253
 
254
- def _create_persistent_font_imports(self, top_hits: list[dict[str, any]]) -> str:
255
- """Creates persistent font imports that work across all steps."""
256
- if not top_hits:
257
- return ""
258
-
259
- imports = []
260
- seen_urls = set()
261
-
262
- for hit in top_hits:
263
- font_info = self.font_data[hit['corpus_id']]
264
- google_fonts_url = font_info['google_fonts_url']
265
-
266
- if google_fonts_url not in seen_urls:
267
- imports.append(f"@import url('{google_fonts_url}');")
268
- seen_urls.add(google_fonts_url)
269
-
270
- return "\n".join(imports)
271
-
272
  def _format_palette_as_html(self, top_hits: list[dict[str, any]]) -> str:
273
- """Formats the top font hits into a displayable HTML string with embedded font imports."""
274
  if not top_hits:
275
  return "<p>Could not generate a font palette. Please try another mood.</p>"
276
 
277
- # Get font imports for the selected fonts
278
- font_imports = self._create_persistent_font_imports(top_hits)
279
-
280
  sample_texts = [
281
  "The Quick Brown Fox Jumps Over The Lazy Dog",
282
  "Sphinx of black quartz, judge my vow",
@@ -308,72 +297,39 @@ class FontMoodGenerator:
308
  </div>
309
  </div>
310
  """
311
-
312
- # Include font imports directly in the HTML with unique ID to prevent conflicts
313
- return f"""
314
- <style id="palette-fonts">
315
- {font_imports}
316
- </style>
317
- <div class='font-palette-container'>{cards_html}</div>
318
- """
319
-
320
- def generate_palette(self, mood_text: str) -> tuple[str, list[dict[str, any]]]:
321
- """Generates font palette and returns both HTML and raw data."""
322
- if not mood_text or not mood_text.strip():
323
- return "<p>Please enter a mood or a description.</p>", []
324
-
325
- mood_embedding = self.embedding_model.encode(
326
- mood_text,
327
- prompt_name=self.config.PROMPT_NAME
328
- )
329
- top_hits = util.semantic_search(
330
- mood_embedding, self.font_embeddings, top_k=self.config.TOP_K
331
- )[0]
332
 
333
- palette_html = self._format_palette_as_html(top_hits)
334
- return palette_html, top_hits
335
-
336
- def generate_css_code(self, top_hits: list[dict[str, any]]) -> str:
337
- """Generates exportable CSS code."""
338
  if not top_hits:
339
- return "/* No fonts generated yet */"
340
-
341
- font_imports = self._create_persistent_font_imports(top_hits)
342
-
343
- css_code = f"""/* Generated Font Palette CSS */
344
- {font_imports}
345
 
346
- /* Font Variables */
347
- :root {{"""
348
 
349
- for i, hit in enumerate(top_hits):
350
  font_info = self.font_data[hit['corpus_id']]
351
- font_name = font_info['name']
352
- css_code += f"""
353
- --font-{i+1}: '{font_name}', {font_info['family']};"""
354
-
355
- css_code += """
356
- }
357
 
358
- /* Usage Examples */
359
- .heading { font-family: var(--font-1); }
360
- .body-text { font-family: var(--font-2); }
361
- .accent { font-family: var(--font-3); }"""
362
-
363
- return css_code
364
 
365
- def apply_theme_css(self, top_hits: list[dict[str, any]]) -> str:
366
- """Generates CSS to apply fonts to the UI while preserving palette fonts."""
367
  if not top_hits:
368
  return ""
369
 
370
- font_imports = self._create_persistent_font_imports(top_hits)
371
 
372
  css_rules = []
373
 
 
374
  if len(top_hits) >= 1:
375
  primary_font = self.font_data[top_hits[0]['corpus_id']]['name'].replace("'", "\\'")
376
- css_rules.append(f"h1, h2, h3:not(.font-card h3), .gr-button-primary {{ font-family: '{primary_font}', sans-serif !important; }}")
377
 
378
  if len(top_hits) >= 2:
379
  secondary_font = self.font_data[top_hits[1]['corpus_id']]['name'].replace("'", "\\'")
@@ -385,16 +341,12 @@ class FontMoodGenerator:
385
 
386
  css_rules_str = "\n ".join(css_rules)
387
 
388
- css = f"""<style id="theme-fonts">
389
  {font_imports}
390
 
391
  {css_rules_str}
392
 
393
- /* Preserve palette fonts */
394
- .font-sample {{
395
- font-family: inherit !important;
396
- }}
397
-
398
  * {{
399
  transition: font-family 0.3s ease-in-out;
400
  }}
@@ -402,12 +354,71 @@ class FontMoodGenerator:
402
 
403
  return css
404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
  # --- GRADIO UI WITH WALKTHROUGH ---
 
407
  def create_ui(generator: FontMoodGenerator):
408
  """Creates the Gradio web interface with Walkthrough."""
409
 
 
 
 
410
  with gr.Blocks(theme="ocean") as demo:
 
 
411
 
412
  gr.Markdown("""
413
  # πŸ“ Font Mood Generator
@@ -425,7 +436,7 @@ def create_ui(generator: FontMoodGenerator):
425
  """)
426
 
427
  mood_input = gr.Textbox(
428
- value="Horror movie poster with scary atmosphere",
429
  label="Enter Your Mood or Scene",
430
  info="Examples: 'Modern tech startup', 'Playful children's book', 'Gothic horror movie'",
431
  lines=3
@@ -437,27 +448,22 @@ def create_ui(generator: FontMoodGenerator):
437
  "Modern tech startup with clean aesthetics",
438
  "Playful children's book with whimsical characters",
439
  "Horror movie poster with scary atmosphere",
440
- "Luxury fashion brand with sophisticated appeal"
 
441
  ],
442
  inputs=mood_input,
443
  )
444
 
445
  generate_btn = gr.Button("Generate Font Palette β†’", variant="primary", size="lg")
446
 
447
- # Hidden outputs to store results
448
- palette_html_hidden = gr.HTML(visible=False)
449
- font_data_hidden = gr.JSON(visible=False)
450
-
451
  def generate_and_move(mood_text):
452
  palette_html, top_hits = generator.generate_palette(mood_text)
453
- # Convert to serializable format
454
- font_data_json = [{"corpus_id": hit["corpus_id"], "score": hit["score"]} for hit in top_hits]
455
- return palette_html, font_data_json, gr.Walkthrough(selected=1)
456
 
457
  generate_btn.click(
458
  fn=generate_and_move,
459
  inputs=mood_input,
460
- outputs=[palette_html_hidden, font_data_hidden, walkthrough]
461
  )
462
 
463
  # STEP 2: Review Generated Fonts
@@ -465,10 +471,21 @@ def create_ui(generator: FontMoodGenerator):
465
  gr.Markdown("""
466
  ### Step 2: Review your generated fonts
467
  Here are the fonts that best match your mood, ranked by similarity score.
 
468
  """)
469
 
470
  palette_display = gr.HTML()
471
 
 
 
 
 
 
 
 
 
 
 
472
  with gr.Row():
473
  back_to_input_btn = gr.Button("← Back to Input", variant="secondary")
474
  apply_theme_btn = gr.Button("Apply Typography Theme β†’", variant="primary", size="lg")
@@ -478,51 +495,29 @@ def create_ui(generator: FontMoodGenerator):
478
  outputs=walkthrough
479
  )
480
 
481
- # Update display when entering this step
482
- def show_generated_palette(palette_html):
483
- return palette_html
484
-
485
- palette_html_hidden.change(
486
- fn=show_generated_palette,
487
- inputs=palette_html_hidden,
488
- outputs=palette_display
489
- )
490
-
491
- # Hidden CSS output for theming
492
- theme_css_hidden = gr.HTML(visible=False)
493
-
494
- def apply_theme_and_move(font_data_json):
495
- # Convert back to the format expected by apply_theme_css
496
- top_hits = [{"corpus_id": item["corpus_id"], "score": item["score"]} for item in font_data_json]
497
- theme_css = generator.apply_theme_css(top_hits)
498
  return theme_css, gr.Walkthrough(selected=2)
499
 
500
  apply_theme_btn.click(
501
- fn=apply_theme_and_move,
502
- inputs=font_data_hidden,
503
- outputs=[theme_css_hidden, walkthrough]
504
  )
505
 
506
  # STEP 3: Experience the Typography
507
  with gr.Step("✨ Experience Your Typography", id=2):
508
  gr.Markdown("""
509
  ### Step 3: See your fonts in action!
510
- Notice how the entire interface has transformed to reflect your chosen aesthetic.
 
511
  """)
512
 
513
- # Apply CSS when entering this step
514
- theme_css_display = gr.HTML()
515
-
516
- theme_css_hidden.change(
517
- fn=lambda css: css,
518
- inputs=theme_css_hidden,
519
- outputs=theme_css_display
520
- )
521
-
522
  gr.Markdown("""
523
  **πŸŽ‰ Your typography theme is now active!**
524
 
525
- Look around the interface - the headings, buttons, and text inputs now use fonts from your generated palette.
 
526
 
527
  **Font Roles:**
528
  - **Primary Font**: Used for headings and primary buttons
@@ -554,20 +549,17 @@ def create_ui(generator: FontMoodGenerator):
554
  css_code_output = gr.Code(
555
  language="css",
556
  label="Your Font Palette CSS",
557
- value="/* Generate a palette first to see CSS code here */",
558
  lines=15
559
  )
560
 
561
- # Update CSS code when font data changes
562
- def update_css_code(font_data_json):
563
- if not font_data_json:
564
- return "/* Generate a palette first to see CSS code here */"
565
- top_hits = [{"corpus_id": item["corpus_id"], "score": item["score"]} for item in font_data_json]
566
  return generator.generate_css_code(top_hits)
567
 
568
- font_data_hidden.change(
569
- fn=update_css_code,
570
- inputs=font_data_hidden,
 
571
  outputs=css_code_output
572
  )
573
 
@@ -577,19 +569,22 @@ def create_ui(generator: FontMoodGenerator):
577
  2. Include it in your website's stylesheet
578
  3. Apply the font variables to your HTML elements
579
  4. Enjoy your new typography!
 
 
580
  """)
581
 
582
- start_over_btn = gr.Button("πŸ”„ Start Over", variant="secondary", size="lg")
583
-
 
584
  def restart():
585
- return "", [], "", "", gr.Walkthrough(selected=0)
586
 
587
  start_over_btn.click(
588
  fn=restart,
589
- outputs=[palette_html_hidden, font_data_hidden, theme_css_hidden, theme_css_display, walkthrough]
590
  )
591
 
592
- # Static CSS for font cards (always loaded)
593
  gr.HTML("""
594
  <style>
595
  .font-palette-container {
@@ -634,9 +629,30 @@ def create_ui(generator: FontMoodGenerator):
634
  .font-description {
635
  color: #5d6d7e; font-size: 0.9em; line-height: 1.4;
636
  }
 
 
 
 
 
 
637
  </style>
638
  """)
639
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  return demo
641
 
642
 
 
5
  from sentence_transformers import SentenceTransformer, util
6
 
7
  # --- CONFIGURATION ---
8
+ # Centralized place for all settings and constants.
9
+
10
  class Config:
11
  """Configuration settings for the application."""
12
  EMBEDDING_MODEL_ID = "google/embeddinggemma-300M"
 
14
  TOP_K = 5
15
  HF_TOKEN = os.getenv('HF_TOKEN')
16
 
17
+
18
  # --- FONT DATA ---
19
+ # Comprehensive font dataset with descriptions for mood matching.
20
+
21
  FONT_DATA = [
22
  {
23
  "name": "Playfair Display",
 
213
  }
214
  ]
215
 
216
+
217
  # --- CORE LOGIC ---
218
+ # Encapsulated in a class to manage state (model, embeddings) cleanly.
219
+
220
  class FontMoodGenerator:
221
  """Handles model loading, embedding generation, and font palette creation."""
222
 
 
235
  login(token=self.config.HF_TOKEN)
236
  else:
237
  print("HF_TOKEN not found. Proceeding without login.")
238
+ print("Note: This may fail if the model is gated.")
239
 
240
  def _load_model(self) -> SentenceTransformer:
241
  """Loads the Sentence Transformer model."""
 
260
  print("Embeddings computed successfully.")
261
  return embeddings
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  def _format_palette_as_html(self, top_hits: list[dict[str, any]]) -> str:
264
+ """Formats the top font hits into a displayable HTML string."""
265
  if not top_hits:
266
  return "<p>Could not generate a font palette. Please try another mood.</p>"
267
 
268
+ # Sample texts for different font types
 
 
269
  sample_texts = [
270
  "The Quick Brown Fox Jumps Over The Lazy Dog",
271
  "Sphinx of black quartz, judge my vow",
 
297
  </div>
298
  </div>
299
  """
300
+ return f"<div class='font-palette-container'>{cards_html}</div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
+ def _create_font_imports_css(self, top_hits: list[dict[str, any]]) -> str:
303
+ """Generates CSS imports for the selected fonts."""
 
 
 
304
  if not top_hits:
305
+ return ""
 
 
 
 
 
306
 
307
+ imports = []
308
+ seen_urls = set()
309
 
310
+ for hit in top_hits:
311
  font_info = self.font_data[hit['corpus_id']]
312
+ google_fonts_url = font_info['google_fonts_url']
313
+
314
+ if google_fonts_url not in seen_urls:
315
+ imports.append(f"@import url('{google_fonts_url}');")
316
+ seen_urls.add(google_fonts_url)
 
317
 
318
+ return "\n".join(imports)
 
 
 
 
 
319
 
320
+ def _create_dynamic_theme_css(self, top_hits: list[dict[str, any]]) -> str:
321
+ """Generates a <style> block to apply fonts to different UI elements."""
322
  if not top_hits:
323
  return ""
324
 
325
+ font_imports = self._create_font_imports_css(top_hits)
326
 
327
  css_rules = []
328
 
329
+ # Apply different fonts to different elements
330
  if len(top_hits) >= 1:
331
  primary_font = self.font_data[top_hits[0]['corpus_id']]['name'].replace("'", "\\'")
332
+ css_rules.append(f"h1, h2, h3, .gr-button-primary {{ font-family: '{primary_font}', sans-serif !important; }}")
333
 
334
  if len(top_hits) >= 2:
335
  secondary_font = self.font_data[top_hits[1]['corpus_id']]['name'].replace("'", "\\'")
 
341
 
342
  css_rules_str = "\n ".join(css_rules)
343
 
344
+ css = f"""<style>
345
  {font_imports}
346
 
347
  {css_rules_str}
348
 
349
+ /* Custom animations for font changes */
 
 
 
 
350
  * {{
351
  transition: font-family 0.3s ease-in-out;
352
  }}
 
354
 
355
  return css
356
 
357
+ def _create_css_code_output(self, top_hits: list[dict[str, any]]) -> str:
358
+ """Creates exportable CSS code for users."""
359
+ if not top_hits:
360
+ return ""
361
+
362
+ font_imports = self._create_font_imports_css(top_hits)
363
+
364
+ css_code = f"""/* Generated Font Palette CSS */
365
+ {font_imports}
366
+
367
+ /* Font Variables */
368
+ :root {{"""
369
+
370
+ for i, hit in enumerate(top_hits):
371
+ font_info = self.font_data[hit['corpus_id']]
372
+ font_name = font_info['name']
373
+ css_code += f"""
374
+ --font-{i+1}: '{font_name}', {font_info['family']};"""
375
+
376
+ css_code += """
377
+ }
378
+
379
+ /* Usage Examples */
380
+ .heading { font-family: var(--font-1); }
381
+ .body-text { font-family: var(--font-2); }
382
+ .accent { font-family: var(--font-3); }"""
383
+
384
+ return css_code
385
+
386
+ def generate_palette(self, mood_text: str) -> tuple[str, list[dict[str, any]]]:
387
+ """Generates font palette and returns both HTML and raw data."""
388
+ if not mood_text or not mood_text.strip():
389
+ return "<p>Please enter a mood or a description.</p>", []
390
+
391
+ mood_embedding = self.embedding_model.encode(
392
+ mood_text,
393
+ prompt_name=self.config.PROMPT_NAME
394
+ )
395
+ top_hits = util.semantic_search(
396
+ mood_embedding, self.font_embeddings, top_k=self.config.TOP_K
397
+ )[0]
398
+
399
+ palette_html = self._format_palette_as_html(top_hits)
400
+ return palette_html, top_hits
401
+
402
+ def apply_theme(self, top_hits: list[dict[str, any]]) -> str:
403
+ """Applies the theme CSS."""
404
+ return self._create_dynamic_theme_css(top_hits)
405
+
406
+ def generate_css_code(self, top_hits: list[dict[str, any]]) -> str:
407
+ """Generates exportable CSS code."""
408
+ return self._create_css_code_output(top_hits)
409
+
410
 
411
  # --- GRADIO UI WITH WALKTHROUGH ---
412
+
413
  def create_ui(generator: FontMoodGenerator):
414
  """Creates the Gradio web interface with Walkthrough."""
415
 
416
+ # Shared state to store generated fonts across steps
417
+ font_state = gr.State([])
418
+
419
  with gr.Blocks(theme="ocean") as demo:
420
+ # Dynamic CSS output
421
+ dynamic_css_output = gr.HTML()
422
 
423
  gr.Markdown("""
424
  # πŸ“ Font Mood Generator
 
436
  """)
437
 
438
  mood_input = gr.Textbox(
439
+ value="Elegant wedding invitation with vintage charm",
440
  label="Enter Your Mood or Scene",
441
  info="Examples: 'Modern tech startup', 'Playful children's book', 'Gothic horror movie'",
442
  lines=3
 
448
  "Modern tech startup with clean aesthetics",
449
  "Playful children's book with whimsical characters",
450
  "Horror movie poster with scary atmosphere",
451
+ "Luxury fashion brand with sophisticated appeal",
452
+ "Retro 1950s diner with nostalgic vibes",
453
  ],
454
  inputs=mood_input,
455
  )
456
 
457
  generate_btn = gr.Button("Generate Font Palette β†’", variant="primary", size="lg")
458
 
 
 
 
 
459
  def generate_and_move(mood_text):
460
  palette_html, top_hits = generator.generate_palette(mood_text)
461
+ return palette_html, top_hits, gr.Walkthrough(selected=1)
 
 
462
 
463
  generate_btn.click(
464
  fn=generate_and_move,
465
  inputs=mood_input,
466
+ outputs=[gr.State(), font_state, walkthrough]
467
  )
468
 
469
  # STEP 2: Review Generated Fonts
 
471
  gr.Markdown("""
472
  ### Step 2: Review your generated fonts
473
  Here are the fonts that best match your mood, ranked by similarity score.
474
+ Each font is scored based on how well it captures your described aesthetic.
475
  """)
476
 
477
  palette_display = gr.HTML()
478
 
479
+ def show_palette(palette_html):
480
+ return palette_html
481
+
482
+ # Update palette when entering this step
483
+ demo.load(
484
+ fn=show_palette,
485
+ inputs=gr.State(),
486
+ outputs=palette_display
487
+ )
488
+
489
  with gr.Row():
490
  back_to_input_btn = gr.Button("← Back to Input", variant="secondary")
491
  apply_theme_btn = gr.Button("Apply Typography Theme β†’", variant="primary", size="lg")
 
495
  outputs=walkthrough
496
  )
497
 
498
+ def apply_and_move(top_hits):
499
+ theme_css = generator.apply_theme(top_hits)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  return theme_css, gr.Walkthrough(selected=2)
501
 
502
  apply_theme_btn.click(
503
+ fn=apply_and_move,
504
+ inputs=font_state,
505
+ outputs=[dynamic_css_output, walkthrough]
506
  )
507
 
508
  # STEP 3: Experience the Typography
509
  with gr.Step("✨ Experience Your Typography", id=2):
510
  gr.Markdown("""
511
  ### Step 3: See your fonts in action!
512
+ Notice how the entire interface has transformed to reflect your chosen aesthetic.
513
+ The fonts from your palette are now applied throughout the UI.
514
  """)
515
 
 
 
 
 
 
 
 
 
 
516
  gr.Markdown("""
517
  **πŸŽ‰ Your typography theme is now active!**
518
 
519
+ Look around the interface - the headings, buttons, and text inputs now use fonts from your generated palette.
520
+ This gives you a real preview of how these fonts work together in a design context.
521
 
522
  **Font Roles:**
523
  - **Primary Font**: Used for headings and primary buttons
 
549
  css_code_output = gr.Code(
550
  language="css",
551
  label="Your Font Palette CSS",
552
+ value="",
553
  lines=15
554
  )
555
 
556
+ def generate_css_code(top_hits):
 
 
 
 
557
  return generator.generate_css_code(top_hits)
558
 
559
+ # Generate CSS when entering this step
560
+ demo.load(
561
+ fn=generate_css_code,
562
+ inputs=font_state,
563
  outputs=css_code_output
564
  )
565
 
 
569
  2. Include it in your website's stylesheet
570
  3. Apply the font variables to your HTML elements
571
  4. Enjoy your new typography!
572
+
573
+ **πŸ’‘ Pro Tip:** You can also use individual Google Fonts links if you prefer to load fonts separately.
574
  """)
575
 
576
+ with gr.Row():
577
+ start_over_btn = gr.Button("πŸ”„ Start Over", variant="secondary", size="lg")
578
+
579
  def restart():
580
+ return "", [], gr.Walkthrough(selected=0)
581
 
582
  start_over_btn.click(
583
  fn=restart,
584
+ outputs=[dynamic_css_output, font_state, walkthrough]
585
  )
586
 
587
+ # Static CSS for font cards
588
  gr.HTML("""
589
  <style>
590
  .font-palette-container {
 
629
  .font-description {
630
  color: #5d6d7e; font-size: 0.9em; line-height: 1.4;
631
  }
632
+
633
+ /* Walkthrough styling enhancements */
634
+ .gr-walkthrough {
635
+ border-radius: 12px;
636
+ overflow: hidden;
637
+ }
638
  </style>
639
  """)
640
 
641
+ gr.Markdown("""
642
+ ----
643
+ ## About This App
644
+
645
+ This **Font Mood Generator** uses the new `gr.Walkthrough` component to create a guided, step-by-step experience for generating typography palettes.
646
+
647
+ The app is powered by [**EmbeddingGemma**](http://huggingface.co/google/embeddinggemma-300M), Google's text embedding model that understands the semantic meaning and emotional qualities of your descriptions to find matching fonts.
648
+
649
+ **πŸ†• Walkthrough Features:**
650
+ - **Guided Experience**: Step-by-step workflow for better user experience
651
+ - **Progressive Disclosure**: Information and controls revealed when needed
652
+ - **Visual Progress**: Clear indication of current step and progress
653
+ - **Interactive Navigation**: Ability to go back and forth between steps
654
+ """)
655
+
656
  return demo
657
 
658