PyxiLabs commited on
Commit
a069f59
·
verified ·
1 Parent(s): e78fdc7

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +1 -644
main.py CHANGED
@@ -364,649 +364,6 @@ class SpeechRequest(BaseModel):
364
  # ---------------------------------------------------------------------------
365
  # Routes
366
  # ---------------------------------------------------------------------------
367
- html = """
368
- <!DOCTYPE html>
369
- <html lang="en">
370
- <head>
371
- <meta charset="UTF-8" />
372
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
373
- <title>Devil Studio — TTS Demo</title>
374
- <link rel="preconnect" href="https://fonts.googleapis.com" />
375
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
376
- <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet" />
377
-
378
- <style>
379
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
380
-
381
- :root {
382
- --bg: #0a0a0a;
383
- --surface: #111111;
384
- --surface2: #181818;
385
- --border: #242424;
386
- --accent: #ff3c00;
387
- --accent2: #ff6b35;
388
- --muted: #444;
389
- --text: #e8e8e8;
390
- --dim: #666;
391
- --mono: 'DM Mono', monospace;
392
- --sans: 'DM Sans', sans-serif;
393
- --disp: 'Bebas Neue', sans-serif;
394
- --r: 4px;
395
- }
396
-
397
- html { font-size: 16px; scroll-behavior: smooth; }
398
-
399
- body {
400
- background: var(--bg);
401
- color: var(--text);
402
- font-family: var(--sans);
403
- font-weight: 300;
404
- min-height: 100vh;
405
- overflow-x: hidden;
406
- }
407
-
408
- /* noise */
409
- body::before {
410
- content: '';
411
- position: fixed; inset: 0;
412
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
413
- pointer-events: none; z-index: 999; opacity: 0.35;
414
- }
415
-
416
- /* grid */
417
- body::after {
418
- content: '';
419
- position: fixed; inset: 0;
420
- background-image: linear-gradient(rgba(255,60,0,0.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,60,0,0.025) 1px,transparent 1px);
421
- background-size: 40px 40px;
422
- pointer-events: none; z-index: 0;
423
- }
424
-
425
- /* ── Header ──────────────────────────────────────────────────── */
426
- header {
427
- position: relative; z-index: 10;
428
- padding: 36px 48px 0;
429
- display: flex; align-items: flex-start; justify-content: space-between;
430
- animation: fadeDown .6s ease both;
431
- }
432
-
433
- .logo-name {
434
- font-family: var(--disp);
435
- font-size: clamp(40px,6vw,76px);
436
- letter-spacing: .04em; line-height: .9;
437
- background: linear-gradient(135deg,#fff 0%,#ff3c00 55%,#ff6b35 100%);
438
- -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
439
- }
440
-
441
- .logo-sub {
442
- font-family: var(--mono); font-size: 10px;
443
- letter-spacing: .28em; text-transform: uppercase;
444
- color: var(--dim); padding-left: 2px; margin-top: 4px; display: block;
445
- }
446
-
447
- .status-badge {
448
- display: flex; align-items: center; gap: 8px;
449
- font-family: var(--mono); font-size: 11px;
450
- letter-spacing: .1em; color: var(--dim);
451
- text-transform: uppercase; padding-top: 10px;
452
- }
453
-
454
- .dot {
455
- width: 7px; height: 7px; border-radius: 50%;
456
- background: #2a2a2a; transition: background .3s, box-shadow .3s;
457
- }
458
- .dot.online { background: #00e676; box-shadow: 0 0 8px rgba(0,230,118,.6); animation: pulse 2s infinite; }
459
- .dot.error { background: var(--accent); }
460
-
461
- @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.45} }
462
-
463
- /* ── Divider ──────────────────────────────────────────────────── */
464
- .hero {
465
- position: relative; z-index: 10;
466
- padding: 40px 48px 0;
467
- animation: fadeDown .7s .1s ease both;
468
- }
469
- .hero-line {
470
- display: block;
471
- font-family: var(--disp);
472
- font-size: clamp(16px,3vw,34px);
473
- letter-spacing: .1em; color: var(--muted); line-height: 1;
474
- }
475
- .divider {
476
- width: 80px; height: 2px; background: var(--accent);
477
- margin: 20px 0; position: relative;
478
- }
479
- .divider::after {
480
- content: '';
481
- position: absolute; left: 80px; top: 0;
482
- width: 300px; height: 2px;
483
- background: linear-gradient(90deg,var(--accent),transparent); opacity: .18;
484
- }
485
-
486
- /* ── Layout ───────────────────────────────────────────────────── */
487
- main {
488
- position: relative; z-index: 10;
489
- display: grid; grid-template-columns: 1fr 320px;
490
- gap: 2px; padding: 28px 48px 48px; max-width: 1300px;
491
- }
492
-
493
- @media(max-width:860px){
494
- main { grid-template-columns:1fr; padding:20px; }
495
- header,.hero { padding-left:20px; padding-right:20px; }
496
- }
497
-
498
- /* ── Panel ────────────────────────────────────────────────────── */
499
- .panel {
500
- background: var(--surface); border: 1px solid var(--border);
501
- padding: 26px; position: relative;
502
- animation: fadeUp .6s .2s ease both;
503
- }
504
- .panel-r { border-left: none; animation-delay: .32s; display:flex; flex-direction:column; gap:22px; }
505
-
506
- @media(max-width:860px){
507
- .panel-r { border-left:1px solid var(--border); border-top:none; }
508
- }
509
-
510
- .sec-label {
511
- font-family: var(--mono); font-size: 10px;
512
- letter-spacing: .28em; text-transform: uppercase; color: var(--dim);
513
- margin-bottom: 14px;
514
- display: flex; align-items: center; gap: 10px;
515
- }
516
- .sec-label::after { content:''; flex:1; height:1px; background:var(--border); }
517
-
518
- /* ── Textarea ─────────────────────────────────────────────────── */
519
- .ta-wrap { position: relative; }
520
-
521
- textarea {
522
- width: 100%; min-height: 170px;
523
- background: var(--surface2); border: 1px solid var(--border);
524
- color: var(--text); font-family: var(--sans); font-size: 15px;
525
- font-weight: 300; line-height: 1.75; padding: 14px 16px;
526
- resize: vertical; outline: none; border-radius: var(--r);
527
- transition: border-color .2s; caret-color: var(--accent);
528
- }
529
- textarea:focus { border-color: var(--accent); }
530
- textarea::placeholder { color: var(--dim); }
531
-
532
- .cc {
533
- position: absolute; bottom: 10px; right: 12px;
534
- font-family: var(--mono); font-size: 10px; color: var(--dim); pointer-events: none;
535
- }
536
-
537
- /* ── Speed ────────────────────────────────────────────────────── */
538
- .speed-row {
539
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;
540
- }
541
- .speed-label { font-family:var(--mono); font-size:10px; letter-spacing:.22em; text-transform:uppercase; color:var(--dim); }
542
- .speed-num { font-family:var(--mono); font-size:18px; font-weight:500; color:var(--accent); }
543
-
544
- input[type=range] {
545
- -webkit-appearance:none; appearance:none;
546
- width:100%; height:2px; background:var(--border); outline:none; cursor:pointer; border-radius:1px; display:block;
547
- }
548
- input[type=range]::-webkit-slider-thumb {
549
- -webkit-appearance:none; width:15px; height:15px; border-radius:50%;
550
- background:var(--accent); cursor:pointer;
551
- box-shadow:0 0 8px rgba(255,60,0,.5); transition:box-shadow .2s,transform .1s;
552
- }
553
- input[type=range]::-webkit-slider-thumb:hover { box-shadow:0 0 16px rgba(255,60,0,.8); transform:scale(1.2); }
554
-
555
- .speed-marks {
556
- display:flex; justify-content:space-between;
557
- font-family:var(--mono); font-size:9px; color:var(--dim); margin-top:5px;
558
- }
559
-
560
- /* ── Generate btn ─────────────────────────────────────────────── */
561
- .btn-gen {
562
- width:100%; margin-top:18px; padding:15px;
563
- background:var(--accent); border:none; color:#fff;
564
- font-family:var(--disp); font-size:20px; letter-spacing:.12em;
565
- cursor:pointer; border-radius:var(--r); position:relative; overflow:hidden;
566
- transition:background .2s,transform .1s;
567
- }
568
- .btn-gen::before {
569
- content:''; position:absolute; inset:0;
570
- background:linear-gradient(135deg,rgba(255,255,255,.1),transparent 55%); pointer-events:none;
571
- }
572
- .btn-gen:hover { background:var(--accent2); }
573
- .btn-gen:active { transform:scale(.99); }
574
- .btn-gen:disabled { background:var(--muted); cursor:not-allowed; transform:none; }
575
-
576
- .btn-inner { display:inline-flex; align-items:center; gap:10px; }
577
- .spinner {
578
- display:none; width:15px; height:15px;
579
- border:2px solid rgba(255,255,255,.3); border-top-color:#fff;
580
- border-radius:50%; animation:spin .7s linear infinite;
581
- }
582
- .btn-gen.loading .spinner { display:block; }
583
- .btn-gen.loading .blabel { opacity:.7; }
584
- @keyframes spin { to{transform:rotate(360deg)} }
585
-
586
- /* ── Audio output ─────────────────────────────────────────────── */
587
- .out-section { margin-top:18px; }
588
-
589
- .wvz {
590
- background:var(--surface2); border:1px solid var(--border);
591
- border-radius:var(--r); height:72px;
592
- display:flex; align-items:center; justify-content:center;
593
- overflow:hidden; position:relative; margin-bottom:10px;
594
- }
595
-
596
- .wvz-ph { font-family:var(--mono); font-size:11px; color:var(--dim); letter-spacing:.2em; text-transform:uppercase; }
597
-
598
- .wvz-bars { display:none; align-items:center; gap:2px; height:100%; padding:10px 16px; }
599
- .wvz-bars.on { display:flex; }
600
-
601
- .bar {
602
- width:3px; border-radius:2px; background:var(--accent); opacity:.7;
603
- animation:wa 1.2s ease-in-out infinite;
604
- }
605
- @keyframes wa { 0%,100%{transform:scaleY(.25)} 50%{transform:scaleY(1)} }
606
-
607
- audio {
608
- width:100%; height:38px; outline:none; border-radius:var(--r);
609
- filter:invert(1) hue-rotate(180deg); opacity:.8;
610
- }
611
-
612
- .act-row { display:flex; gap:7px; margin-top:9px; }
613
-
614
- .act-btn {
615
- flex:1; padding:9px; background:var(--surface2); border:1px solid var(--border);
616
- color:var(--dim); font-family:var(--mono); font-size:11px;
617
- letter-spacing:.1em; text-transform:uppercase; cursor:pointer;
618
- border-radius:var(--r); transition:all .15s;
619
- display:flex; align-items:center; justify-content:center; gap:6px;
620
- text-decoration:none;
621
- }
622
- .act-btn:hover { border-color:var(--accent); color:var(--accent); }
623
-
624
- /* ── Right panel pieces ───────────────────────────────────────── */
625
- .model-cards { display:flex; flex-direction:column; gap:5px; }
626
-
627
- .mc {
628
- background:var(--surface2); border:1px solid var(--border);
629
- border-radius:var(--r); padding:10px 13px;
630
- cursor:pointer; transition:border-color .2s,background .2s;
631
- display:flex; align-items:center; justify-content:space-between;
632
- }
633
- .mc:hover { border-color:var(--muted); }
634
- .mc.sel { border-color:var(--accent); background:rgba(255,60,0,.06); }
635
- .mc-name { font-family:var(--mono); font-size:12px; font-weight:500; color:var(--text); letter-spacing:.04em; }
636
- .mc-desc { font-size:11px; color:var(--dim); margin-top:2px; }
637
- .mc-badge {
638
- font-family:var(--mono); font-size:9px; letter-spacing:.1em; text-transform:uppercase;
639
- padding:3px 7px; border-radius:2px; border:1px solid var(--border); color:var(--dim);
640
- transition:border-color .2s,color .2s;
641
- }
642
- .mc.sel .mc-badge { border-color:var(--accent); color:var(--accent); }
643
-
644
- .voice-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:5px; }
645
-
646
- .vbtn {
647
- background:var(--surface2); border:1px solid var(--border);
648
- color:var(--dim); font-family:var(--mono); font-size:11px;
649
- padding:9px 4px; text-align:center; cursor:pointer;
650
- border-radius:var(--r); transition:all .15s; letter-spacing:.04em;
651
- }
652
- .vbtn:hover { border-color:var(--muted); color:var(--text); }
653
- .vbtn.sel { border-color:var(--accent); color:var(--accent); background:rgba(255,60,0,.06); }
654
-
655
- .fmt-chips { display:flex; gap:5px; flex-wrap:wrap; }
656
-
657
- .chip {
658
- font-family:var(--mono); font-size:11px; letter-spacing:.1em;
659
- text-transform:uppercase; padding:6px 13px;
660
- border:1px solid var(--border); border-radius:var(--r);
661
- cursor:pointer; color:var(--dim); background:var(--surface2); transition:all .15s;
662
- }
663
- .chip:hover { border-color:var(--muted); color:var(--text); }
664
- .chip.sel { border-color:var(--accent); color:var(--accent); background:rgba(255,60,0,.06); }
665
-
666
- .srv-info { font-family:var(--mono); font-size:11px; color:var(--dim); line-height:2; }
667
-
668
- /* ── Status bar ───────────────────────────────────────────────── */
669
- .sbar {
670
- grid-column:1/-1; border:1px solid var(--border); border-top:none;
671
- background:var(--surface); padding:9px 26px;
672
- display:flex; align-items:center; gap:22px; flex-wrap:wrap;
673
- font-family:var(--mono); font-size:10px; color:var(--dim); letter-spacing:.07em;
674
- animation:fadeUp .6s .5s ease both;
675
- }
676
- .si { display:flex; align-items:center; gap:5px; }
677
- .si .k { color:var(--muted); }
678
- .si .v { color:var(--text); }
679
- .si .v.ac { color:var(--accent); }
680
-
681
- .mbars { display:flex; align-items:flex-end; gap:2px; margin-left:auto; }
682
- .mb { width:3px; height:10px; background:var(--border); border-radius:1px; transition:background .3s; }
683
- .mb.lit { background:#00e676; }
684
-
685
- /* ── Toast ────────────────────────────────────────────────────── */
686
- .toast {
687
- position:fixed; bottom:22px; right:22px;
688
- background:#1a0a0a; border:1px solid var(--accent);
689
- color:var(--text); font-family:var(--mono); font-size:12px;
690
- padding:13px 17px; border-radius:var(--r); z-index:1000;
691
- max-width:300px; transform:translateY(70px); opacity:0;
692
- transition:all .3s ease; box-shadow:0 0 20px rgba(255,60,0,.15);
693
- }
694
- .toast.show { transform:translateY(0); opacity:1; }
695
-
696
- @keyframes fadeDown { from{opacity:0;transform:translateY(-14px)} to{opacity:1;transform:translateY(0)} }
697
- @keyframes fadeUp { from{opacity:0;transform:translateY(14px)} to{opacity:1;transform:translateY(0)} }
698
- </style>
699
- </head>
700
- <body>
701
-
702
- <header>
703
- <div>
704
- <div class="logo-name">DEVIL STUDIO</div>
705
- <span class="logo-sub">Text · to · Speech · API &nbsp;/&nbsp; v1.0.0</span>
706
- </div>
707
- <div class="status-badge">
708
- <div class="dot" id="dot"></div>
709
- <span id="statusTxt">Connecting…</span>
710
- </div>
711
- </header>
712
-
713
- <div class="hero">
714
- <span class="hero-line">SYNTHESISE SPEECH.</span>
715
- <span class="hero-line">INSTANTLY.</span>
716
- <div class="divider"></div>
717
- </div>
718
-
719
- <main>
720
-
721
- <!-- ── Left panel ──────────────────────────────────────────── -->
722
- <div class="panel">
723
- <div class="sec-label">Input</div>
724
-
725
- <div class="ta-wrap">
726
- <textarea id="tin" placeholder="Type or paste your text here…" maxlength="5000" spellcheck="true">Devil Studio delivers low-latency, high-quality speech synthesis powered by KittenTTS — three models, eight voices, permanently loaded in memory and ready to respond.</textarea>
727
- <span class="cc"><span id="cc">0</span> / 5000</span>
728
- </div>
729
-
730
- <div style="margin-top:18px;">
731
- <div class="speed-row">
732
- <span class="speed-label">Speed</span>
733
- <span class="speed-num" id="sv">1.00×</span>
734
- </div>
735
- <input type="range" id="spd" min="0.25" max="4" step="0.05" value="1.0" />
736
- <div class="speed-marks">
737
- <span>0.25×</span><span>1×</span><span>2×</span><span>3×</span><span>4×</span>
738
- </div>
739
- </div>
740
-
741
- <button class="btn-gen" id="genBtn" onclick="generate()">
742
- <span class="btn-inner">
743
- <div class="spinner"></div>
744
- <span class="blabel">GENERATE SPEECH</span>
745
- </span>
746
- </button>
747
-
748
- <!-- Output -->
749
- <div class="out-section" id="outSection" style="display:none;">
750
- <div style="height:14px;"></div>
751
- <div class="sec-label">Output</div>
752
- <div class="wvz">
753
- <span class="wvz-ph" id="wvph">AWAITING SIGNAL</span>
754
- <div class="wvz-bars" id="wvbars"></div>
755
- </div>
756
- <audio id="ap" controls></audio>
757
- <div class="act-row">
758
- <a class="act-btn" id="dlBtn" href="#" download="speech.wav">↓ Download</a>
759
- <button class="act-btn" onclick="copyCurl()">⌘ Copy cURL</button>
760
- </div>
761
- </div>
762
- </div>
763
-
764
- <!-- ── Right panel ─────────────────────────────────────────── -->
765
- <div class="panel panel-r">
766
-
767
- <div>
768
- <div class="sec-label">Model</div>
769
- <div class="model-cards">
770
- <div class="mc sel" data-m="tts-1" onclick="selModel(this)">
771
- <div><div class="mc-name">tts-1</div><div class="mc-desc">Nano · 15M · Fastest</div></div>
772
- <span class="mc-badge">Speed</span>
773
- </div>
774
- <div class="mc" data-m="tts-1-hd" onclick="selModel(this)">
775
- <div><div class="mc-name">tts-1-hd</div><div class="mc-desc">Micro · 40M · Balanced</div></div>
776
- <span class="mc-badge">Balance</span>
777
- </div>
778
- <div class="mc" data-m="tts-1-hd-mini" onclick="selModel(this)">
779
- <div><div class="mc-name">tts-1-hd-mini</div><div class="mc-desc">Mini · 80M · Best Quality</div></div>
780
- <span class="mc-badge">Quality</span>
781
- </div>
782
- </div>
783
- </div>
784
-
785
- <div>
786
- <div class="sec-label">Voice</div>
787
- <div class="voice-grid">
788
- <button class="vbtn sel" data-v="Jasper" onclick="selVoice(this)">Jasper</button>
789
- <button class="vbtn" data-v="Bella" onclick="selVoice(this)">Bella</button>
790
- <button class="vbtn" data-v="Luna" onclick="selVoice(this)">Luna</button>
791
- <button class="vbtn" data-v="Bruno" onclick="selVoice(this)">Bruno</button>
792
- <button class="vbtn" data-v="Rosie" onclick="selVoice(this)">Rosie</button>
793
- <button class="vbtn" data-v="Hugo" onclick="selVoice(this)">Hugo</button>
794
- <button class="vbtn" data-v="Kiki" onclick="selVoice(this)">Kiki</button>
795
- <button class="vbtn" data-v="Leo" onclick="selVoice(this)">Leo</button>
796
- </div>
797
- </div>
798
-
799
- <div>
800
- <div class="sec-label">Format</div>
801
- <div class="fmt-chips">
802
- <div class="chip sel" data-f="wav" onclick="selFmt(this)">WAV</div>
803
- <div class="chip" data-f="flac" onclick="selFmt(this)">FLAC</div>
804
- <div class="chip" data-f="pcm" onclick="selFmt(this)">PCM</div>
805
- <div class="chip" data-f="mp3" onclick="selFmt(this)">MP3*</div>
806
- </div>
807
- <p style="font-size:10px;color:var(--dim);margin-top:7px;font-family:var(--mono);">
808
- * MP3 / OPUS / AAC served as WAV (ffmpeg not bundled)
809
- </p>
810
- </div>
811
-
812
- <div>
813
- <div class="sec-label">Server</div>
814
- <div class="srv-info" id="srvInfo">Fetching status…</div>
815
- </div>
816
-
817
- </div>
818
-
819
- <!-- ── Status bar ──────────────────────────────────────────── -->
820
- <div class="sbar">
821
- <div class="si"><span class="k">ENDPOINT</span>&nbsp;<span class="v">pyxilabs-srv-tts-01.hf.space</span></div>
822
- <div class="si"><span class="k">LATENCY</span>&nbsp;<span class="v ac" id="latD">—</span></div>
823
- <div class="si"><span class="k">LAST GEN</span>&nbsp;<span class="v" id="lgD">—</span></div>
824
- <div class="mbars" id="mbars">
825
- <div class="mb"></div><div class="mb"></div><div class="mb"></div>
826
- <div class="mb"></div><div class="mb"></div>
827
- </div>
828
- </div>
829
-
830
- </main>
831
-
832
- <div class="toast" id="toast"></div>
833
-
834
- <script>
835
- const API = 'https://pyxilabs-srv-tts-01.hf.space';
836
-
837
- let model = 'tts-1';
838
- let voice = 'Jasper';
839
- let fmt = 'wav';
840
- let blobUrl = null;
841
-
842
- // refs
843
- const tin = document.getElementById('tin');
844
- const ccEl = document.getElementById('cc');
845
- const spd = document.getElementById('spd');
846
- const svEl = document.getElementById('sv');
847
- const genBtn = document.getElementById('genBtn');
848
- const outSec = document.getElementById('outSection');
849
- const ap = document.getElementById('ap');
850
- const dlBtn = document.getElementById('dlBtn');
851
- const wvbars = document.getElementById('wvbars');
852
- const wvph = document.getElementById('wvph');
853
- const latD = document.getElementById('latD');
854
- const lgD = document.getElementById('lgD');
855
- const dotEl = document.getElementById('dot');
856
- const stxtEl = document.getElementById('statusTxt');
857
- const toastEl = document.getElementById('toast');
858
- const srvInfo = document.getElementById('srvInfo');
859
- const mbarsEl = document.getElementById('mbars');
860
-
861
- // char count
862
- tin.addEventListener('input', () => ccEl.textContent = tin.value.length);
863
- ccEl.textContent = tin.value.length;
864
-
865
- // speed
866
- spd.addEventListener('input', () => svEl.textContent = (+spd.value).toFixed(2) + '×');
867
-
868
- // selections
869
- function selModel(el) {
870
- document.querySelectorAll('.mc').forEach(e => e.classList.remove('sel'));
871
- el.classList.add('sel'); model = el.dataset.m;
872
- }
873
- function selVoice(el) {
874
- document.querySelectorAll('.vbtn').forEach(e => e.classList.remove('sel'));
875
- el.classList.add('sel'); voice = el.dataset.v;
876
- }
877
- function selFmt(el) {
878
- document.querySelectorAll('.chip').forEach(e => e.classList.remove('sel'));
879
- el.classList.add('sel'); fmt = el.dataset.f;
880
- }
881
-
882
- // waveform
883
- function buildWave() {
884
- wvbars.innerHTML = '';
885
- for (let i = 0; i < 58; i++) {
886
- const b = document.createElement('div');
887
- b.className = 'bar';
888
- b.style.height = (20 + Math.random() * 60) + '%';
889
- b.style.animationDelay = (i / 58 * 1.2) + 's';
890
- wvbars.appendChild(b);
891
- }
892
- }
893
-
894
- // latency meter
895
- function setMeter(ms) {
896
- const bars = mbarsEl.querySelectorAll('.mb');
897
- const lit = ms < 500 ? 5 : ms < 1000 ? 4 : ms < 2000 ? 3 : ms < 3500 ? 2 : 1;
898
- bars.forEach((b, i) => b.classList.toggle('lit', (5 - i) <= lit));
899
- }
900
-
901
- // toast
902
- let tt;
903
- function toast(msg, dur = 3500) {
904
- toastEl.textContent = msg;
905
- toastEl.classList.add('show');
906
- clearTimeout(tt);
907
- tt = setTimeout(() => toastEl.classList.remove('show'), dur);
908
- }
909
-
910
- // copy curl
911
- function copyCurl() {
912
- const txt = (tin.value.trim() || 'Hello!').replace(/'/g, "\\'");
913
- const speed = (+spd.value).toFixed(2);
914
- const cmd = `curl -X POST ${API}/v1/audio/speech \\\n -H "Content-Type: application/json" \\\n -d '{"model":"${model}","input":"${txt}","voice":"${voice}","response_format":"${fmt}","speed":${speed}}' \\\n --output speech.${fmt}`;
915
- navigator.clipboard.writeText(cmd)
916
- .then(() => toast('⌘ cURL command copied!'))
917
- .catch(() => toast('Copy failed.'));
918
- }
919
-
920
- // status
921
- async function checkStatus() {
922
- try {
923
- const r = await fetch(`${API}/v1/status`);
924
- if (!r.ok) throw new Error();
925
- const d = await r.json();
926
- dotEl.className = 'dot online';
927
- stxtEl.textContent = 'Online';
928
-
929
- const models = (d.models || []).map(m => {
930
- const col = m.status === 'idle' ? '#00e676' : m.status === 'running' ? 'var(--accent)' : 'var(--dim)';
931
- return `<span style="color:${col}">■</span> ${m.name} <span style="color:var(--text)">${m.status.toUpperCase()}</span>`;
932
- }).join('<br>');
933
-
934
- const sys = d.system || {};
935
- const mem = sys.memory || {};
936
- srvInfo.innerHTML = `${models}<br>CPU <span style="color:var(--text)">${sys.cpu_usage_percent ?? '—'}%</span> &nbsp;·&nbsp; MEM <span style="color:var(--text)">${mem.used_mb ?? '—'} / ${mem.total_mb ?? '—'} MB</span> &nbsp;·&nbsp; UP <span style="color:var(--text)">${d.uptime ?? '—'}</span>`;
937
- } catch {
938
- dotEl.className = 'dot error';
939
- stxtEl.textContent = 'Offline';
940
- srvInfo.innerHTML = '<span style="color:var(--accent)">Cannot reach server</span>';
941
- }
942
- }
943
-
944
- // generate
945
- async function generate() {
946
- const text = tin.value.trim();
947
- if (!text) { toast('⚠ Please enter some text.'); return; }
948
-
949
- genBtn.disabled = true;
950
- genBtn.classList.add('loading');
951
- genBtn.querySelector('.blabel').textContent = 'SYNTHESISING…';
952
-
953
- const t0 = performance.now();
954
- try {
955
- const res = await fetch(`${API}/v1/audio/speech`, {
956
- method: 'POST',
957
- headers: { 'Content-Type': 'application/json' },
958
- body: JSON.stringify({ model, input: text, voice, response_format: fmt, speed: +spd.value }),
959
- });
960
-
961
- if (!res.ok) {
962
- const e = await res.json().catch(() => ({ detail: res.statusText }));
963
- throw new Error(e.detail || `HTTP ${res.status}`);
964
- }
965
-
966
- const blob = await res.blob();
967
- const ms = Math.round(performance.now() - t0);
968
-
969
- if (blobUrl) URL.revokeObjectURL(blobUrl);
970
- blobUrl = URL.createObjectURL(blob);
971
-
972
- ap.src = blobUrl; ap.load(); ap.play().catch(() => {});
973
-
974
- const ext = fmt === 'mp3' ? 'wav' : fmt;
975
- dlBtn.href = blobUrl;
976
- dlBtn.download = `devil-studio-${voice.toLowerCase()}.${ext}`;
977
-
978
- outSec.style.display = 'block';
979
- buildWave();
980
- wvph.style.display = 'none';
981
- wvbars.classList.add('on');
982
-
983
- const latStr = ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : ms + 'ms';
984
- latD.textContent = latStr;
985
- lgD.textContent = new Date().toLocaleTimeString();
986
- setMeter(ms);
987
-
988
- checkStatus();
989
- } catch (err) {
990
- toast('✗ ' + err.message, 5000);
991
- } finally {
992
- genBtn.disabled = false;
993
- genBtn.classList.remove('loading');
994
- genBtn.querySelector('.blabel').textContent = 'GENERATE SPEECH';
995
- }
996
- }
997
-
998
- document.addEventListener('keydown', e => { if ((e.ctrlKey||e.metaKey) && e.key==='Enter') generate(); });
999
-
1000
- checkStatus();
1001
- setInterval(checkStatus, 30000);
1002
- </script>
1003
- </body>
1004
- </html>
1005
- """
1006
- @app.get("/", include_in_schema=False)
1007
- async def index():
1008
- return HTMLResponse(content=html)
1009
-
1010
  @app.get("/health", tags=["Utility"], summary="Liveness probe")
1011
  async def health():
1012
  return {"status": "ok", "server": "Devil Studio"}
@@ -1127,6 +484,6 @@ if __name__ == "__main__":
1127
  "main:app",
1128
  host="0.0.0.0",
1129
  port=int(os.getenv("PORT", "7860")),
1130
- workers=1, # single worker — all models live in one process
1131
  log_level="info",
1132
  )
 
364
  # ---------------------------------------------------------------------------
365
  # Routes
366
  # ---------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  @app.get("/health", tags=["Utility"], summary="Liveness probe")
368
  async def health():
369
  return {"status": "ok", "server": "Devil Studio"}
 
484
  "main:app",
485
  host="0.0.0.0",
486
  port=int(os.getenv("PORT", "7860")),
487
+ workers=2,
488
  log_level="info",
489
  )