roop0726 commited on
Commit
93ebf0e
·
verified ·
1 Parent(s): 98eae7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +933 -557
app.py CHANGED
@@ -1,827 +1,1203 @@
 
1
  import os
2
- import sys
 
 
 
 
 
3
  import subprocess
4
- import base64
5
- import mimetypes
6
  import threading
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- # ============================================================
9
- # Qwen2.5-VL-3B GGUF + Gradio
10
- #
11
- # This version DOES NOT use llama-server.
12
- # It uses llama-cpp-python directly from Python.
13
- #
14
- # Model:
15
- # Qwen2.5-VL-3B-Instruct-Q4_K_M.gguf
16
- #
17
- # Vision projector:
18
- # mmproj-Qwen2.5-VL-3B-Instruct-Q8_0.gguf
19
- #
20
- # Both files are downloaded automatically from Hugging Face.
21
- # ============================================================
22
 
 
 
23
 
24
- MODEL_REPO = "ggml-org/Qwen2.5-VL-3B-Instruct-GGUF"
25
 
26
- MODEL_FILE = "Qwen2.5-VL-3B-Instruct-Q4_K_M.gguf"
 
 
27
 
28
- MMPROJ_FILE = "mmproj-Qwen2.5-VL-3B-Instruct-Q8_0.gguf"
 
 
29
 
30
- MODEL_DIR = os.environ.get(
31
- "MODEL_DIR",
32
- "/tmp/qwen25vl"
 
 
33
  )
34
 
35
- MODEL_PATH = os.path.join(
36
- MODEL_DIR,
37
- MODEL_FILE
38
  )
39
 
40
- MMPROJ_PATH = os.path.join(
41
- MODEL_DIR,
42
- MMPROJ_FILE
43
  )
44
 
 
 
 
45
 
46
- # ============================================================
47
- # SETTINGS
48
- # ============================================================
49
 
50
- N_CTX = 4096
 
51
 
52
- # -1 = try to offload all supported layers to GPU.
53
- #
54
- # If your Space has no CUDA-enabled llama-cpp-python,
55
- # llama.cpp will run on CPU instead.
56
- N_GPU_LAYERS = -1
57
 
58
- N_THREADS = max(
59
- 1,
60
- os.cpu_count() or 4
61
- )
62
 
63
- N_BATCH = 512
64
 
65
- DEFAULT_MAX_TOKENS = 1024
 
 
 
 
 
 
 
 
 
66
 
 
 
 
67
 
68
- # Global model
69
- llm = None
70
 
71
- model_lock = threading.Lock()
 
 
72
 
 
73
 
74
- # ============================================================
75
- # LOG
76
- # ============================================================
 
 
77
 
78
- def log(message=""):
79
- print(message, flush=True)
80
 
 
 
 
81
 
82
- # ============================================================
83
- # INSTALL / CHECK PYTHON DEPENDENCIES
84
- # ============================================================
85
 
86
- def ensure_dependencies():
87
 
88
- missing = []
89
 
90
- try:
91
- import gradio
92
- except ImportError:
93
- missing.append("gradio")
94
 
95
- try:
96
- import requests
97
- except ImportError:
98
- missing.append("requests")
99
 
100
- try:
101
- import huggingface_hub
102
- except ImportError:
103
- missing.append("huggingface_hub")
104
 
105
- try:
106
- import llama_cpp
107
- except ImportError:
108
- missing.append("llama-cpp-python")
109
 
110
- if not missing:
111
- return
 
 
 
 
 
 
 
112
 
113
- log("=" * 70)
114
- log("INSTALLING MISSING PYTHON PACKAGES")
115
- log("=" * 70)
116
 
117
- packages = []
118
 
119
- for package in missing:
120
 
121
- if package == "llama-cpp-python":
122
- packages.append(
123
- "llama-cpp-python>=0.3.35"
124
- )
125
- else:
126
- packages.append(package)
127
 
128
- subprocess.check_call(
129
- [
130
- sys.executable,
131
- "-m",
132
- "pip",
133
- "install",
134
- "-U",
135
- *packages,
136
- ]
137
  )
138
 
 
 
 
 
 
 
139
 
140
- ensure_dependencies()
 
 
141
 
 
 
 
 
 
 
 
 
142
 
143
- # Imports after dependency check.
144
- import gradio as gr
145
 
146
- from huggingface_hub import hf_hub_download
147
 
148
- from llama_cpp import Llama
 
 
 
 
 
 
149
 
150
- try:
151
- from llama_cpp.llama_chat_format import Qwen25VLChatHandler
152
- except ImportError:
153
- Qwen25VLChatHandler = None
154
 
 
155
 
156
- # ============================================================
157
- # DOWNLOAD MODEL
158
- # ============================================================
 
 
 
 
 
159
 
160
- def download_models():
 
 
 
161
 
162
- os.makedirs(
163
- MODEL_DIR,
164
- exist_ok=True
165
- )
166
 
167
- log("=" * 70)
168
- log("DOWNLOADING / CHECKING QWEN2.5-VL MODEL")
169
- log("=" * 70)
 
170
 
171
- log(
172
- "Repository: "
173
- + MODEL_REPO
174
- )
175
 
