uto1125 commited on
Commit
c65ba9d
·
verified ·
1 Parent(s): 75963c5

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +647 -0
app.py ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import queue
3
+ from huggingface_hub import snapshot_download
4
+ import hydra
5
+ import numpy as np
6
+ import wave
7
+ import io
8
+ import pyrootutils
9
+ import gc
10
+
11
+ # Download if not exists
12
+ os.makedirs("checkpoints", exist_ok=True)
13
+ snapshot_download(repo_id="fishaudio/fish-speech-1.4", local_dir="./checkpoints/fish-speech-1.4")
14
+
15
+ print("All checkpoints downloaded")
16
+
17
+ import html
18
+ import os
19
+ import threading
20
+ from argparse import ArgumentParser
21
+ from pathlib import Path
22
+ from functools import partial
23
+
24
+ import gradio as gr
25
+ import librosa
26
+ import torch
27
+ import torchaudio
28
+
29
+ torchaudio.set_audio_backend("soundfile")
30
+
31
+ from loguru import logger
32
+ from transformers import AutoTokenizer
33
+
34
+ from tools.llama.generate import launch_thread_safe_queue
35
+ from tools.vqgan.inference import load_model as load_vqgan_model
36
+ from fish_speech.text.chn_text_norm.text import Text as ChnNormedText
37
+ from tools.api import decode_vq_tokens, encode_reference
38
+ from tools.auto_rerank import batch_asr, calculate_wer, is_chinese, load_model
39
+ from tools.llama.generate import (
40
+ GenerateRequest,
41
+ GenerateResponse,
42
+ WrappedGenerateResponse,
43
+ launch_thread_safe_queue,
44
+ )
45
+ from tools.vqgan.inference import load_model as load_decoder_model
46
+
47
+ # Make einx happy
48
+ os.environ["EINX_FILTER_TRACEBACK"] = "false"
49
+
50
+
51
+ HEADER_MD = """# Fish Speech
52
+ ## The demo in this space is version 1.4, Please check [Fish Audio](https://fish.audio) for the best model.
53
+ ## 该 Demo 为 Fish Speech 1.4 版本, 请在 [Fish Audio](https://fish.audio) 体验最新 DEMO.
54
+ A text-to-speech model based on VQ-GAN and Llama developed by [Fish Audio](https://fish.audio).
55
+ 由 [Fish Audio](https://fish.audio) 研发的基于 VQ-GAN 和 Llama 的多语种语音合成.
56
+ You can find the source code [here](https://github.com/fishaudio/fish-speech) and models [here](https://huggingface.co/fishaudio/fish-speech-1.4).
57
+ 你可以在 [这里](https://github.com/fishaudio/fish-speech) 找到源代码和 [这里](https://huggingface.co/fishaudio/fish-speech-1.4) 找到模型.
58
+ Related code and weights are released under CC BY-NC-SA 4.0 License.
59
+ 相关代码,权重使用 CC BY-NC-SA 4.0 许可证发布.
60
+ We are not responsible for any misuse of the model, please consider your local laws and regulations before using it.
61
+ 我们不对模型的任何滥用负责,请在使用之前考虑您当地的法律法规.
62
+ The model running in this WebUI is Fish Speech V1.4 Medium.
63
+ 在此 WebUI 中运行的模型是 Fish Speech V1.4 Medium.
64
+ """
65
+
66
+ TEXTBOX_PLACEHOLDER = """Put your text here. 在此处输入文本."""
67
+
68
+ try:
69
+ import spaces
70
+
71
+ GPU_DECORATOR = spaces.GPU
72
+ except ImportError:
73
+
74
+ def GPU_DECORATOR(func):
75
+ def wrapper(*args, **kwargs):
76
+ return func(*args, **kwargs)
77
+
78
+ return wrapper
79
+
80
+
81
+ def build_html_error_message(error):
82
+ return f"""
83
+ <div style="color: red;
84
+ font-weight: bold;">
85
+ {html.escape(error)}
86
+ </div>
87
+ """
88
+
89
+
90
+ @GPU_DECORATOR
91
+ @torch.inference_mode()
92
+ def inference(
93
+ text,
94
+ enable_reference_audio,
95
+ reference_audio,
96
+ reference_text,
97
+ max_new_tokens,
98
+ chunk_length,
99
+ top_p,
100
+ repetition_penalty,
101
+ temperature,
102
+ streaming=False
103
+ ):
104
+ if args.max_gradio_length > 0 and len(text) > args.max_gradio_length:
105
+ return (
106
+ None,
107
+ None,
108
+ "Text is too long, please keep it under {} characters.".format(
109
+ args.max_gradio_length
110
+ ),
111
+ )
112
+
113
+ # Parse reference audio aka prompt
114
+ prompt_tokens = encode_reference(
115
+ decoder_model=decoder_model,
116
+ reference_audio=reference_audio,
117
+ enable_reference_audio=enable_reference_audio,
118
+ )
119
+
120
+ # LLAMA Inference
121
+ request = dict(
122
+ device=decoder_model.device,
123
+ max_new_tokens=max_new_tokens,
124
+ text=text,
125
+ top_p=top_p,
126
+ repetition_penalty=repetition_penalty,
127
+ temperature=temperature,
128
+ compile=args.compile,
129
+ iterative_prompt=chunk_length > 0,
130
+ chunk_length=chunk_length,
131
+ max_length=2048,
132
+ prompt_tokens=prompt_tokens if enable_reference_audio else None,
133
+ prompt_text=reference_text if enable_reference_audio else None,
134
+ )
135
+
136
+ response_queue = queue.Queue()
137
+ llama_queue.put(
138
+ GenerateRequest(
139
+ request=request,
140
+ response_queue=response_queue,
141
+ )
142
+ )
143
+
144
+ segments = []
145
+
146
+ while True:
147
+ result: WrappedGenerateResponse = response_queue.get()
148
+ if result.status == "error":
149
+ return None, None, build_html_error_message(result.response)
150
+
151
+ result: GenerateResponse = result.response
152
+ if result.action == "next":
153
+ break
154
+
155
+ with torch.autocast(
156
+ device_type=(
157
+ "cpu"
158
+ if decoder_model.device.type == "mps"
159
+ else decoder_model.device.type
160
+ ),
161
+ dtype=args.precision,
162
+ ):
163
+ fake_audios = decode_vq_tokens(
164
+ decoder_model=decoder_model,
165
+ codes=result.codes,
166
+ )
167
+
168
+ fake_audios = fake_audios.float().cpu().numpy()
169
+ segments.append(fake_audios)
170
+
171
+ if len(segments) == 0:
172
+ return (
173
+ None,
174
+ None,
175
+ build_html_error_message(
176
+ "No audio generated, please check the input text."
177
+ ),
178
+ )
179
+
180
+ # Return the final audio
181
+ audio = np.concatenate(segments, axis=0)
182
+ return None, (decoder_model.spec_transform.sample_rate, audio), None
183
+
184
+ if torch.cuda.is_available():
185
+ torch.cuda.empty_cache()
186
+ gc.collect()
187
+
188
+
189
+ def inference_with_auto_rerank(
190
+ text,
191
+ enable_reference_audio,
192
+ reference_audio,
193
+ reference_text,
194
+ max_new_tokens,
195
+ chunk_length,
196
+ top_p,
197
+ repetition_penalty,
198
+ temperature,
199
+ use_auto_rerank,
200
+ streaming=False,
201
+ ):
202
+ max_attempts = 2 if use_auto_rerank else 1
203
+ best_wer = float("inf")
204
+ best_audio = None
205
+ best_sample_rate = None
206
+
207
+ for attempt in range(max_attempts):
208
+ _, (sample_rate, audio), message = inference(
209
+ text,
210
+ enable_reference_audio,
211
+ reference_audio,
212
+ reference_text,
213
+ max_new_tokens,
214
+ chunk_length,
215
+ top_p,
216
+ repetition_penalty,
217
+ temperature,
218
+ streaming=False,
219
+ )
220
+
221
+ if audio is None:
222
+ return None, None, message
223
+
224
+ if not use_auto_rerank:
225
+ return None, (sample_rate, audio), None
226
+
227
+ asr_result = batch_asr(asr_model, [audio], sample_rate)[0]
228
+ wer = calculate_wer(text, asr_result["text"])
229
+
230
+ if wer <= 0.3 and not asr_result["huge_gap"]:
231
+ return None, (sample_rate, audio), None
232
+
233
+ if wer < best_wer:
234
+ best_wer = wer
235
+ best_audio = audio
236
+ best_sample_rate = sample_rate
237
+
238
+ if attempt == max_attempts - 1:
239
+ break
240
+
241
+ return None, (best_sample_rate, best_audio), None
242
+
243
+
244
+ n_audios = 4
245
+
246
+ global_audio_list = []
247
+ global_error_list = []
248
+
249
+ def inference_wrapper(
250
+ text,
251
+ enable_reference_audio,
252
+ reference_audio,
253
+ reference_text,
254
+ max_new_tokens,
255
+ chunk_length,
256
+ top_p,
257
+ repetition_penalty,
258
+ temperature,
259
+ batch_infer_num,
260
+ if_load_asr_model,
261
+ ):
262
+ audios = []
263
+ errors = []
264
+
265
+ for _ in range(batch_infer_num):
266
+ result = inference_with_auto_rerank(
267
+ text,
268
+ enable_reference_audio,
269
+ reference_audio,
270
+ reference_text,
271
+ max_new_tokens,
272
+ chunk_length,
273
+ top_p,
274
+ repetition_penalty,
275
+ temperature,
276
+ if_load_asr_model,
277
+ )
278
+
279
+ _, audio_data, error_message = result
280
+
281
+ audios.append(
282
+ gr.Audio(value=audio_data if audio_data else None, visible=True),
283
+ )
284
+ errors.append(
285
+ gr.HTML(value=error_message if error_message else None, visible=True),
286
+ )
287
+
288
+ for _ in range(batch_infer_num, n_audios):
289
+ audios.append(
290
+ gr.Audio(value=None, visible=False),
291
+ )
292
+ errors.append(
293
+ gr.HTML(value=None, visible=False),
294
+ )
295
+
296
+ return None, *audios, *errors
297
+
298
+
299
+ def wav_chunk_header(sample_rate=44100, bit_depth=16, channels=1):
300
+ buffer = io.BytesIO()
301
+
302
+ with wave.open(buffer, "wb") as wav_file:
303
+ wav_file.setnchannels(channels)
304
+ wav_file.setsampwidth(bit_depth // 8)
305
+ wav_file.setframerate(sample_rate)
306
+
307
+ wav_header_bytes = buffer.getvalue()
308
+ buffer.close()
309
+ return wav_header_bytes
310
+
311
+
312
+ def normalize_text(user_input, use_normalization):
313
+ if use_normalization:
314
+ return ChnNormedText(raw_text=user_input).normalize()
315
+ else:
316
+ return user_input
317
+
318
+
319
+ asr_model = None
320
+
321
+
322
+ def change_if_load_asr_model(if_load):
323
+ global asr_model
324
+
325
+ if if_load:
326
+ gr.Warning("Loading faster whisper model...")
327
+ if asr_model is None:
328
+ asr_model = load_model()
329
+ return gr.Checkbox(label="Unload faster whisper model", value=if_load)
330
+
331
+ if if_load is False:
332
+ gr.Warning("Unloading faster whisper model...")
333
+ del asr_model
334
+ asr_model = None
335
+ if torch.cuda.is_available():
336
+ torch.cuda.empty_cache()
337
+ gc.collect()
338
+ return gr.Checkbox(label="Load faster whisper model", value=if_load)
339
+
340
+
341
+ def change_if_auto_label(if_load, if_auto_label, enable_ref, ref_audio, ref_text):
342
+ if if_load and asr_model is not None:
343
+ if (
344
+ if_auto_label
345
+ and enable_ref
346
+ and ref_audio is not None
347
+ and ref_text.strip() == ""
348
+ ):
349
+ data, sample_rate = librosa.load(ref_audio)
350
+ res = batch_asr(asr_model, [data], sample_rate)[0]
351
+ ref_text = res["text"]
352
+ else:
353
+ gr.Warning("Whisper model not loaded!")
354
+
355
+ return gr.Textbox(value=ref_text)
356
+
357
+
358
+ def build_app():
359
+ with gr.Blocks(theme=gr.themes.Base()) as app:
360
+ gr.Markdown(HEADER_MD)
361
+
362
+ # Use light theme by default
363
+ app.load(
364
+ None,
365
+ None,
366
+ js="() => {const params = new URLSearchParams(window.location.search);if (!params.has('__theme')) {params.set('__theme', '%s');window.location.search = params.toString();}}"
367
+ % args.theme,
368
+ )
369
+
370
+ # Inference
371
+ with gr.Row():
372
+ with gr.Column(scale=3):
373
+ text = gr.Textbox(
374
+ label="Input Text", placeholder=TEXTBOX_PLACEHOLDER, lines=10
375
+ )
376
+ refined_text = gr.Textbox(
377
+ label="Realtime Transform Text",
378
+ placeholder=
379
+ "Normalization Result Preview (Currently Only Chinese)",
380
+ lines=5,
381
+ interactive=False,
382
+ )
383
+
384
+ with gr.Row():
385
+ if_refine_text = gr.Checkbox(
386
+ label="Text Normalization (ZH)",
387
+ value=False,
388
+ scale=1,
389
+ )
390
+
391
+ if_load_asr_model = gr.Checkbox(
392
+ label="Load / Unload ASR model for auto-reranking",
393
+ value=False,
394
+ scale=3,
395
+ )
396
+
397
+ with gr.Row():
398
+ with gr.Tab(label="Advanced Config"):
399
+ chunk_length = gr.Slider(
400
+ label="Iterative Prompt Length, 0 means off",
401
+ minimum=0,
402
+ maximum=500,
403
+ value=200,
404
+ step=8,
405
+ )
406
+
407
+ max_new_tokens = gr.Slider(
408
+ label="Maximum tokens per batch, 0 means no limit",
409
+ minimum=0,
410
+ maximum=2048,
411
+ value=1024, # 0 means no limit
412
+ step=8,
413
+ )
414
+
415
+ top_p = gr.Slider(
416
+ label="Top-P",
417
+ minimum=0.6,
418
+ maximum=0.9,
419
+ value=0.7,
420
+ step=0.01,
421
+ )
422
+
423
+ repetition_penalty = gr.Slider(
424
+ label="Repetition Penalty",
425
+ minimum=1,
426
+ maximum=1.5,
427
+ value=1.2,
428
+ step=0.01,
429
+ )
430
+
431
+ temperature = gr.Slider(
432
+ label="Temperature",
433
+ minimum=0.6,
434
+ maximum=0.9,
435
+ value=0.7,
436
+ step=0.01,
437
+ )
438
+
439
+ with gr.Tab(label="Reference Audio"):
440
+ gr.Markdown(
441
+ "5 to 10 seconds of reference audio, useful for specifying speaker."
442
+ )
443
+
444
+ enable_reference_audio = gr.Checkbox(
445
+ label="Enable Reference Audio",
446
+ )
447
+
448
+ # Add dropdown for selecting example audio files
449
+ example_audio_files = [f for f in os.listdir("examples") if f.endswith(".wav")]
450
+ example_audio_dropdown = gr.Dropdown(
451
+ label="Select Example Audio",
452
+ choices=[""] + example_audio_files,
453
+ value=""
454
+ )
455
+
456
+ reference_audio = gr.Audio(
457
+ label="Reference Audio",
458
+ type="filepath",
459
+ )
460
+ with gr.Row():
461
+ if_auto_label = gr.Checkbox(
462
+ label="Auto Labeling",
463
+ min_width=100,
464
+ scale=0,
465
+ value=False,
466
+ )
467
+ reference_text = gr.Textbox(
468
+ label="Reference Text",
469
+ lines=1,
470
+ placeholder="在一无所知中,梦里的一天结束了,一个新的「轮回」便会开始。",
471
+ value="",
472
+ )
473
+ with gr.Tab(label="Batch Inference"):
474
+ batch_infer_num = gr.Slider(
475
+ label="Batch infer nums",
476
+ minimum=1,
477
+ maximum=n_audios,
478
+ step=1,
479
+ value=1,
480
+ )
481
+
482
+ with gr.Column(scale=3):
483
+ for _ in range(n_audios):
484
+ with gr.Row():
485
+ error = gr.HTML(
486
+ label="Error Message",
487
+ visible=True if _ == 0 else False,
488
+ )
489
+ global_error_list.append(error)
490
+ with gr.Row():
491
+ audio = gr.Audio(
492
+ label="Generated Audio",
493
+ type="numpy",
494
+ interactive=False,
495
+ visible=True if _ == 0 else False,
496
+ )
497
+ global_audio_list.append(audio)
498
+
499
+ with gr.Row():
500
+ stream_audio = gr.Audio(
501
+ label="Streaming Audio",
502
+ streaming=True,
503
+ autoplay=True,
504
+ interactive=False,
505
+ show_download_button=True,
506
+ )
507
+ with gr.Row():
508
+ with gr.Column(scale=3):
509
+ generate = gr.Button(
510
+ value="\U0001F3A7 " + "Generate", variant="primary"
511
+ )
512
+ generate_stream = gr.Button(
513
+ value="\U0001F3A7 " + "Streaming Generate",
514
+ variant="primary",
515
+ )
516
+
517
+ text.input(
518
+ fn=normalize_text, inputs=[text, if_refine_text], outputs=[refined_text]
519
+ )
520
+
521
+ if_load_asr_model.change(
522
+ fn=change_if_load_asr_model,
523
+ inputs=[if_load_asr_model],
524
+ outputs=[if_load_asr_model],
525
+ )
526
+
527
+ if_auto_label.change(
528
+ fn=lambda: gr.Textbox(value=""),
529
+ inputs=[],
530
+ outputs=[reference_text],
531
+ ).then(
532
+ fn=change_if_auto_label,
533
+ inputs=[
534
+ if_load_asr_model,
535
+ if_auto_label,
536
+ enable_reference_audio,
537
+ reference_audio,
538
+ reference_text,
539
+ ],
540
+ outputs=[reference_text],
541
+ )
542
+
543
+ def select_example_audio(audio_file):
544
+ if audio_file:
545
+ audio_path = os.path.join("examples", audio_file)
546
+ lab_file = os.path.splitext(audio_file)[0] + ".lab"
547
+ lab_path = os.path.join("examples", lab_file)
548
+
549
+ if os.path.exists(lab_path):
550
+ with open(lab_path, "r", encoding="utf-8") as f:
551
+ lab_content = f.read().strip()
552
+ else:
553
+ lab_content = ""
554
+
555
+ return audio_path, lab_content, True
556
+ return None, "", False
557
+
558
+ # Connect the dropdown to update reference audio and text
559
+ example_audio_dropdown.change(
560
+ fn=select_example_audio,
561
+ inputs=[example_audio_dropdown],
562
+ outputs=[reference_audio, reference_text, enable_reference_audio]
563
+ )
564
+ # # Submit
565
+ generate.click(
566
+ inference_wrapper,
567
+ [
568
+ refined_text,
569
+ enable_reference_audio,
570
+ reference_audio,
571
+ reference_text,
572
+ max_new_tokens,
573
+ chunk_length,
574
+ top_p,
575
+ repetition_penalty,
576
+ temperature,
577
+ batch_infer_num,
578
+ if_load_asr_model,
579
+ ],
580
+ [stream_audio, *global_audio_list, *global_error_list],
581
+ concurrency_limit=1,
582
+ )
583
+ return app
584
+
585
+
586
+ def parse_args():
587
+ parser = ArgumentParser()
588
+ parser.add_argument(
589
+ "--llama-checkpoint-path",
590
+ type=Path,
591
+ default="checkpoints/fish-speech-1.4",
592
+ )
593
+ parser.add_argument(
594
+ "--decoder-checkpoint-path",
595
+ type=Path,
596
+ default="checkpoints/fish-speech-1.4/firefly-gan-vq-fsq-8x1024-21hz-generator.pth",
597
+ )
598
+ parser.add_argument("--decoder-config-name", type=str, default="firefly_gan_vq")
599
+ parser.add_argument("--device", type=str, default="cuda")
600
+ parser.add_argument("--half", action="store_true")
601
+ parser.add_argument("--compile", action="store_true",default=True)
602
+ parser.add_argument("--max-gradio-length", type=int, default=0)
603
+ parser.add_argument("--theme", type=str, default="light")
604
+
605
+ return parser.parse_args()
606
+
607
+
608
+ if __name__ == "__main__":
609
+ args = parse_args()
610
+ args.precision = torch.half if args.half else torch.bfloat16
611
+
612
+ logger.info("Loading Llama model...")
613
+ llama_queue = launch_thread_safe_queue(
614
+ checkpoint_path=args.llama_checkpoint_path,
615
+ device=args.device,
616
+ precision=args.precision,
617
+ compile=args.compile,
618
+ )
619
+ logger.info("Llama model loaded, loading VQ-GAN model...")
620
+
621
+ decoder_model = load_decoder_model(
622
+ config_name=args.decoder_config_name,
623
+ checkpoint_path=args.decoder_checkpoint_path,
624
+ device=args.device,
625
+ )
626
+
627
+ logger.info("Decoder model loaded, warming up...")
628
+
629
+ # Dry run to check if the model is loaded correctly and avoid the first-time latency
630
+ list(
631
+ inference(
632
+ text="Hello, world!",
633
+ enable_reference_audio=False,
634
+ reference_audio=None,
635
+ reference_text="",
636
+ max_new_tokens=0,
637
+ chunk_length=100,
638
+ top_p=0.7,
639
+ repetition_penalty=1.2,
640
+ temperature=0.7,
641
+ )
642
+ )
643
+
644
+ logger.info("Warming up done, launching the web UI...")
645
+
646
+ app = build_app()
647
+ app.launch(show_api=True)