176
- log(
177
- "Model: "
178
- + MODEL_FILE
179
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- log(
182
- "MMProj: "
183
- + MMPROJ_FILE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  )
185
 
186
- # Main Q4_K_M model.
187
- if os.path.isfile(MODEL_PATH):
188
 
189
- log(
190
- "[MODEL] Already exists: "
191
- + MODEL_PATH
 
 
 
192
  )
193
 
194
- else:
 
 
 
195
 
196
- log(
197
- "[MODEL] Downloading Q4_K_M..."
198
- )
 
 
 
 
199
 
200
- downloaded_model = hf_hub_download(
201
- repo_id=MODEL_REPO,
202
- filename=MODEL_FILE,
203
- local_dir=MODEL_DIR,
204
- local_dir_use_symlinks=False,
205
- )
206
 
207
- log(
208
- "[MODEL] Downloaded: "
209
- + downloaded_model
210
- )
211
 
212
- # Vision projector.
213
- if os.path.isfile(MMPROJ_PATH):
214
 
215
- log(
216
- "[MMPROJ] Already exists: "
217
- + MMPROJ_PATH
218
- )
219
 
220
- else:
 
 
 
 
 
 
 
 
221
 
222
- log(
223
- "[MMPROJ] Downloading Q8_0 projector..."
 
224
  )
225
 
226
- downloaded_mmproj = hf_hub_download(
227
- repo_id=MODEL_REPO,
228
- filename=MMPROJ_FILE,
229
- local_dir=MODEL_DIR,
230
- local_dir_use_symlinks=False,
231
  )
232
 
233
- log(
234
- "[MMPROJ] Downloaded: "
235
- + downloaded_mmproj
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  )
237
 
238
- if not os.path.isfile(MODEL_PATH):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
- raise FileNotFoundError(
241
- "Main Qwen model was not found:\n"
242
- + MODEL_PATH
 
 
 
 
 
 
 
 
 
 
 
 
243
  )
244
 
245
- if not os.path.isfile(MMPROJ_PATH):
246
 
247
- raise FileNotFoundError(
248
- "Qwen vision projector was not found:\n"
249
- + MMPROJ_PATH
250
  )
251
 
252
- log(
253
- "[MODEL] Size: "
254
- + str(
255
- os.path.getsize(MODEL_PATH)
256
- / (1024 * 1024 * 1024)
257
  )
258
- + " GB"
 
 
 
259
  )
260
 
261
- log(
262
- "[MMPROJ] Size: "
263
- + str(
264
- os.path.getsize(MMPROJ_PATH)
265
- / (1024 * 1024 * 1024)
266
- )
267
- + " GB"
268
  )
269
 
 
 
 
 
 
 
 
 
270
 
271
- # ============================================================
272
- # LOAD QWEN2.5-VL
273
- # ============================================================
274
 
275
- def load_model():
 
 
276
 
277
- global llm
 
 
 
278
 
279
- if llm is not None:
280
- return
 
281
 
282
- if Qwen25VLChatHandler is None:
283
 
284
  raise RuntimeError(
285
- "Qwen25VLChatHandler is not available in the "
286
- "installed llama-cpp-python package.\n\n"
287
- "Please use llama-cpp-python 0.3.35 or newer."
288
  )
289
 
290
- download_models()
 
 
291
 
292
- log("=" * 70)
293
- log("LOADING QWEN2.5-VL-3B")
294
- log("=" * 70)
295
 
296
- log(
297
- "Model path: "
298
- + MODEL_PATH
299
- )
300
 
301
- log(
302
- "MMProj path: "
303
- + MMPROJ_PATH
304
- )
305
 
306
- log(
307
- "Context: "
308
- + str(N_CTX)
309
- )
310
 
311
- log(
312
- "GPU layers: "
313
- + str(N_GPU_LAYERS)
314
  )
315
 
316
- log(
317
- "CPU threads: "
318
- + str(N_THREADS)
319
- )
320
 
321
- log(
322
- "Batch: "
323
- + str(N_BATCH)
324
  )
325
 
326
- # Qwen2.5-VL uses a dedicated multimodal chat handler.
327
- #
328
- # llama-cpp-python 0.3.35 uses clip_model_path for
329
- # the multimodal projector.
330
- chat_handler = Qwen25VLChatHandler(
331
- clip_model_path=MMPROJ_PATH,
332
- verbose=False,
333
- )
334
 
335
- llm = Llama(
336
- model_path=MODEL_PATH,
 
 
337
 
338
- chat_handler=chat_handler,
339
 
340
- n_ctx=N_CTX,
 
 
 
341
 
342
- n_batch=N_BATCH,
 
 
 
 
 
 
 
 
 
 
343
 
344
- n_threads=N_THREADS,
 
 
 
345
 
346
- n_gpu_layers=N_GPU_LAYERS,
347
 
348
- use_mmap=True,
 
 
349
 
350
- verbose=False,
 
 
 
 
351
  )
352
 
353
- log("=" * 70)
354
- log("QWEN2.5-VL READY")
355
- log("=" * 70)
356
 
 
 
 
 
 
357
 
358
- # ============================================================
359
- # IMAGE -> BASE64 DATA URI
360
- # ============================================================
 
 
 
 
 
361
 
362
- def image_to_data_uri(image_path):
363
 
364
- if not image_path:
365
- raise ValueError(
366
- "No image was supplied."
367
  )
 
368
 
369
- if not os.path.isfile(image_path):
370
 
371
- raise FileNotFoundError(
372
- image_path
 
373
  )
374
 
375
- mime_type, _ = mimetypes.guess_type(
376
- image_path
377
- )
 
 
378
 
379
- if not mime_type:
380
- mime_type = "image/jpeg"
 
 
 
 
 
 
 
381
 
382
- with open(
383
- image_path,
384
- "rb"
385
- ) as f:
386
 
387
- encoded = base64.b64encode(
388
- f.read()
389
- ).decode(
390
- "utf-8"
391
  )
392
 
393
- return (
394
- "data:"
395
- + mime_type
396
- + ";base64,"
397
- + encoded
398
  )
399
 
 
 
 
400
 
401
- # ============================================================
402
- # DEFAULT OCR INSTRUCTION
403
- # ============================================================
404
 
405
- DEFAULT_INSTRUCTION = """Read all visible text in this image.
406
 
407
- Use natural reading order:
408
- 1. Top to bottom.
409
- 2. Left to right within each line.
410
- 3. Follow columns in their natural order.
411
- 4. Preserve paragraphs and line breaks where possible.
412
- 5. Do not describe the image.
413
- 6. Return only the text you can read.
414
- 7. Do not invent missing or unclear text."""
415
 
 
416
 
417
- # ============================================================
418
- # GENERATE STREAM
419
- # ============================================================
420
 
421
- def generate(
422
- image,
423
- instruction,
424
- max_tokens,
425
- ):
426
 
427
- if not image:
428
 
429
- raise gr.Error(
430
- "Please upload an image."
431
  )
432
 
433
- if (
434
- instruction is None
435
- or not instruction.strip()
436
- ):
437
-
438
- instruction = DEFAULT_INSTRUCTION
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
- try:
 
 
441
 
442
- image_data_uri = image_to_data_uri(
443
- image
 
 
 
 
 
444
  )
445
 
446
- except Exception as e:
447
 
448
- raise gr.Error(
449
- "Image error:\n"
450
- + str(e)
451
- )
452
 
453
- if llm is None:
 
 
 
454
 
455
- raise gr.Error(
456
- "The Qwen2.5-VL model is not loaded."
457
- )
 
 
458
 
459
- messages = [
460
- {
461
- "role": "system",
462
- "content": (
463
- "You are an image reading assistant. "
464
- "Follow the user's instruction exactly. "
465
- "When extracting text, read it in natural "
466
- "reading order from top to bottom and left "
467
- "to right. Never invent text."
468
- ),
469
- },
470
- {
471
- "role": "user",
472
- "content": [
473
- {
474
- "type": "image_url",
475
- "image_url": {
476
- "url": image_data_uri
477
- },
478
- },
479
- {
480
- "type": "text",
481
- "text": instruction,
482
- },
483
- ],
484
- },
485
- ]
486
 
487
- log("=" * 70)
488
- log("IMAGE REQUEST")
489
- log("=" * 70)
 
490
 
491
- log(
492
- "Instruction:\n"
493
- + instruction
494
  )
495
 
496
- log("=" * 70)
 
 
497
 
498
- output = ""
499
 
500
- try:
501
 
502
- # Only one generation at a time.
503
- # This avoids multiple users competing for the
504
- # same llama.cpp model context.
505
- with model_lock:
 
506
 
507
- stream = llm.create_chat_completion(
508
- messages=messages,
 
 
 
509
 
510
- max_tokens=int(
511
- max_tokens
512
- ),
513
 
514
- temperature=0.0,
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
- top_p=1.0,
517
 
518
- stream=True,
 
 
 
 
 
 
 
 
 
519
  )
520
 
521
- for chunk in stream:
522
 
523
- if not chunk:
524
- continue
525
 
526
- choices = chunk.get(
527
- "choices",
528
- []
529
- )
530
 
531
- if not choices:
532
- continue
533
 
534
- choice = choices[0]
 
 
 
 
535
 
536
- delta = choice.get(
537
- "delta",
538
- {}
539
- )
540
 
541
- token = delta.get(
542
- "content",
543
- ""
 
 
 
 
 
 
 
 
544
  )
545
 
546
- if token:
 
 
 
547
 
548
- output += token
549
 
550
- yield output
 
 
 
 
551
 
552
- except Exception as e:
553
 
554
- log("=" * 70)
555
- log("INFERENCE ERROR")
556
- log("=" * 70)
557
- log(repr(e))
558
- log("=" * 70)
559
 
560
- raise gr.Error(
561
- "Qwen2.5-VL inference failed:\n\n"
562
- + str(e)
563
- )
564
 
565
- if not output:
 
 
566
 
567
- yield (
568
- "The model returned no text."
 
569
  )
570
 
 
 
 
 
571
 
572
- # ============================================================
573
- # CLEAR
574
- # ============================================================
575
 
576
- def clear():
 
 
577
 
578
- return (
579
- None,
580
- DEFAULT_INSTRUCTION,
581
- "",
582
- )
583
 
 
 
 
584
 
585
- # ============================================================
586
- # GRADIO UI
587
- # ============================================================
 
588
 
589
- def create_ui():
 
 
 
 
 
 
 
 
590
 
591
- with gr.Blocks(
592
- title="Qwen2.5-VL 3B Image Reader"
593
- ) as demo:
594
 
595
- gr.Markdown(
596
- """
597
- # Qwen2.5-VL 3B Image Reader
598
 
599
- Upload an image and give Qwen2.5-VL an instruction.
600
 
601
- The response is streamed as the model generates it.
602
- """
 
 
 
 
 
 
603
  )
604
 
605
- with gr.Row():
 
 
 
 
 
606
 
607
- # =================================================
608
- # INPUT
609
- # =================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
- with gr.Column(
612
- scale=1
613
- ):
614
 
615
- image = gr.Image(
616
- label="Upload Image",
617
- type="filepath",
618
- sources=[
619
- "upload",
620
- "clipboard",
621
- ],
622
- )
623
 
624
- instruction = gr.Textbox(
625
- label="Instruction",
626
- value=DEFAULT_INSTRUCTION,
627
- lines=10,
628
- placeholder=(
629
- "Tell Qwen2.5-VL what "
630
- "to do with the image..."
631
- ),
632
- )
633
 
634
- max_tokens = gr.Slider(
635
- minimum=64,
636
- maximum=4096,
637
- value=DEFAULT_MAX_TOKENS,
638
- step=64,
639
- label="Maximum output tokens",
640
- )
641
 
642
- with gr.Row():
 
 
 
643
 
644
- read_button = gr.Button(
645
- "Read Image",
646
- variant="primary",
647
- )
648
 
649
- clear_button = gr.Button(
650
- "Clear"
651
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
- # =================================================
654
- # OUTPUT
655
- # =================================================
656
 
657
- with gr.Column(
658
- scale=1
659
- ):
 
 
 
660
 
661
- output = gr.Textbox(
662
- label="Model Response",
663
- lines=25,
664
- show_copy_button=True,
665
- autoscroll=True,
666
- )
667
 
668
- gr.Markdown(
669
- """
670
- ### Example
 
671
 
672
- **OCR**
673
 
674
- ```text
675
- Read all visible text from top to bottom and left to right.
676
- Return only the text.
677
- ```
678
 
679
- **Question**
680
 
681
- ```text
682
- What does the sign in this image say?
683
- ```
 
 
 
 
 
684
 
685
- **Specific region**
686
 
687
- ```text
688
- Read only the text in the upper half of the image.
689
- ```
 
 
690
 
691
- **Extraction**
692
 
693
- ```text
694
- Extract all names and phone numbers visible in the image.
695
- ```
696
- """
 
697
  )
698
 
699
- read_button.click(
700
- fn=generate,
 
701
 
702
- inputs=[
703
- image,
704
- instruction,
705
- max_tokens,
706
- ],
 
707
 
708
- outputs=output,
 
 
709
  )
710
 
711
- clear_button.click(
712
- fn=clear,
713
 
714
- inputs=[],
 
 
 
 
 
715
 
716
- outputs=[
717
- image,
718
- instruction,
719
- output,
720
- ],
 
721
  )
722
 
 
 
 
 
 
 
 
 
723
  return demo
724
 
725
 
726
- # ============================================================
727
- # MAIN
728
- # ============================================================
729
 
730
- def main():
 
 
731
 
732
- log("=" * 70)
733
- log("APP.PY STARTED")
734
- log("=" * 70)
735
 
736
- log(
737
- "Python: "
738
- + sys.version
739
  )
740
 
741
- log(
742
- "Gradio: "
743
- + getattr(
744
- gr,
745
- "__version__",
746
- "unknown",
747
- )
748
  )
749
 
750
- log(
751
- "Model: "
752
- + MODEL_REPO
753
  )
754
 
755
- log(
756
- "Q4 model: "
757
- + MODEL_FILE
758
  )
759
 
760
- log(
761
- "MMProj: "
762
- + MMPROJ_FILE
763
  )
764
 
765
- log(
766
- "Context: "
767
- + str(N_CTX)
768
  )
769
 
770
- log(
771
- "GPU layers: "
772
- + str(N_GPU_LAYERS)
773
  )
774
 
775
- log("=" * 70)
776
 
777
- # Load model before opening Gradio.
778
- load_model()
779
 
780
- demo = create_ui()
781
 
782
- # Queue enables generator-based streaming.
783
- demo.queue(
784
- max_size=20,
785
- default_concurrency_limit=1,
786
  )
787
 
788
- log("=" * 70)
789
- log("STARTING GRADIO")
790
- log("=" * 70)
791
 
792
- demo.launch(
793
- server_name="0.0.0.0",
794
- server_port=7860,
795
- show_error=True,
796
  )
797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
 
799
- # ============================================================
800
- # ENTRY POINT
801
- # ============================================================
802
 
803
  if __name__ == "__main__":
804
 
805
  try:
806
 
807
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
808
 
809
  except KeyboardInterrupt:
810
 
811
- log(
812
  "[SYSTEM] Interrupted."
813
  )
814
 
815
  except Exception as e:
816
 
817
- log("=" * 70)
818
- log("STARTUP FAILED")
819
- log("=" * 70)
 
 
 
 
820
 
821
- log(
822
  repr(e)
823
  )
824
 
825
- log("=" * 70)
 
 
826
 
827
  raise
 
 
 
 
 
1
+
2
  import os
3
+ import time
4
+ import json
5
+ import shutil
6
+ import tarfile
7
+ import zipfile
8
+ import stat
9
  import subprocess
 
 
10
  import threading
11
+ from pathlib import Path
12
+
13
+ import requests
14
+ import gradio as gr
15
+ from huggingface_hub import hf_hub_download
16
+
17
+
18
+ APP_DIR = Path("/app")
19
+
20
+ MODEL_DIR = APP_DIR / "models"
21
+ MODEL_DIR.mkdir(parents=True, exist_ok=True)
22
+
23
+ MODEL_REPO = "Qwen/Qwen2.5-Coder-3B-Instruct-GGUF"
24
+ MODEL_FILE = "qwen2.5-coder-3b-instruct-q4_k_m.gguf"
25
+ MODEL_PATH = MODEL_DIR / MODEL_FILE
26
+
27
+ LLAMA_DIR = APP_DIR / "llama-server"
28
+ LLAMA_EXTRACT_DIR = APP_DIR / "llama-extracted"
29
+ LLAMA_SERVER = LLAMA_DIR / "llama-server"
30
 
31
+ SERVER_HOST = "127.0.0.1"
32
+ SERVER_PORT = 8080
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ GRADIO_HOST = "0.0.0.0"
35
+ GRADIO_PORT = 7860
36
 
37
+ API_KEY = "local-space-key"
38
 
39
+ PARALLEL_USERS = 4
40
+ CONTEXT_PER_USER = 2048
41
+ TOTAL_CONTEXT = PARALLEL_USERS * CONTEXT_PER_USER
42
 
43
+ CPU_THREADS = int(
44
+ os.environ.get("CPU_THREADS", "4")
45
+ )
46
 
47
+ CPU_THREADS_BATCH = int(
48
+ os.environ.get(
49
+ "CPU_THREADS_BATCH",
50
+ str(CPU_THREADS)
51
+ )
52
  )
53
 
54
+ BATCH_SIZE = int(
55
+ os.environ.get("BATCH_SIZE", "512")
 
56
  )
57
 
58
+ MAX_TOKENS = int(
59
+ os.environ.get("MAX_TOKENS", "512")
 
60
  )
61
 
62
+ TEMPERATURE = float(
63
+ os.environ.get("TEMPERATURE", "0.2")
64
+ )
65
 
66
+ TOP_P = float(
67
+ os.environ.get("TOP_P", "0.9")
68
+ )
69
 
70
+ server_process = None
71
+ server_lock = threading.Lock()
72
 
 
 
 
 
 
73
 
74
+ def download_model():
 
 
 
75
 
76
+ if MODEL_PATH.exists():
77
 
78
+ size_gb = (
79
+ MODEL_PATH.stat().st_size
80
+ / 1024
81
+ / 1024
82
+ / 1024
83
+ )
84
+
85
+ print(
86
+ f"[MODEL] Already exists: {MODEL_PATH}"
87
+ )
88
 
89
+ print(
90
+ f"[MODEL] Size: {size_gb:.2f} GB"
91
+ )
92
 
93
+ return str(MODEL_PATH)
 
94
 
95
+ print(
96
+ "[MODEL] Downloading Qwen2.5-Coder-3B Q4_K_M..."
97
+ )
98
 
99
+ hf_token = os.environ.get("HF_TOKEN")
100
 
101
+ kwargs = {
102
+ "repo_id": MODEL_REPO,
103
+ "filename": MODEL_FILE,
104
+ "local_dir": str(MODEL_DIR),
105
+ }
106
 
107
+ if hf_token:
108
+ kwargs["token"] = hf_token
109
 
110
+ downloaded = hf_hub_download(
111
+ **kwargs
112
+ )
113
 
114
+ print(
115
+ f"[MODEL] Download complete: {downloaded}"
116
+ )
117
 
118
+ return downloaded
119
 
 
120
 
121
+ def find_llama_server(directory):
 
 
 
122
 
123
+ directory = Path(directory)
 
 
 
124
 
125
+ for path in directory.rglob(
126
+ "llama-server"
127
+ ):
 
128
 
129
+ if path.is_file():
 
 
 
130
 
131
+ try:
132
+ path.chmod(
133
+ path.stat().st_mode
134
+ | stat.S_IXUSR
135
+ | stat.S_IXGRP
136
+ | stat.S_IXOTH
137
+ )
138
+ except Exception:
139
+ pass
140
 
141
+ return path
 
 
142
 
143
+ return None
144
 
 
145
 
146
+ def find_cpu_asset():
 
 
 
 
 
147
 
148
+ api_url = (
149
+ "https://api.github.com/"
150
+ "repos/ggml-org/llama.cpp/releases"
 
 
 
 
 
 
151
  )
152
 
153
+ headers = {
154
+ "Accept":
155
+ "application/vnd.github+json",
156
+ "User-Agent":
157
+ "hf-space-qwen-llama-server"
158
+ }
159
 
160
+ print(
161
+ "[LLAMA] Searching llama.cpp releases..."
162
+ )
163
 
164
+ response = requests.get(
165
+ api_url,
166
+ params={
167
+ "per_page": 20
168
+ },
169
+ headers=headers,
170
+ timeout=30
171
+ )
172
 
173
+ response.raise_for_status()
 
174
 
175
+ releases = response.json()
176
 
177
+ if not isinstance(
178
+ releases,
179
+ list
180
+ ):
181
+ raise RuntimeError(
182
+ "Invalid GitHub releases response."
183
+ )
184
 
185
+ stable = []
 
 
 
186
 
187
+ for release in releases:
188
 
189
+ if not release.get(
190
+ "draft",
191
+ False
192
+ ) and not release.get(
193
+ "prerelease",
194
+ False
195
+ ):
196
+ stable.append(release)
197
 
198
+ releases = stable + [
199
+ r for r in releases
200
+ if r not in stable
201
+ ]
202
 
203
+ for release in releases:
 
 
 
204
 
205
+ tag = release.get(
206
+ "tag_name",
207
+ "unknown"
208
+ )
209
 
210
+ assets = release.get(
211
+ "assets",
212
+ []
213
+ )
214
 
215
+ print(
216
+ f"[LLAMA] Checking {tag}"
217
+ )
218
+
219
+ for asset in assets:
220
+
221
+ name = asset.get(
222
+ "name",
223
+ ""
224
+ )
225
+
226
+ lower = name.lower()
227
+
228
+ special = any(
229
+ x in lower
230
+ for x in (
231
+ "cuda",
232
+ "vulkan",
233
+ "rocm",
234
+ "openvino",
235
+ "sycl",
236
+ "metal"
237
+ )
238
+ )
239
 
240
+ current_cpu = (
241
+ "ubuntu-x64" in lower
242
+ and lower.endswith(
243
+ ".tar.gz"
244
+ )
245
+ )
246
+
247
+ old_cpu = (
248
+ "ubuntu" in lower
249
+ and "x64" in lower
250
+ and (
251
+ "cpu" in lower
252
+ or "bin-" in lower
253
+ )
254
+ and lower.endswith(
255
+ (
256
+ ".tar.gz",
257
+ ".zip"
258
+ )
259
+ )
260
+ )
261
+
262
+ if (
263
+ not special
264
+ and (
265
+ current_cpu
266
+ or old_cpu
267
+ )
268
+ ):
269
+
270
+ print(
271
+ f"[LLAMA] Found CPU build: {name}"
272
+ )
273
+
274
+ return asset
275
+
276
+ print(
277
+ "[LLAMA] No compatible CPU build found."
278
  )
279
 
280
+ for release in releases[:5]:
 
281
 
282
+ print(
283
+ "Release:",
284
+ release.get(
285
+ "tag_name",
286
+ "unknown"
287
+ )
288
  )
289
 
290
+ for asset in release.get(
291
+ "assets",
292
+ []
293
+ ):
294
 
295
+ print(
296
+ " ",
297
+ asset.get(
298
+ "name",
299
+ ""
300
+ )
301
+ )
302
 
303
+ raise RuntimeError(
304
+ "Could not find Ubuntu x64 CPU "
305
+ "llama.cpp release asset."
306
+ )
 
 
307
 
 
 
 
 
308
 
309
+ def download_llama_server():
 
310
 
311
+ if LLAMA_SERVER.exists():
 
 
 
312
 
313
+ try:
314
+ LLAMA_SERVER.chmod(
315
+ LLAMA_SERVER.stat().st_mode
316
+ | stat.S_IXUSR
317
+ | stat.S_IXGRP
318
+ | stat.S_IXOTH
319
+ )
320
+ except Exception:
321
+ pass
322
 
323
+ print(
324
+ f"[LLAMA] Existing server found: "
325
+ f"{LLAMA_SERVER}"
326
  )
327
 
328
+ return str(
329
+ LLAMA_SERVER
 
 
 
330
  )
331
 
332
+ print(
333
+ "[LLAMA] Downloading prebuilt llama.cpp server..."
334
+ )
335
+
336
+ asset = find_cpu_asset()
337
+
338
+ asset_name = asset[
339
+ "name"
340
+ ]
341
+
342
+ download_url = asset[
343
+ "browser_download_url"
344
+ ]
345
+
346
+ archive_path = (
347
+ APP_DIR /
348
+ asset_name
349
+ )
350
+
351
+ print(
352
+ f"[LLAMA] Selected: {asset_name}"
353
+ )
354
+
355
+ if not archive_path.exists():
356
+
357
+ print(
358
+ "[LLAMA] Downloading archive..."
359
  )
360
 
361
+ headers = {
362
+ "User-Agent":
363
+ "hf-space-qwen-llama-server"
364
+ }
365
+
366
+ with requests.get(
367
+ download_url,
368
+ headers=headers,
369
+ stream=True,
370
+ timeout=120
371
+ ) as response:
372
+
373
+ response.raise_for_status()
374
+
375
+ total = int(
376
+ response.headers.get(
377
+ "content-length",
378
+ 0
379
+ )
380
+ )
381
+
382
+ downloaded = 0
383
+ last_print = 0
384
+
385
+ with open(
386
+ archive_path,
387
+ "wb"
388
+ ) as f:
389
+
390
+ for chunk in response.iter_content(
391
+ chunk_size=1024 * 1024
392
+ ):
393
+
394
+ if not chunk:
395
+ continue
396
+
397
+ f.write(chunk)
398
+
399
+ downloaded += len(
400
+ chunk
401
+ )
402
+
403
+ if total:
404
+
405
+ now = time.time()
406
+
407
+ if now - last_print >= 0.5:
408
+
409
+ percent = (
410
+ downloaded
411
+ * 100
412
+ / total
413
+ )
414
 
415
+ print(
416
+ f"\r[LLAMA] "
417
+ f"{percent:5.1f}% "
418
+ f"{downloaded / 1024 / 1024:.1f} MB / "
419
+ f"{total / 1024 / 1024:.1f} MB",
420
+ end="",
421
+ flush=True
422
+ )
423
+
424
+ last_print = now
425
+
426
+ print()
427
+
428
+ print(
429
+ "[LLAMA] Archive download complete."
430
  )
431
 
432
+ else:
433
 
434
+ print(
435
+ "[LLAMA] Archive already exists."
 
436
  )
437
 
438
+ if LLAMA_EXTRACT_DIR.exists():
439
+
440
+ shutil.rmtree(
441
+ LLAMA_EXTRACT_DIR
 
442
  )
443
+
444
+ LLAMA_EXTRACT_DIR.mkdir(
445
+ parents=True,
446
+ exist_ok=True
447
  )
448
 
449
+ print(
450
+ "[LLAMA] Extracting..."
 
 
 
 
 
451
  )
452
 
453
+ if asset_name.lower().endswith(
454
+ ".tar.gz"
455
+ ):
456
+
457
+ with tarfile.open(
458
+ archive_path,
459
+ "r:gz"
460
+ ) as tar:
461
 
462
+ tar.extractall(
463
+ LLAMA_EXTRACT_DIR
464
+ )
465
 
466
+ elif asset_name.lower().endswith(
467
+ ".zip"
468
+ ):
469
 
470
+ with zipfile.ZipFile(
471
+ archive_path,
472
+ "r"
473
+ ) as z:
474
 
475
+ z.extractall(
476
+ LLAMA_EXTRACT_DIR
477
+ )
478
 
479
+ else:
480
 
481
  raise RuntimeError(
482
+ f"Unsupported archive: {asset_name}"
 
 
483
  )
484
 
485
+ found = find_llama_server(
486
+ LLAMA_EXTRACT_DIR
487
+ )
488
 
489
+ if found is None:
 
 
490
 
491
+ for path in LLAMA_EXTRACT_DIR.rglob(
492
+ "*"
493
+ ):
 
494
 
495
+ if path.is_file():
496
+ print(
497
+ path
498
+ )
499
 
500
+ raise RuntimeError(
501
+ "llama-server was not found "
502
+ "inside the archive."
503
+ )
504
 
505
+ print(
506
+ f"[LLAMA] Found: {found}"
 
507
  )
508
 
509
+ source_dir = found.parent
 
 
 
510
 
511
+ LLAMA_DIR.mkdir(
512
+ parents=True,
513
+ exist_ok=True
514
  )
515
 
516
+ for item in source_dir.iterdir():
 
 
 
 
 
 
 
517
 
518
+ destination = (
519
+ LLAMA_DIR /
520
+ item.name
521
+ )
522
 
523
+ if item.is_dir():
524
 
525
+ if destination.exists():
526
+ shutil.rmtree(
527
+ destination
528
+ )
529
 
530
+ shutil.copytree(
531
+ item,
532
+ destination
533
+ )
534
+
535
+ else:
536
+
537
+ shutil.copy2(
538
+ item,
539
+ destination
540
+ )
541
 
542
+ final_server = (
543
+ LLAMA_DIR /
544
+ found.name
545
+ )
546
 
547
+ if not final_server.exists():
548
 
549
+ raise RuntimeError(
550
+ "Failed to install llama-server."
551
+ )
552
 
553
+ final_server.chmod(
554
+ final_server.stat().st_mode
555
+ | stat.S_IXUSR
556
+ | stat.S_IXGRP
557
+ | stat.S_IXOTH
558
  )
559
 
560
+ library_dirs = []
 
 
561
 
562
+ for root, dirs, files in os.walk(
563
+ LLAMA_DIR
564
+ ):
565
+
566
+ for filename in files:
567
 
568
+ if (
569
+ filename.endswith(".so")
570
+ or ".so." in filename
571
+ ):
572
+
573
+ library_dirs.append(
574
+ root
575
+ )
576
 
577
+ break
578
 
579
+ library_dirs = list(
580
+ dict.fromkeys(
581
+ library_dirs
582
  )
583
+ )
584
 
585
+ if library_dirs:
586
 
587
+ old_ld = os.environ.get(
588
+ "LD_LIBRARY_PATH",
589
+ ""
590
  )
591
 
592
+ new_ld = ":".join(
593
+ library_dirs
594
+ )
595
+
596
+ if old_ld:
597
 
598
+ os.environ[
599
+ "LD_LIBRARY_PATH"
600
+ ] = (
601
+ new_ld
602
+ + ":"
603
+ + old_ld
604
+ )
605
+
606
+ else:
607
 
608
+ os.environ[
609
+ "LD_LIBRARY_PATH"
610
+ ] = new_ld
 
611
 
612
+ print(
613
+ "[LLAMA] Libraries configured."
 
 
614
  )
615
 
616
+ print(
617
+ f"[LLAMA] Server installed: "
618
+ f"{final_server}"
 
 
619
  )
620
 
621
+ return str(
622
+ final_server
623
+ )
624
 
 
 
 
625
 
626
+ def start_llama_server():
627
 
628
+ global server_process
 
 
 
 
 
 
 
629
 
630
+ with server_lock:
631
 
632
+ if server_process is not None:
 
 
633
 
634
+ if (
635
+ server_process.poll()
636
+ is None
637
+ ):
 
638
 
639
+ return
640
 
641
+ server_path = (
642
+ download_llama_server()
643
  )
644
 
645
+ command = [
646
+ server_path,
647
+ "-m",
648
+ str(MODEL_PATH),
649
+ "-np",
650
+ str(PARALLEL_USERS),
651
+ "-c",
652
+ str(TOTAL_CONTEXT),
653
+ "--cont-batching",
654
+ "-ngl",
655
+ "0",
656
+ "-t",
657
+ str(CPU_THREADS),
658
+ "-tb",
659
+ str(CPU_THREADS_BATCH),
660
+ "-b",
661
+ str(BATCH_SIZE),
662
+ "--host",
663
+ SERVER_HOST,
664
+ "--port",
665
+ str(SERVER_PORT),
666
+ "--api-key",
667
+ API_KEY
668
+ ]
669
 
670
+ print(
671
+ "[LLAMA] Starting server..."
672
+ )
673
 
674
+ server_process = subprocess.Popen(
675
+ command,
676
+ stdout=subprocess.PIPE,
677
+ stderr=subprocess.STDOUT,
678
+ text=True,
679
+ bufsize=1,
680
+ env=os.environ.copy()
681
  )
682
 
683
+ def read_logs():
684
 
685
+ try:
 
 
 
686
 
687
+ for line in iter(
688
+ server_process.stdout.readline,
689
+ ""
690
+ ):
691
 
692
+ if line:
693
+ print(
694
+ "[llama.cpp]",
695
+ line.rstrip()
696
+ )
697
 
698
+ except Exception:
699
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
 
701
+ threading.Thread(
702
+ target=read_logs,
703
+ daemon=True
704
+ ).start()
705
 
706
+ health_url = (
707
+ f"http://{SERVER_HOST}:"
708
+ f"{SERVER_PORT}/health"
709
  )
710
 
711
+ print(
712
+ "[LLAMA] Waiting for server..."
713
+ )
714
 
715
+ started = time.time()
716
 
717
+ while True:
718
 
719
+ if (
720
+ server_process is not None
721
+ and server_process.poll()
722
+ is not None
723
+ ):
724
 
725
+ raise RuntimeError(
726
+ "llama-server stopped "
727
+ f"with code "
728
+ f"{server_process.returncode}"
729
+ )
730
 
731
+ try:
 
 
732
 
733
+ response = requests.get(
734
+ health_url,
735
+ timeout=2
736
+ )
737
+
738
+ if response.status_code in (
739
+ 200,
740
+ 204
741
+ ):
742
+
743
+ print(
744
+ "[LLAMA] Server is ready."
745
+ )
746
 
747
+ return
748
 
749
+ except Exception:
750
+ pass
751
+
752
+ if (
753
+ time.time() - started
754
+ > 180
755
+ ):
756
+
757
+ raise RuntimeError(
758
+ "Timed out waiting for llama-server."
759
  )
760
 
761
+ time.sleep(1)
762
 
 
 
763
 
764
+ def clean_content(content):
 
 
 
765
 
766
+ if content is None:
767
+ return ""
768
 
769
+ if isinstance(
770
+ content,
771
+ str
772
+ ):
773
+ return content
774
 
775
+ if isinstance(
776
+ content,
777
+ list
778
+ ):
779
 
780
+ result = []
781
+
782
+ for item in content:
783
+
784
+ if isinstance(
785
+ item,
786
+ str
787
+ ):
788
+
789
+ result.append(
790
+ item
791
  )
792
 
793
+ elif isinstance(
794
+ item,
795
+ dict
796
+ ):
797
 
798
+ if "text" in item:
799
 
800
+ result.append(
801
+ str(
802
+ item["text"]
803
+ )
804
+ )
805
 
806
+ elif "content" in item:
807
 
808
+ result.append(
809
+ str(
810
+ item["content"]
811
+ )
812
+ )
813
 
814
+ else:
 
 
 
815
 
816
+ result.append(
817
+ str(item)
818
+ )
819
 
820
+ return "\n".join(
821
+ x for x in result
822
+ if x
823
  )
824
 
825
+ if isinstance(
826
+ content,
827
+ dict
828
+ ):
829
 
830
+ if "text" in content:
 
 
831
 
832
+ return str(
833
+ content["text"]
834
+ )
835
 
836
+ if "content" in content:
 
 
 
 
837
 
838
+ return str(
839
+ content["content"]
840
+ )
841
 
842
+ return json.dumps(
843
+ content,
844
+ ensure_ascii=False
845
+ )
846
 
847
+ return str(content)
848
+
849
+
850
+ def build_messages(
851
+ message,
852
+ history
853
+ ):
854
+
855
+ messages = []
856
 
857
+ history = history or []
 
 
858
 
859
+ recent_history = history[-10:]
 
 
860
 
861
+ for item in recent_history:
862
 
863
+ if not isinstance(
864
+ item,
865
+ dict
866
+ ):
867
+ continue
868
+
869
+ role = item.get(
870
+ "role"
871
  )
872
 
873
+ content = clean_content(
874
+ item.get(
875
+ "content",
876
+ ""
877
+ )
878
+ )
879
 
880
+ if role in (
881
+ "user",
882
+ "assistant",
883
+ "system"
884
+ ) and content:
885
+
886
+ messages.append({
887
+ "role":
888
+ role,
889
+ "content":
890
+ content
891
+ })
892
+
893
+ messages.append({
894
+ "role":
895
+ "user",
896
+ "content":
897
+ clean_content(
898
+ message
899
+ )
900
+ })
901
 
902
+ return messages
 
 
903
 
 
 
 
 
 
 
 
 
904
 
905
+ def generate_response(
906
+ message,
907
+ history
908
+ ):
 
 
 
 
 
909
 
910
+ if not message:
911
+ return "Please enter a message."
 
 
 
 
 
912
 
913
+ messages = build_messages(
914
+ message,
915
+ history
916
+ )
917
 
918
+ url = (
919
+ f"http://{SERVER_HOST}:"
920
+ f"{SERVER_PORT}/v1/chat/completions"
921
+ )
922
 
923
+ payload = {
924
+ "model":
925
+ "qwen2.5-coder-3b",
926
+ "messages":
927
+ messages,
928
+ "temperature":
929
+ TEMPERATURE,
930
+ "top_p":
931
+ TOP_P,
932
+ "max_tokens":
933
+ MAX_TOKENS,
934
+ "stream":
935
+ False
936
+ }
937
+
938
+ headers = {
939
+ "Content-Type":
940
+ "application/json",
941
+ "Authorization":
942
+ f"Bearer {API_KEY}"
943
+ }
944
 
945
+ try:
 
 
946
 
947
+ response = requests.post(
948
+ url,
949
+ headers=headers,
950
+ json=payload,
951
+ timeout=300
952
+ )
953
 
954
+ except requests.RequestException as e:
 
 
 
 
 
955
 
956
+ return (
957
+ "Server connection error:\n\n"
958
+ f"{e}"
959
+ )
960
 
961
+ if response.status_code != 200:
962
 
963
+ try:
 
 
 
964
 
965
+ data = response.json()
966
 
967
+ return (
968
+ "llama.cpp error:\n\n"
969
+ + json.dumps(
970
+ data,
971
+ indent=2,
972
+ ensure_ascii=False
973
+ )
974
+ )
975
 
976
+ except Exception:
977
 
978
+ return (
979
+ f"llama.cpp HTTP "
980
+ f"{response.status_code}\n\n"
981
+ f"{response.text}"
982
+ )
983
 
984
+ try:
985
 
986
+ data = response.json()
987
+
988
+ choices = data.get(
989
+ "choices",
990
+ []
991
  )
992
 
993
+ if not choices:
994
+
995
+ return "No response generated."
996
 
997
+ choice = choices[0]
998
+
999
+ message_data = choice.get(
1000
+ "message",
1001
+ {}
1002
+ )
1003
 
1004
+ result = message_data.get(
1005
+ "content",
1006
+ ""
1007
  )
1008
 
1009
+ if not result:
 
1010
 
1011
+ result = choice.get(
1012
+ "text",
1013
+ ""
1014
+ )
1015
+
1016
+ return result.strip()
1017
 
1018
+ except Exception as e:
1019
+
1020
+ return (
1021
+ "Response parsing error:\n\n"
1022
+ f"{e}\n\n"
1023
+ f"{response.text}"
1024
  )
1025
 
1026
+
1027
+ def create_ui():
1028
+
1029
+ demo = gr.ChatInterface(
1030
+ fn=generate_response,
1031
+ concurrency_limit=PARALLEL_USERS
1032
+ )
1033
+
1034
  return demo
1035
 
1036
 
1037
+ def initialize():
 
 
1038
 
1039
+ print(
1040
+ "============================================================"
1041
+ )
1042
 
1043
+ print(
1044
+ "Qwen2.5-Coder-3B llama.cpp server"
1045
+ )
1046
 
1047
+ print(
1048
+ "============================================================"
 
1049
  )
1050
 
1051
+ print(
1052
+ f"[CONFIG] Users: {PARALLEL_USERS}"
1053
+ )
1054
+
1055
+ print(
1056
+ f"[CONFIG] Context/user: {CONTEXT_PER_USER}"
 
1057
  )
1058
 
1059
+ print(
1060
+ f"[CONFIG] CPU threads: {CPU_THREADS}"
 
1061
  )
1062
 
1063
+ print(
1064
+ f"[CONFIG] Batch threads: {CPU_THREADS_BATCH}"
 
1065
  )
1066
 
1067
+ print(
1068
+ f"[CONFIG] Batch size: {BATCH_SIZE}"
 
1069
  )
1070
 
1071
+ print(
1072
+ f"[CONFIG] Max tokens: {MAX_TOKENS}"
 
1073
  )
1074
 
1075
+ print(
1076
+ "============================================================"
 
1077
  )
1078
 
1079
+ download_model()
1080
 
1081
+ download_llama_server()
 
1082
 
1083
+ start_llama_server()
1084
 
1085
+ print(
1086
+ "============================================================"
 
 
1087
  )
1088
 
1089
+ print(
1090
+ "INITIALIZATION COMPLETE"
1091
+ )
1092
 
1093
+ print(
1094
+ f"Model: {MODEL_FILE}"
 
 
1095
  )
1096
 
1097
+ print(
1098
+ f"Users: {PARALLEL_USERS}"
1099
+ )
1100
+
1101
+ print(
1102
+ f"Context/user: {CONTEXT_PER_USER}"
1103
+ )
1104
+
1105
+ print(
1106
+ f"CPU threads: {CPU_THREADS}"
1107
+ )
1108
+
1109
+ print(
1110
+ "============================================================"
1111
+ )
1112
+
1113
+
1114
+ def shutdown():
1115
+
1116
+ global server_process
1117
+
1118
+ print(
1119
+ "[SYSTEM] Shutting down..."
1120
+ )
1121
+
1122
+ if server_process is None:
1123
+ return
1124
+
1125
+ try:
1126
+
1127
+ if (
1128
+ server_process.poll()
1129
+ is None
1130
+ ):
1131
+
1132
+ server_process.terminate()
1133
+
1134
+ try:
1135
+
1136
+ server_process.wait(
1137
+ timeout=5
1138
+ )
1139
+
1140
+ except subprocess.TimeoutExpired:
1141
+
1142
+ server_process.kill()
1143
+
1144
+ except Exception as e:
1145
+
1146
+ print(
1147
+ f"[SYSTEM] Shutdown error: {e}"
1148
+ )
1149
 
 
 
 
1150
 
1151
  if __name__ == "__main__":
1152
 
1153
  try:
1154
 
1155
+ initialize()
1156
+
1157
+ demo = create_ui()
1158
+
1159
+ demo.queue(
1160
+ default_concurrency_limit=
1161
+ PARALLEL_USERS
1162
+ )
1163
+
1164
+ demo.launch(
1165
+ server_name=
1166
+ GRADIO_HOST,
1167
+ server_port=
1168
+ GRADIO_PORT,
1169
+ show_error=True,
1170
+ max_threads=
1171
+ PARALLEL_USERS + 4,
1172
+ share=False
1173
+ )
1174
 
1175
  except KeyboardInterrupt:
1176
 
1177
+ print(
1178
  "[SYSTEM] Interrupted."
1179
  )
1180
 
1181
  except Exception as e:
1182
 
1183
+ print(
1184
+ "============================================================"
1185
+ )
1186
+
1187
+ print(
1188
+ "STARTUP FAILED"
1189
+ )
1190
 
1191
+ print(
1192
  repr(e)
1193
  )
1194
 
1195
+ print(
1196
+ "============================================================"
1197
+ )
1198
 
1199
  raise
1200
+
1201
+ finally:
1202
+
1203
+ shutdown()