-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfrontend-ui.h
More file actions
1884 lines (1791 loc) · 134 KB
/
Copy pathfrontend-ui.h
File metadata and controls
1884 lines (1791 loc) · 134 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#pragma once
// NOTE: This file defines a member function of FrontendServer.
// It must be included AFTER the full class definition in frontend.cpp.
// It is not self-contained and cannot be used as a standalone header.
#include <string>
inline std::string FrontendServer::build_ui_pages() {
return R"PG(
<div class="wt-page active" id="page-dashboard">
<div class="wt-content">
<div class="wt-pipeline-hero" style="border-radius:var(--wt-radius-lg);position:relative">
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.04) 2px,rgba(0,0,0,0.04) 4px);pointer-events:none;border-radius:inherit;z-index:0"></div>
<div style="position:relative;z-index:1">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
<div>
<div class="wt-headline" style="color:#fff;text-shadow:0 0 20px rgba(255,45,149,0.4)">Pipeline Overview</div>
<div style="font-size:11px;opacity:0.5;margin-top:4px;font-family:var(--wt-body);letter-spacing:0.05em">REAL-TIME SERVICE HEALTH & DATA FLOW</div>
</div>
<div id="dashHealthBadge" style="padding:6px 16px;border-radius:var(--wt-radius);font-size:11px;font-weight:700;font-family:var(--wt-font);background:rgba(0,0,0,0.4);color:var(--wt-accent-cyan);border:1px solid rgba(0,255,245,0.3);letter-spacing:0.05em">CHECKING...</div>
</div>
<div class="pfu-topo-badges">
<span style="font-size:8px;padding:2px 8px;border-radius:10px;font-family:var(--wt-font);color:rgba(0,255,245,0.7);background:rgba(0,255,245,0.07);border:1px solid rgba(0,255,245,0.25);letter-spacing:0.08em" id="pfu-badge-neural">⚡ NEURAL • MOSHI-RAG</span>
<span style="font-size:8px;padding:2px 8px;border-radius:10px;font-family:var(--wt-font);color:rgba(255,45,149,0.5);background:rgba(255,45,149,0.05);border:1px solid rgba(255,45,149,0.2);letter-spacing:0.08em" id="pfu-badge-classic">CLASSIC • HALF-DUPLEX</span>
</div>
<div class="pfu-topo-wrap">
<svg class="pfu-topo-svg" viewBox="0 0 860 340" preserveAspectRatio="xMidYMid meet">
<defs>
<marker id="pfu-arr-n" markerWidth="8" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="rgba(0,255,245,0.85)"/></marker>
<marker id="pfu-arr-n2" markerWidth="8" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="rgba(0,255,245,0.6)"/></marker>
<marker id="pfu-arr-c" markerWidth="8" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="rgba(255,45,149,0.7)"/></marker>
<marker id="pfu-arr-v" markerWidth="8" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="rgba(176,38,255,0.8)"/></marker>
<marker id="pfu-arr-o" markerWidth="8" markerHeight="6" refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="rgba(255,184,0,0.6)"/></marker>
<filter id="pfu-gln" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
<filter id="pfu-glv" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
</defs>
<line x1="37" y1="112" x2="37" y2="155" stroke="rgba(176,38,255,0.45)" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#pfu-arr-v)"/>
<text x="42" y="137" fill="rgba(176,38,255,0.35)" font-size="8" font-family="Space Mono,monospace">G.711</text>
<line x1="71" y1="178" x2="261" y2="116" stroke="rgba(0,255,245,0.75)" stroke-width="2" filter="url(#pfu-gln)" marker-end="url(#pfu-arr-n)"/>
<text x="140" y="133" fill="rgba(0,255,245,0.45)" font-size="8" font-family="Space Mono,monospace">PCM 16kHz</text>
<line x1="68" y1="196" x2="210" y2="248" stroke="rgba(255,45,149,0.45)" stroke-width="1.5" stroke-dasharray="5,2" marker-end="url(#pfu-arr-c)"/>
<text x="106" y="235" fill="rgba(255,45,149,0.32)" font-size="8" font-family="Space Mono,monospace">PCM</text>
<path d="M 329 102 Q 418 62 476 80" stroke="rgba(176,38,255,0.8)" stroke-width="1.5" fill="none" filter="url(#pfu-glv)" marker-end="url(#pfu-arr-v)"/>
<text x="378" y="60" fill="rgba(176,38,255,0.6)" font-size="8" font-family="Space Mono,monospace">text →</text>
<path d="M 476 90 Q 418 128 329 122" stroke="rgba(0,255,245,0.55)" stroke-width="1.5" fill="none" filter="url(#pfu-gln)" marker-end="url(#pfu-arr-n2)"/>
<text x="368" y="134" fill="rgba(0,255,245,0.38)" font-size="8" font-family="Space Mono,monospace">← inject</text>
<line x1="329" y1="120" x2="745" y2="152" stroke="rgba(0,255,245,0.65)" stroke-width="2" filter="url(#pfu-gln)" marker-end="url(#pfu-arr-n)"/>
<text x="512" y="124" fill="rgba(0,255,245,0.38)" font-size="8" font-family="Space Mono,monospace">PCM</text>
<line x1="274" y1="253" x2="348" y2="253" stroke="rgba(255,45,149,0.55)" stroke-width="1.5" marker-end="url(#pfu-arr-c)"/>
<line x1="416" y1="253" x2="493" y2="253" stroke="rgba(255,45,149,0.55)" stroke-width="1.5" marker-end="url(#pfu-arr-c)"/>
<line x1="561" y1="253" x2="638" y2="253" stroke="rgba(255,45,149,0.55)" stroke-width="1.5" marker-end="url(#pfu-arr-c)"/>
<line x1="706" y1="246" x2="779" y2="166" stroke="rgba(255,45,149,0.5)" stroke-width="1.5" marker-end="url(#pfu-arr-c)"/>
<line x1="510" y1="108" x2="520" y2="268" stroke="rgba(176,38,255,0.38)" stroke-width="1.2" stroke-dasharray="4,3" marker-end="url(#pfu-arr-v)"/>
<text x="527" y="196" fill="rgba(176,38,255,0.6)" font-size="7" font-family="Space Mono,monospace">query</text>
<path d="M 648 283 Q 605 268 561 263" stroke="rgba(255,184,0,0.4)" stroke-width="1.2" fill="none" stroke-dasharray="3,3" marker-end="url(#pfu-arr-o)"/>
<path d="M 660 270 Q 592 198 518 106" stroke="rgba(255,184,0,0.3)" stroke-width="1" fill="none" stroke-dasharray="3,4" marker-end="url(#pfu-arr-o)"/>
<text x="607" y="200" fill="rgba(255,184,0,0.6)" font-size="7" font-family="Space Mono,monospace">opt.</text>
<line x1="783" y1="130" x2="71" y2="80" stroke="rgba(176,38,255,0.2)" stroke-width="1" stroke-dasharray="3,5"/>
</svg>
<div class="pfu-topo-zone" style="left:0.5%;top:1%">⚡ NEURAL</div>
<div class="pfu-topo-zone" style="left:0.5%;bottom:1%">CLASSIC</div>
<div class="pfu-topo-zone" style="right:0.5%;bottom:1%">EXTERNAL</div>
<div class="pfu-topo-node pfu-topo-shared" id="pipeline-node-SIP_CLIENT" style="left:4.3%;top:24.1%" title="SIP Client — G.711 telephony in/out">
<span class="node-label">SIP</span><span class="node-sub">G.711</span><span class="node-status offline" id="pipeline-status-SIP_CLIENT"></span>
</div>
<div class="pfu-topo-node pfu-topo-shared" id="pipeline-node-INBOUND_AUDIO_PROCESSOR" style="left:4.3%;top:54.4%" title="Inbound Audio Processor — decodes G.711 to PCM 16kHz">
<span class="node-label">IAP</span><span class="node-sub">PCM 16kHz</span><span class="node-status offline" id="pipeline-status-INBOUND_AUDIO_PROCESSOR"></span>
</div>
<div class="pfu-topo-node pfu-topo-neural" id="pipeline-node-MOSHI_SERVICE" style="left:34.3%;top:32.4%;width:80px;height:74px" title="Moshi Full-Duplex Neural Voice — ~80ms end-to-end. Replaces VAD → ASR → LLM → TTS.">
<span class="node-label">MSH</span>
<div class="pfu-topo-msh-chips"><span class="pfu-topo-chip pfu-topo-chip-stt">STT</span><span class="pfu-topo-chip pfu-topo-chip-lm">LM</span></div>
<span class="node-sub" id="mosh-stt-badge">LM</span>
<span class="pfu-topo-swap">or PersonaPlex</span>
<span class="node-status offline" id="pipeline-status-MOSHI_SERVICE"></span>
</div>
<div id="pfu-rag-backend-row" style="position:absolute;left:59.3%;top:22.9%;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;gap:2px;z-index:2">
<span style="font-size:6px;color:rgba(176,38,255,0.55);letter-spacing:0.07em;font-family:var(--wt-font)">⇄ WS</span>
<div class="pfu-topo-node pfu-topo-ragbe" id="pipeline-node-MOSHI_RAG_BACKEND" style="position:static;transform:scale(1)" title="moshi-rag-backend-service — receives text token stream via WebSocket, dispatches triggers and returns reference injections.">
<span class="node-label">RAG-BE</span><span class="node-sub">WS :8090</span><span class="node-status offline" id="pipeline-status-MOSHI_RAG_BACKEND"></span>
</div>
</div>
<div class="pfu-topo-node pfu-topo-shared" id="pipeline-node-OUTBOUND_AUDIO_PROCESSOR" style="left:91%;top:47.1%" title="Outbound Audio Processor — encodes PCM to G.711 for SIP">
<span class="node-label">OAP</span><span class="node-sub">PCM→G.711</span><span class="node-status offline" id="pipeline-status-OUTBOUND_AUDIO_PROCESSOR"></span>
</div>
<div class="pfu-topo-node pfu-topo-classic" id="pipeline-node-VAD_SERVICE" style="left:27.9%;top:74.4%" title="Voice Activity Detection">
<span class="node-label">VAD</span><span class="node-sub">silence det.</span><span class="node-status offline" id="pipeline-status-VAD_SERVICE"></span>
</div>
<div class="pfu-topo-node pfu-topo-classic" id="pipeline-node-WHISPER_SERVICE" style="left:44.8%;top:74.4%" title="Automatic Speech Recognition — Whisper">
<span class="node-label">ASR</span><span class="node-sub">Whisper</span><span class="node-status offline" id="pipeline-status-WHISPER_SERVICE"></span>
</div>
<div class="pfu-topo-node pfu-topo-classic" id="pipeline-node-LLAMA_SERVICE" style="left:61.6%;top:74.4%" title="Large Language Model — LLaMA">
<span class="node-label">LLM</span><span class="node-sub">LLaMA</span><span class="node-status offline" id="pipeline-status-LLAMA_SERVICE"></span>
</div>
<div class="pfu-topo-node pfu-topo-classic" id="pipeline-node-TTS_SERVICE" style="left:78.5%;top:74.4%" title="Text-to-Speech synthesis">
<span class="node-label">TTS</span><span class="node-status-sub" id="pipeline-tts-engine" style="display:block;font-size:7px;margin-top:1px;color:rgba(255,255,255,0.65);letter-spacing:0.04em">no engine</span><span class="node-status offline" id="pipeline-status-TTS_SERVICE"></span>
</div>
<div class="pfu-topo-node pfu-topo-crawl" id="pipeline-node-TOMEDO_CRAWL_SERVICE" style="left:60.5%;top:88.2%" title="Tomedo crawl — semantic patient data search">
<span class="node-label" style="font-size:8px">crawl</span><span class="node-sub">:13181</span><span class="node-status offline" id="pipeline-status-TOMEDO_CRAWL_SERVICE"></span>
</div>
<div class="pfu-topo-node pfu-topo-ext" id="pipeline-node-OLLAMA" style="left:79.1%;top:88.2%" title="Ollama — local LLM for RAG retrieval, TTS embeddings, classic LLM inference">
<span class="node-label">Ollama</span><span class="node-sub">:11434</span><span class="node-status offline" id="pipeline-status-OLLAMA"></span>
</div>
<span id="ragDashInfo" style="position:absolute;bottom:4px;left:50%;transform:translateX(-50%);font-size:8px;color:rgba(255,255,255,0.28);letter-spacing:0.05em;white-space:nowrap;z-index:1;pointer-events:none"></span>
</div>
<div class="pfu-topo-legend">
<div class="pfu-topo-legend-item"><div class="pfu-topo-leg-line" style="background:rgba(0,255,245,0.7)"></div><span>Neural PCM/audio</span></div>
<div class="pfu-topo-legend-item"><div class="pfu-topo-leg-line" style="background:rgba(255,45,149,0.6)"></div><span>Classic pipeline</span></div>
<div class="pfu-topo-legend-item"><div class="pfu-topo-leg-line" style="background:rgba(176,38,255,0.7)"></div><span>Text/WS stream</span></div>
<div class="pfu-topo-legend-item"><div class="pfu-topo-leg-dash" style="border-color:rgba(255,184,0,0.5)"></div><span>Ollama (optional)</span></div>
<div class="pfu-topo-legend-item"><div class="pfu-topo-leg-dash" style="border-color:rgba(255,255,255,0.2)"></div><span>Shared / async</span></div>
</div>
</div>
</div>
<div class="wt-metrics-grid">
<div class="wt-metric-card" style="background:var(--wt-gradient-success);border-radius:var(--wt-radius-lg);border:1px solid rgba(0,255,159,0.3)">
<div class="metric-value" id="dashMetricServicesOnline">0</div>
<div class="metric-label">Services Online</div>
</div>
<div class="wt-metric-card" style="background:var(--wt-gradient-info);border-radius:var(--wt-radius-lg);border:1px solid rgba(0,255,245,0.3)">
<div class="metric-value" id="dashMetricRunningTests">0</div>
<div class="metric-label">Running Tests</div>
</div>
<div class="wt-metric-card" style="background:var(--wt-gradient-hero);border-radius:var(--wt-radius-lg);border:1px solid rgba(255,45,149,0.3)">
<div class="metric-value" id="dashMetricTestPass">0</div>
<div class="metric-label">Tests Passed</div>
<div class="metric-delta" id="dashMetricTestFail"></div>
</div>
<div class="wt-metric-card" style="background:var(--wt-gradient-neutral);border-radius:var(--wt-radius-lg);border:1px solid rgba(106,106,128,0.3)">
<div class="metric-value" id="dashMetricUptime">0s</div>
<div class="metric-label">Uptime</div>
</div>
</div>
<div class="wt-dashboard-content">
<div>
<div class="wt-card" style="max-height:400px;display:flex;flex-direction:column;border-radius:var(--wt-radius-lg);border-color:rgba(0,255,245,0.15)">
<div class="wt-card-header" style="border-radius:var(--wt-radius-lg) var(--wt-radius-lg) 0 0"><span class="wt-card-title">Real-Time Feed</span><span id="dashFeedPulse" style="width:8px;height:8px;border-radius:50%;background:var(--wt-accent-cyan);box-shadow:0 0 8px var(--wt-accent-cyan);animation:neonPulse 2s infinite"></span></div>
<div id="dashActivityFeed" style="flex:1;overflow-y:auto;font-size:11px;font-family:var(--wt-mono);line-height:1.8"></div>
</div>
</div>
<div>
<div class="wt-card" style="border-radius:var(--wt-radius-lg);border-color:rgba(255,45,149,0.15)">
<div class="wt-card-header" style="border-radius:var(--wt-radius-lg) var(--wt-radius-lg) 0 0"><span class="wt-card-title">Quick Actions</span></div>
<div style="display:flex;flex-direction:column;gap:8px">
<button class="wt-btn wt-btn-primary" style="width:100%;justify-content:center" onclick="dashStartAll()">▶ Start All Services</button>
<button class="wt-btn wt-btn-danger" style="width:100%;justify-content:center" onclick="dashStopAll()">■ Stop All Services</button>
<button class="wt-btn wt-btn-secondary" style="width:100%;justify-content:center" onclick="dashRestartFailed()">↻ Restart Failed</button>
<div style="border-top:1px solid rgba(255,255,255,0.08);margin-top:8px;padding-top:10px">
<label for="dashLanguageSelect" style="font-size:11px;color:var(--wt-text-secondary);letter-spacing:0.05em;display:block;margin-bottom:6px">PIPELINE LANGUAGE</label>
<select id="dashLanguageSelect" class="wt-input" style="width:100%" onchange="dashSetPipelineLanguage(this.value)">
<option value="de">German (de)</option>
<option value="auto">Auto-detect</option>
<option value="all">All</option>
<option value="en">English (en)</option>
<option value="fr">French (fr)</option>
<option value="es">Spanish (es)</option>
<option value="it">Italian (it)</option>
<option value="ja">Japanese (ja)</option>
<option value="zh">Chinese (zh)</option>
</select>
<div id="dashLanguageHint" style="font-size:10px;color:var(--wt-text-secondary);margin-top:6px;line-height:1.4">Restart services to apply.</div>
</div>
</div>
</div>
</div>
</div>
<div id="ollamaAlertOverlay" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:9999;align-items:center;justify-content:center">
<div style="background:var(--wt-card-bg);border:1px solid rgba(255,159,10,0.5);border-radius:var(--wt-radius-lg);padding:24px;max-width:420px;width:90%">
<div style="font-size:16px;font-weight:700;margin-bottom:12px;color:var(--wt-warning)">⚠ Ollama Not Installed</div>
<div style="font-size:13px;color:var(--wt-text-secondary);margin-bottom:16px;line-height:1.6">Ollama is required for RAG embeddings. Without it, the tomedo-crawl service cannot generate vector embeddings for patient data retrieval.</div>
<div id="ollamaInstallStatus" style="font-size:12px;color:var(--wt-text-secondary);margin-bottom:12px;min-height:16px"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="wt-btn wt-btn-secondary" onclick="dismissOllamaAlert()">OK</button>
<button class="wt-btn wt-btn-primary" id="ollamaInstallBtn" onclick="installOllama()">Install</button>
</div>
</div>
</div>
</div></div>
<div class="wt-page" id="page-services">
<div class="wt-content">
<div id="services-overview">
<h2 class="wt-page-title">Pipeline Services</h2>
<div id="servicesContainer"></div>
</div>
<div id="services-detail" class="hidden">
<div class="wt-detail-back" onclick="showServicesOverview()">← All Services</div>
<h2 class="wt-page-title" id="svcDetailName"></h2>
<div class="wt-card">
<div class="wt-card-header"><span class="wt-card-title">Configuration</span>
<span id="svcDetailStatus"></span></div>
<div class="wt-field"><label>Binary Path</label>
<div style="font-size:13px;color:var(--wt-text-secondary);font-family:var(--wt-mono)" id="svcDetailPath"></div></div>
<div id="whisperConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">Whisper Configuration</div>
<div class="wt-field" style="margin-bottom:6px"><label style="font-size:12px">Language</label>
<select class="wt-select" id="whisperLang" onchange="updateWhisperArgs()" title="BCP-47 language code passed to Whisper. Use 'de' for German, 'en' for English. Setting the correct language improves accuracy and avoids auto-detection overhead." style="font-size:12px"></select></div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Model</label>
<select class="wt-select" id="whisperModel" onchange="updateWhisperArgs()" title="GGML model file from bin/models/. Larger models are more accurate but slower. Recommended: ggml-large-v3-turbo-q5_0.bin (547 MB, best speed/accuracy). Model files must be placed in bin/models/ and converted to CoreML for ANE acceleration." style="font-size:12px"></select></div>
<div class="wt-field" style="margin-top:8px;margin-bottom:0;display:flex;align-items:center;gap:8px">
<label style="font-size:12px;margin:0;cursor:pointer;display:flex;align-items:center;gap:6px" title="When enabled, Whisper output is checked against a list of known hallucination strings (e.g. 'Untertitel', 'Copyright') and repetition patterns. Matching transcriptions are suppressed before reaching LLaMA. Disable if legitimate speech is being incorrectly filtered.">
<input type="checkbox" id="whisperHallucinationFilter" onchange="toggleHallucinationFilter(this.checked)" style="width:16px;height:16px;cursor:pointer">
Hallucination Filter</label>
<span id="whisperHalluFilterStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
</div>
<div id="llamaConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">LLaMA Configuration</div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Model</label>
<select class="wt-select" id="llamaModel" onchange="updateLlamaArgs()" title="GGUF model file from bin/models/. Selecting a model updates the positional .gguf argument in the service startup arguments." style="font-size:12px"></select></div>
</div>
<div id="kokoroConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">Kokoro Configuration</div>
<div class="wt-field" style="margin-bottom:6px"><label style="font-size:12px">Variant</label>
<select class="wt-select" id="kokoroVariant" onchange="updateKokoroVoices(); updateKokoroArgs()" title="Kokoro model variant directory under bin/models/. Each variant ships its own CoreML duration predictor and decoder voices." style="font-size:12px"></select></div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Voice</label>
<select class="wt-select" id="kokoroVoice" onchange="updateKokoroArgs()" title="Voice embedding from the selected variant's decoder_variants/ directory. The voice determines speaker identity and language." style="font-size:12px"></select></div>
</div>
<div id="neuttsConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">NeuTTS Configuration</div>
<div id="neuttsModelStatus" style="font-size:12px;color:var(--wt-text-secondary)">Loading...</div>
</div>
<div id="vits2Config" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">VITS2 Configuration</div>
<div class="wt-field" style="margin-bottom:6px"><label style="font-size:12px">Voice</label>
<select class="wt-select" id="vits2Voice" onchange="updateVITS2Args()" title="Voice model from bin/models/vits2-de/ or vits2-en/ directory." style="font-size:12px"></select></div>
<div class="wt-field" style="margin-bottom:6px"><label style="font-size:12px">G2P Backend</label>
<select class="wt-select" id="vits2G2P" onchange="updateVITS2Args()" style="font-size:12px">
<option value="auto">Auto</option><option value="neural">Neural (de)</option><option value="espeak">espeak-ng</option>
</select></div>
<div class="wt-field" style="margin-bottom:6px"><label style="font-size:12px">Language</label>
<select class="wt-select" id="vits2Lang" onchange="updateVITS2Args()" style="font-size:12px">
<option value="de">German (de)</option><option value="en">English (en)</option><option value="auto">Auto</option>
</select></div>
<div style="display:flex;gap:6px;align-items:center">
<button class="wt-btn wt-btn-primary" style="font-size:11px" onclick="saveVITS2Config()">Save</button>
<span id="vits2SaveStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
</div>
<div id="matchaConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">Matcha-TTS Configuration</div>
<div class="wt-field" style="margin-bottom:6px"><label style="font-size:12px">Voice</label>
<select class="wt-select" id="matchaVoice" onchange="updateMatchaArgs()" title="Voice model from bin/models/matcha-de/ or matcha-en/ directory." style="font-size:12px"></select></div>
<div class="wt-field" style="margin-bottom:6px"><label style="font-size:12px">G2P Backend</label>
<select class="wt-select" id="matchaG2P" onchange="updateMatchaArgs()" style="font-size:12px">
<option value="auto">Auto</option><option value="neural">Neural (de)</option><option value="espeak">espeak-ng</option>
</select></div>
<div class="wt-field" style="margin-bottom:6px"><label style="font-size:12px">Language</label>
<select class="wt-select" id="matchaLang" onchange="updateMatchaArgs()" style="font-size:12px">
<option value="de">German (de)</option><option value="en">English (en)</option><option value="auto">Auto</option>
</select></div>
<div style="display:flex;gap:6px;align-items:center">
<button class="wt-btn wt-btn-primary" style="font-size:11px" onclick="saveMatchaConfig()">Save</button>
<span id="matchaSaveStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
</div>
<div id="sipClientConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">PBX Connection</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px">
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Server IP</label>
<input class="wt-input" id="sipPbxServer" placeholder="192.168.1.100" title="IP address or hostname of the SIP PBX (e.g. FreePBX, Asterisk, 3CX). The SIP client registers a line on this server and receives inbound calls." style="font-size:12px"></div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Port</label>
<input class="wt-input" id="sipPbxPort" placeholder="5060" value="5060" title="SIP signalling port on the PBX. Standard SIP uses UDP 5060. Use 5061 for TLS-encrypted SIP (SIPS)." style="font-size:12px"></div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Username</label>
<input class="wt-input" id="sipPbxUser" placeholder="extension100" title="SIP extension number or username to register with the PBX. Must match the account configured on the PBX." style="font-size:12px"></div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Password</label>
<input class="wt-input" id="sipPbxPassword" type="password" placeholder="password" title="SIP account password for digest authentication with the PBX." style="font-size:12px"></div>
</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px">
<button class="wt-btn wt-btn-primary" style="font-size:11px" title="Register the entered SIP credentials with the PBX and add a new active line. Each call session uses its own independent line. The line remains registered until the service is stopped." onclick="sipConnectPbx()">Connect New Line</button>
<span id="sipPbxStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
</div>
<div id="sipProviderConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">SIP Provider Configuration</div>
<div class="wt-field" style="margin-top:8px;margin-bottom:6px;display:flex;align-items:center;gap:8px">
<label style="font-size:12px;margin:0;cursor:pointer;display:flex;align-items:center;gap:6px" title="When enabled, raw inbound RTP audio is written to 16-bit PCM WAV files (one file per call) in the configured directory. Useful for debugging ASR issues or recording call data.">
<input type="checkbox" id="sipProviderSaveWav" onchange="saveSipProviderWavConfig()" style="width:16px;height:16px;cursor:pointer">
Save incoming audio as WAV</label>
<span id="sipProviderWavStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Save to directory</label>
<input class="wt-input" id="sipProviderWavDir" placeholder="wav_recordings (relative to bin/)" title="Relative directory under bin/ where inbound WAV recordings are written. Absolute paths and '..' are rejected. The directory is created if missing. Files are named by call_id and timestamp." style="font-size:12px" onchange="saveSipProviderWavConfig()"></div>
</div>
<div id="tomedoCrawlConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">Tomedo RAG Configuration</div>
<div id="ragHealthStatus" title="Live status from tomedo-crawl /health endpoint. Green = service running and Ollama available. Shows indexed document count and timestamp of last completed crawl." style="margin-bottom:8px;padding:6px 10px;border-radius:4px;font-size:11px;background:rgba(0,0,0,0.2)">
<span id="ragStatusDot" style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--wt-text-secondary);margin-right:6px"></span>
<span id="ragStatusText">Checking...</span>
<span id="ragDocCount" style="margin-left:12px;color:var(--wt-text-secondary)"></span>
<span id="ragLastCrawl" style="margin-left:12px;color:var(--wt-text-secondary)"></span>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">Tomedo Server</div>
<div style="display:grid;grid-template-columns:1fr 100px;gap:6px;margin-bottom:8px">
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">IP / Hostname</label>
<input class="wt-input" id="ragTomedoHost" placeholder="192.168.10.9" title="IP address or hostname of the Tomedo EMR server. Default port is 8443 (HTTPS with mutual TLS)." style="font-size:12px"></div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Port</label>
<input class="wt-input" id="ragTomedoPort" placeholder="8443" title="HTTPS port of the Tomedo REST API. Default is 8443. The connection uses mutual TLS client certificate authentication." style="font-size:12px"></div>
</div>
<div class="wt-field" style="margin-bottom:8px"><label style="font-size:12px">Client Certificate (PEM)</label>
<div style="display:flex;gap:6px;align-items:center">
<input type="file" id="ragCertFile" accept=".pem,.crt,.cert,.key" title="Select the PEM file containing both the client certificate and private key for Tomedo mutual TLS authentication. Export from macOS Keychain: security export -t identities -f pkcs12 | openssl pkcs12 -nodes -out client.pem" style="font-size:11px;flex:1">
<button class="wt-btn wt-btn-secondary" style="font-size:11px" title="Upload the selected PEM file. The certificate is stored securely on disk and the path is saved to the tomedo-crawl config database." onclick="uploadRagCert()">Upload</button>
<span id="ragCertStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">Ollama Subservice</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
<span id="ollamaStatusDot" title="Current Ollama process status as reported by tomedo-crawl /health. Green = running and serving embeddings." style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--wt-text-secondary)"></span>
<span id="ollamaStatusText" style="font-size:11px">Checking...</span>
<div style="margin-left:auto;display:flex;gap:4px">
<button class="wt-btn wt-btn-primary" style="font-size:10px;padding:3px 8px" title="Start the Ollama embedding server (ollama serve). tomedo-crawl will manage the process lifecycle." onclick="ollamaStart()">▶ Start</button>
<button class="wt-btn wt-btn-danger" style="font-size:10px;padding:3px 8px" title="Stop the Ollama server. In-progress embedding requests will be aborted. The RAG /query endpoint will return 503 until Ollama is restarted." onclick="ollamaStop()">■ Stop</button>
<button class="wt-btn wt-btn-secondary" style="font-size:10px;padding:3px 8px" title="Stop and immediately restart the Ollama server. Use this to apply a new embedding model or recover from a crash." onclick="ollamaRestart()">↻ Restart</button>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px">
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Ollama URL</label>
<input class="wt-input" id="ragOllamaUrl" placeholder="http://127.0.0.1:11434" title="Base URL of the Ollama HTTP API. Default is http://127.0.0.1:11434. Change only if Ollama runs on a different host or port." style="font-size:12px"></div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Embedding Model</label>
<select class="wt-select" id="ragOllamaModel" title="Select the embedding model to use for generating vector representations of patient text chunks. The currently active model (used for existing vectors) is highlighted. Changing the model requires wiping the vector store and re-crawling." style="font-size:12px">
<option value="">Loading...</option>
</select>
</div>
</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px">
<input class="wt-input" id="ragOllamaPullModel" placeholder="Model name to pull..." title="Enter an Ollama model name (e.g. nomic-embed-text, embeddinggemma:300m) and click Pull to download it. Progress is shown in the live logs." style="font-size:11px;flex:1">
<button class="wt-btn wt-btn-secondary" style="font-size:10px;padding:3px 8px" title="Download the embedding model specified in the field to the left. Runs ollama pull in the background. Check live logs for download progress." onclick="ollamaPullModel()">Pull</button>
<span id="ollamaPullStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">Crawl Schedule</div>
<div style="display:flex;gap:12px;margin-bottom:6px;font-size:12px">
<label style="display:flex;align-items:center;gap:4px;cursor:pointer" title="Trigger one crawl per day at the specified local time. Recommended for production use — minimises load on the Tomedo server.">
<input type="radio" name="ragCrawlMode" value="daily" checked onchange="toggleCrawlMode()"> Daily at fixed time</label>
<label style="display:flex;align-items:center;gap:4px;cursor:pointer" title="Repeat crawl every N minutes. Useful during initial setup or when patient data changes frequently. Minimum interval: 5 minutes.">
<input type="radio" name="ragCrawlMode" value="interval" onchange="toggleCrawlMode()"> Repeat interval</label>
</div>
<div id="ragCrawlDailyRow" style="margin-bottom:6px">
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Daily crawl time</label>
<input type="time" class="wt-input" id="ragCrawlTime" value="02:00" title="Local time at which the daily crawl runs automatically. Default is 02:00 (2 AM) to avoid peak hours on the Tomedo server." style="font-size:12px;width:120px"></div>
</div>
<div id="ragCrawlIntervalRow" style="margin-bottom:6px;display:none">
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Repeat every (minutes)</label>
<input type="number" class="wt-input" id="ragCrawlRepeatMin" placeholder="60" min="5" max="1440" step="5" value="60" title="Interval between automatic crawls in minutes. Range: 5–1440 (1 day). The crawl fetches updated patient data from Tomedo and refreshes the vector store." style="font-size:12px;width:120px"></div>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;margin-top:8px;color:var(--wt-text-secondary)">Service Arguments</div>
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px">
<button class="wt-btn wt-btn-sm wt-btn-secondary" style="font-size:10px" title="Enable DEBUG log level. Logs every patient fetch, embedding call, and vector upsert. Useful for diagnosing crawl failures. Adds --verbose to service arguments." onclick="toggleRagArg('--verbose')">Verbose</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" style="font-size:10px" title="Do not run a crawl immediately when the service starts. The vector store is loaded from disk but no new data is fetched until the next scheduled crawl or a manual Trigger Crawl. Adds --skip-initial-crawl." onclick="toggleRagArg('--skip-initial-crawl')">Skip Initial Crawl</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" style="font-size:10px" title="Only update the phone-number index (phone → patient_id mapping). No text chunks are embedded or stored in the vector store. Faster than a full crawl. Useful when only caller identification is needed. Adds --phone-only." onclick="toggleRagArg('--phone-only')">Phone Index Only</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" style="font-size:10px" title="Fetch patient data and update the phone index, but skip the Ollama embedding step. No new vectors are written to the store. Useful when Ollama is unavailable but phone lookup is still needed. Adds --no-embed." onclick="toggleRagArg('--no-embed')">No Embedding</button>
</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px;align-items:center">
<label style="font-size:11px;display:flex;align-items:center;gap:4px" title="Number of vector search results returned per /query request. Higher values give more context to LLaMA but increase prompt length. Range: 1–20.">Top-K:
<input type="number" class="wt-input" id="ragArgTopK" min="1" max="20" value="3" style="font-size:11px;width:50px" onchange="buildRagArgs()"></label>
<label style="font-size:11px;display:flex;align-items:center;gap:4px" title="Maximum size of each text chunk in estimated tokens (Unicode codepoints ÷ 4). Larger chunks provide more context per vector but require a model with a larger context window. Range: 64–2048, step 64.">Chunk size:
<input type="number" class="wt-input" id="ragArgChunkSize" min="64" max="2048" value="512" step="64" style="font-size:11px;width:60px" onchange="buildRagArgs()"></label>
<label style="font-size:11px;display:flex;align-items:center;gap:4px" title="Token overlap between consecutive text chunks. Overlap ensures that information near chunk boundaries is not lost. Recommended: 10–15% of chunk size. Range: 0–256, step 16.">Overlap:
<input type="number" class="wt-input" id="ragArgOverlap" min="0" max="256" value="64" step="16" style="font-size:11px;width:55px" onchange="buildRagArgs()"></label>
<label style="font-size:11px;display:flex;align-items:center;gap:4px" title="Number of parallel embedding worker threads. Each worker makes one HTTP request to Ollama at a time. Increasing workers speeds up the crawl but increases load on Ollama. Match to Ollama's concurrency limit. Range: 1–8.">Workers:
<input type="range" id="ragArgWorkers" min="1" max="8" value="4" style="width:60px" oninput="document.getElementById('ragArgWorkersVal').textContent=this.value;buildRagArgs()">
<span id="ragArgWorkersVal" style="font-size:11px">4</span></label>
</div>
<div style="display:flex;gap:6px;align-items:center">
<button class="wt-btn wt-btn-primary" style="font-size:11px" title="Save all Tomedo RAG configuration (server address, certificate path, Ollama settings, crawl schedule) to the tomedo-crawl encrypted SQLite database. Changes take effect on the next service start." onclick="saveRagConfig()">Save Config</button>
<button class="wt-btn wt-btn-secondary" style="font-size:11px" title="Request an immediate crawl of the Tomedo patient database. The crawl runs in the background without restarting the service. Monitor progress in Live Logs." onclick="triggerRagCrawl()">Trigger Crawl</button>
<span id="ragConfigStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
</div>
<div id="moshiConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">Moshi Voice Configuration</div>
<details open style="border:1px solid var(--wt-border);border-radius:4px;padding:8px;margin-bottom:10px;background:var(--wt-bg-tertiary)">
<summary style="font-size:11px;font-weight:600;color:var(--wt-text-secondary);cursor:pointer;margin-bottom:6px">Moshi Frontend Model Files</summary>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">Model Files (Moshi LM)</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;font-size:11px">
<div><label style="font-size:11px">LM model file</label><input class="wt-input" id="moshiLmModelFile" placeholder="hf://kyutai/moshika-pytorch-bf16/model.safetensors" title="Path or HF URL for the Moshi LM model weights." style="font-size:11px"></div>
<div><label style="font-size:11px">Mimi model file</label><input class="wt-input" id="moshiMimiModelFile" placeholder="hf://kyutai/moshika-pytorch-bf16/tokenizer-e351c8d8-checkpoint125.safetensors" title="Path or HF URL for the Mimi tokenizer model." style="font-size:11px"></div>
</div>
<div style="margin-bottom:8px;font-size:11px">
<label style="font-size:11px">Text tokenizer file</label><input class="wt-input" id="moshiTextTokenizerFile" placeholder="hf://kyutai/moshika-pytorch-bf16/tokenizer_spm_32k_3.model" title="Path or HF URL for the SentencePiece text tokenizer." style="font-size:11px;width:100%">
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">Batch & GPU</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:8px;font-size:11px">
<div><label style="font-size:11px">Batch size</label><input class="wt-input" id="moshiBatchSize" type="number" min="1" value="8" title="Number of simultaneous call slots per backend process." style="font-size:11px"></div>
<div><label style="font-size:11px">Moshi GPU ID</label><input class="wt-input" id="moshiGpuId" type="number" min="0" value="0" title="GPU device index for the main Moshi LM." style="font-size:11px"></div>
<div><label style="font-size:11px">STT GPU ID</label><input class="wt-input" id="moshiSttGpuId" type="number" min="0" value="0" title="GPU device index for the in-process STT model." style="font-size:11px"></div>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">In-Process Streaming STT</div>
<div style="margin-bottom:8px">
<label style="cursor:pointer;display:flex;align-items:center;gap:4px;font-size:11px" title="Enable in-process streaming STT (Speech-to-Text) using the STT-1b model. Requires ~2 GB additional GPU memory."><input type="checkbox" id="moshiSttEnabled" onchange="moshiToggleSttFields()" style="width:14px;height:14px;cursor:pointer"> In-process Streaming STT</label>
</div>
<div id="moshiSttFields" class="hidden">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;font-size:11px">
<div><label style="font-size:11px">STT LM model file</label><input class="wt-input" id="moshiSttLmModelFile" placeholder="hf://kyutai/stt-1b-en_fr-pytorch/model.safetensors" title="Path or HF URL for the STT LM model weights." style="font-size:11px"></div>
<div><label style="font-size:11px">STT Mimi model file</label><input class="wt-input" id="moshiSttMimiModelFile" placeholder="hf://kyutai/stt-1b-en_fr-pytorch/tokenizer-e351c8d8-checkpoint125.safetensors" title="Path or HF URL for the STT Mimi tokenizer model." style="font-size:11px"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 100px;gap:8px;margin-bottom:8px;font-size:11px">
<div><label style="font-size:11px">STT text tokenizer file</label><input class="wt-input" id="moshiSttTextTokenizerFile" placeholder="hf://kyutai/stt-1b-en_fr-pytorch/tokenizer_spm_32k_3.model" title="Path or HF URL for the STT SentencePiece text tokenizer." style="font-size:11px"></div>
<div><label style="font-size:11px">ASR delay (tokens)</label><input class="wt-input" id="moshiAsrDelayInTokens" type="number" min="0" value="0" title="ASR delay in tokens before text output begins." style="font-size:11px"></div>
</div>
</div>
</details>
<details style="border:1px solid var(--wt-border);border-radius:4px;padding:8px;margin-bottom:10px;background:var(--wt-bg-tertiary)">
<summary style="font-size:11px;font-weight:600;color:var(--wt-text-secondary);cursor:pointer;margin-bottom:6px">Moshi RAG Backend Settings</summary>
<p style="font-size:11px;color:var(--wt-text-secondary);margin-bottom:6px">Settings for the moshi-rag-backend-service. Read-only fields require a service restart to change.</p>
<div style="display:grid;grid-template-columns:1fr 100px;gap:8px;margin-bottom:8px;font-size:11px">
<div><label style="font-size:11px">Listen address</label><input class="wt-input" id="moshiBeListenAddr" placeholder="127.0.0.1" readonly title="Backend listen address (read-only — requires service restart)." style="font-size:11px;opacity:0.6"></div>
<div><label style="font-size:11px">Listen port</label><input class="wt-input" id="moshiBeListenPort" type="number" placeholder="8090" readonly title="Backend listen port (read-only — requires service restart)." style="font-size:11px;opacity:0.6"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:8px;font-size:11px">
<div><label style="font-size:11px">Trigger window (chars)</label><input class="wt-input" id="moshiBeTriggerWindowChars" type="number" min="0" value="200" title="Number of trailing characters to check for trigger keyword matches." style="font-size:11px"></div>
<div><label style="font-size:11px">Default timeout (s)</label><input class="wt-input" id="moshiBeDefaultTimeoutSecs" type="number" min="0.5" step="0.5" value="3.0" title="Default timeout in seconds for LLM retrieval and action requests." style="font-size:11px"></div>
<div><label style="font-size:11px">Default max tokens</label><input class="wt-input" id="moshiBeDefaultMaxTokens" type="number" min="1" value="256" title="Maximum tokens for LLM retrieval responses." style="font-size:11px"></div>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">LLM Profiles</div>
<div id="moshiBeLlmProfiles" style="margin-bottom:8px"></div>
<div style="display:grid;grid-template-columns:100px 1fr 120px 120px 60px;gap:4px;margin-bottom:4px;font-size:11px;align-items:end">
<div><label style="font-size:11px">ID</label><input class="wt-input" id="moshiBeNewProfileId" placeholder="default" style="font-size:11px"></div>
<div><label style="font-size:11px">URL</label><input class="wt-input" id="moshiBeNewProfileUrl" placeholder="http://localhost:11434/v1" style="font-size:11px"></div>
<div><label style="font-size:11px">Model</label><input class="wt-input" id="moshiBeNewProfileModel" placeholder="llama3" style="font-size:11px"></div>
<div><label style="font-size:11px">API key</label><input class="wt-input" id="moshiBeNewProfileApiKey" type="password" placeholder="" style="font-size:11px"></div>
<div style="display:flex;align-items:flex-end"><button class="wt-btn wt-btn-secondary" style="font-size:10px;height:32px;white-space:nowrap" onclick="moshiBeAddProfile()">+</button></div>
</div>
</details>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">Language Backends</div>
<p style="font-size:11px;color:var(--wt-text-secondary);margin-bottom:6px">Each backend is a persistent Rust moshi-backend process. One process per language. Slots in each process handle multiple simultaneous calls (batch_size in config.json).</p>
<div id="moshiBackendList" style="margin-bottom:8px"></div>
<div style="display:grid;grid-template-columns:60px 1fr 1fr 80px;gap:4px;margin-bottom:4px;font-size:11px;align-items:end">
<div><label style="font-size:11px">Lang code</label><input class="wt-input" id="moshiNewLang" placeholder="de" title="ISO-639-1 language code, e.g. en, de, fr" style="font-size:11px"></div>
<div><label style="font-size:11px">Config JSON path</label><input class="wt-input" id="moshiNewConfig" placeholder="bin/models/moshi-de-backend-config.json" title="Path to moshi-backend config.json relative to the project root" style="font-size:11px"></div>
<div><label style="font-size:11px">Binary path (optional)</label><input class="wt-input" id="moshiNewBinary" placeholder="bin/moshi-backend" title="Path to the moshi-backend binary. Leave empty to use the default binary." style="font-size:11px"></div>
<div style="display:flex;align-items:flex-end"><button class="wt-btn wt-btn-secondary" style="font-size:10px;height:32px;white-space:nowrap" onclick="moshiAddBackend()">+ Add</button></div>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;margin-top:10px;color:var(--wt-text-secondary)">Default Language</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<input class="wt-input" id="moshiDefaultLang" placeholder="en" title="Language code used when no language-specific backend is available. Must match one of the configured backend language codes." style="font-size:11px;width:80px">
<span style="font-size:11px;color:var(--wt-text-secondary)">Falls back to this language if no backend matches the pipeline language.</span>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;margin-top:10px;color:var(--wt-text-secondary)">RAG Backend Connection</div>
<p style="font-size:11px;color:var(--wt-text-secondary);margin-bottom:6px">The moshi-rag-backend-service receives the continuous text token stream and dispatches actions on trigger matches.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px;font-size:11px">
<div><label style="font-size:11px">Backend URL</label><input class="wt-input" id="moshiBackendUrl" placeholder="http://127.0.0.1:8090" title="URL of the moshi-rag-backend-service WebSocket/HTTP endpoint." style="font-size:11px"></div>
<div><label style="font-size:11px">tomedo-crawl URL</label><input class="wt-input" id="moshiTomedoCrawlUrl" placeholder="http://127.0.0.1:13181" title="URL of the tomedo-crawl service for patient lookup and semantic search." style="font-size:11px"></div>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;margin-top:10px;color:var(--wt-text-secondary)">Mode Toggles</div>
<div style="display:flex;flex-wrap:wrap;gap:16px;margin-bottom:10px;font-size:11px">
<label style="cursor:pointer;display:flex;align-items:center;gap:4px" title="Enable ARC encoder for reference text injection. Uses ~6 GB additional GPU memory. When off (default), teacher-forcing injection is used instead (zero extra GPU memory)."><input type="checkbox" id="moshiArcMode" onchange="moshiToggleArcFields()" style="width:14px;height:14px;cursor:pointer"> ARC Mode</label>
<label style="cursor:pointer;display:flex;align-items:center;gap:4px" title="Enable LLM-based retrieval via the backend. When off, only tomedo-crawl queries and action dispatch are available."><input type="checkbox" id="moshiLlmMode" onchange="moshiToggleLlmFields()" style="width:14px;height:14px;cursor:pointer"> LLM Retrieval Mode</label>
</div>
<div id="moshiArcFields" class="hidden" style="border:1px solid var(--wt-border);border-radius:4px;padding:8px;margin-bottom:10px;background:var(--wt-bg-tertiary)">
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">ARC Encoder Settings</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:11px">
<div><label style="font-size:11px">ARC model file</label><input class="wt-input" id="moshiArcModelFile" placeholder="hf://kyutai/moshiko-pytorch-bf16/arc_encoder.safetensors" title="Path or HF URL for the ARC encoder model weights." style="font-size:11px"></div>
<div><label style="font-size:11px">ARC tokenizer path</label><input class="wt-input" id="moshiArcTokenizerPath" placeholder="hf://kyutai/moshiko-pytorch-bf16/arc_tokenizer.model" title="Path or HF URL for the ARC encoder SentencePiece tokenizer." style="font-size:11px"></div>
</div>
</div>
<div id="moshiLlmFields" class="hidden" style="border:1px solid var(--wt-border);border-radius:4px;padding:8px;margin-bottom:10px;background:var(--wt-bg-tertiary)">
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">LLM Retrieval Settings</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:11px">
<div><label style="font-size:11px">LLM API URL</label><input class="wt-input" id="moshiLlmApiUrl" placeholder="http://127.0.0.1:11434/v1" title="OpenAI-compatible LLM API endpoint used for retrieval." style="font-size:11px"></div>
<div><label style="font-size:11px">LLM API key (optional)</label><input class="wt-input" id="moshiLlmApiKey" type="password" placeholder="" title="API key for the LLM endpoint. Leave empty for local models." style="font-size:11px"></div>
</div>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;margin-top:10px;color:var(--wt-text-secondary)">[RET] Token Action</div>
<p style="font-size:11px;color:var(--wt-text-secondary);margin-bottom:6px">Action dispatched when the Moshi model emits a [RET] retrieval token.</p>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<select class="wt-select" id="moshiRetAction" style="font-size:11px;width:200px" title="Default action for [RET] token events.">
<option value="tomedo-crawl-query">tomedo-crawl-query</option>
<option value="llm-retrieval">llm-retrieval</option>
</select>
<span style="font-size:11px;color:var(--wt-text-secondary)">Default action fired when [RET] is detected.</span>
</div>
<div style="font-size:11px;font-weight:600;margin-bottom:4px;color:var(--wt-text-secondary)">Retrieval Trigger Rules</div>
<p style="font-size:11px;color:var(--wt-text-secondary);margin-bottom:6px">Triggers are evaluated by the backend service. When a matching keyword or phrase is detected in the text stream, the configured action is dispatched and the result can be injected into Moshi as reference context.</p>
<div id="moshiTriggerList" style="margin-bottom:8px"></div>
<div style="display:grid;grid-template-columns:90px 1fr 120px 1fr 1fr 70px 50px 80px;gap:4px;margin-bottom:4px;font-size:11px;align-items:end">
<div>
<label style="font-size:11px">Type</label>
<select class="wt-select" id="moshiNewTriggerType" style="font-size:11px">
<option value="keyword">Keyword</option>
<option value="regex">Regex</option>
<option value="command">Command</option>
</select>
</div>
<div><label style="font-size:11px">Match (keyword/regex)</label><input class="wt-input" id="moshiNewTriggerMatch" placeholder="Medikament|medication" title="Keyword or regex to match against the text stream. Leave empty for a command-only trigger." style="font-size:11px"></div>
<div>
<label style="font-size:11px">Action Type</label>
<select class="wt-select" id="moshiNewTriggerActionType" onchange="moshiTriggerActionTypeChanged()" style="font-size:11px">
<option value="tomedo-crawl-query">tomedo-crawl-query</option>
<option value="webhook">webhook</option>
<option value="llm-retrieval">llm-retrieval</option>
<option value="calendar">calendar</option>
<option value="script">script</option>
<option value="retrieval_and_webhook">retrieval+webhook</option>
</select>
</div>
<div id="moshiNewTriggerActionUrlWrap"><label style="font-size:11px">Action URL / path</label><input class="wt-input" id="moshiNewTriggerActionUrl" placeholder="https://..." title="URL for webhook/calendar, or script path. Not needed for tomedo-crawl-query / llm-retrieval." style="font-size:11px"></div>
<div><label style="font-size:11px">Label</label><input class="wt-input" id="moshiNewTriggerLabel" placeholder="Medication query" title="Human-readable label for this trigger, shown in logs." style="font-size:11px"></div>
<div><label style="font-size:11px">Cooldown</label><input class="wt-input" id="moshiNewTriggerCooldown" type="number" min="0" value="30" placeholder="30" title="Cooldown in seconds before this trigger can fire again." style="font-size:11px"></div>
<div style="display:flex;align-items:center;gap:2px;padding-top:14px"><input type="checkbox" id="moshiNewTriggerInject" checked title="Inject the action result as reference text into the Moshi model." style="width:14px;height:14px"><label style="font-size:10px;cursor:pointer" for="moshiNewTriggerInject">Inject</label></div>
<div style="display:flex;align-items:flex-end"><button class="wt-btn wt-btn-secondary" style="font-size:10px;height:32px;white-space:nowrap" onclick="moshiAddTrigger()">+ Add</button></div>
</div>
<div style="display:flex;gap:6px;align-items:center;margin-top:8px">
<button class="wt-btn wt-btn-primary" style="font-size:11px" onclick="saveMoshiConfig()">Save Config</button>
<span id="moshiConfigStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
</div>
<div id="oapConfig" class="hidden" style="border:1px solid var(--wt-border);border-radius:6px;padding:10px;margin-bottom:8px;background:var(--wt-bg-secondary)">
<div style="font-size:12px;font-weight:600;margin-bottom:6px">Outbound Audio Processor Configuration</div>
<div class="wt-field" style="margin-top:8px;margin-bottom:6px;display:flex;align-items:center;gap:8px">
<label style="font-size:12px;margin:0;cursor:pointer;display:flex;align-items:center;gap:6px" title="When enabled, the synthesised TTS audio sent to the caller is written to 16-bit PCM WAV files (one per call). Useful for verifying TTS quality and debugging audio routing.">
<input type="checkbox" id="oapSaveWav" onchange="saveOapWavConfig()" style="width:16px;height:16px;cursor:pointer">
Save outgoing audio as WAV</label>
<span id="oapWavStatus" style="font-size:11px;color:var(--wt-text-secondary)"></span>
</div>
<div class="wt-field" style="margin-bottom:0"><label style="font-size:12px">Save to directory</label>
<input class="wt-input" id="oapWavDir" placeholder="wav_recordings (relative to bin/)" title="Relative directory under bin/ where outbound TTS WAV recordings are written. Absolute paths and '..' are rejected. The directory is created if missing. Files are named by call_id and timestamp." style="font-size:12px" onchange="saveOapWavConfig()"></div>
</div>
<div class="wt-field" id="svcRuntimeLogLevelWrap"><label>Runtime Log Level</label>
<select class="wt-select" id="svcRuntimeLogLevel" onchange="changeServiceLogLevel(this.value)">
<option value="ERROR">ERROR</option>
<option value="WARN">WARN</option>
<option value="INFO">INFO</option>
<option value="DEBUG">DEBUG</option>
<option value="TRACE">TRACE</option>
</select></div>
<div class="wt-field"><label>Arguments</label>
<input class="wt-input" id="svcDetailArgs" placeholder="Service arguments..." title="Command-line arguments passed to the service binary on start. For Tomedo RAG the arguments are built automatically by the buttons above. For other services refer to the service documentation."></div>
<div style="display:flex;gap:8px">
<button class="wt-btn wt-btn-primary" id="svcStartBtn" title="Start the selected service. If the service is already running it will not be started again." onclick="startSvcDetail()">▶ Start</button>
<button class="wt-btn wt-btn-danger" id="svcStopBtn" title="Send SIGTERM to the service process and wait for it to exit cleanly. Active calls on this service will be terminated." onclick="stopSvcDetail()">■ Stop</button>
<button class="wt-btn wt-btn-secondary" id="svcRestartBtn" title="Stop the service (SIGTERM) and immediately start it again with the current saved arguments. Use after changing configuration." onclick="restartSvcDetail()">↻ Restart</button>
<button class="wt-btn wt-btn-secondary" id="svcSaveBtn" title="Persist the current service path and arguments to the frontend settings database. These values are restored automatically the next time the frontend starts." onclick="saveSvcConfig()">💾 Save Config</button>
</div></div>
<div id="sipActiveLinesCard" class="wt-card hidden">
<div class="wt-card-header"><span class="wt-card-title">Active Lines</span>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="sipRefreshActiveLines()">Refresh</button></div>
<div id="sipActiveLines" style="padding:8px;font-size:12px;color:var(--wt-text-secondary)">Loading...</div>
</div>
<div class="wt-card">
<div class="wt-card-header"><span class="wt-card-title">Live Logs</span>
<select class="wt-select" id="svcLogLevelFilter" onchange="applyServiceLogLevelFilter()" style="font-size:12px"><option value="">All Levels</option><option value="TRACE">Trace</option><option value="DEBUG">Debug</option><option value="INFO">Info</option><option value="WARN">Warn</option><option value="ERROR">Error</option></select>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="clearSvcLog()">Clear</button></div>
<div class="wt-log-view" id="svcDetailLog">Waiting for logs...</div>
</div></div></div></div>
<div class="wt-page" id="page-logs">
<div class="wt-content">
<h2 class="wt-page-title">Live Logs</h2>
<div class="wt-filter-bar">
<select class="wt-select" id="logServiceFilter" onchange="reconnectLogSSE()">
<option value="">All Services</option>
<option value="SIP_CLIENT">SIP Client</option>
<option value="INBOUND_AUDIO_PROCESSOR">Inbound Audio</option>
<option value="VAD_SERVICE">VAD</option>
<option value="WHISPER_SERVICE">Whisper ASR</option>
<option value="LLAMA_SERVICE">LLaMA LLM</option>
<option value="TTS_SERVICE">TTS Stage (dock)</option>
<option value="KOKORO_ENGINE">Kokoro Engine</option>
<option value="NEUTTS_ENGINE">NeuTTS Engine</option>
<option value="VITS2_ENGINE">VITS2 Engine</option>
<option value="MATCHA_ENGINE">Matcha Engine</option>
<option value="OUTBOUND_AUDIO_PROCESSOR">Outbound Audio</option>
<option value="MOSHI_SERVICE">Moshi Voice</option>
<option value="MOSHI_RAG_BACKEND">Moshi RAG Backend</option>
<option value="TOMEDO_CRAWL">Tomedo RAG</option>
<option value="TEST_SIP_PROVIDER">Test SIP Provider</option>
<option value="FRONTEND">Frontend</option>
</select>
<select class="wt-select" id="logLevelFilter" onchange="applyLogLevelFilter()">
<option value="">All Levels</option>
<option value="TRACE">Trace</option>
<option value="DEBUG">Debug</option>
<option value="INFO">Info</option>
<option value="WARN">Warn</option>
<option value="ERROR">Error</option>
</select>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="clearLiveLogs()">Clear</button>
<label style="font-size:12px;display:flex;align-items:center;gap:6px;margin-left:auto">
<span>Auto-scroll</span>
<div class="wt-toggle on" id="autoScrollToggle" onclick="this.classList.toggle('on')"></div></label>
</div>
<div class="wt-log-view" id="liveLogView" style="max-height:calc(100vh - 200px)"></div>
</div></div>
<div class="wt-page" id="page-database">
<div class="wt-content">
<h2 class="wt-page-title">Database Admin</h2>
<div class="wt-card">
<div class="wt-card-header"><span class="wt-card-title">SQL Query</span>
<div style="display:flex;align-items:center;gap:8px">
<label style="font-size:12px;display:flex;align-items:center;gap:6px">
<span>Write Mode</span>
<div class="wt-toggle" id="dbWriteToggle" onclick="toggleDbWrite()"></div></label>
</div></div>
<textarea class="wt-textarea" id="sqlQuery" rows="3">SELECT * FROM logs ORDER BY id DESC LIMIT 50</textarea>
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">
<button class="wt-btn wt-btn-primary" onclick="runQuery()">▶ Execute</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="document.getElementById('sqlQuery').value='SELECT * FROM logs ORDER BY id DESC LIMIT 50'">Recent Logs</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="document.getElementById('sqlQuery').value='SELECT * FROM test_runs ORDER BY start_time DESC LIMIT 20'">Test History</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="document.getElementById('sqlQuery').value='SELECT service, COUNT(*) as count FROM logs GROUP BY service'">Log Stats</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="loadSchema()">Show Schema</button>
</div></div>
<div id="queryResults"></div>
<div id="schemaView" class="hidden"></div>
</div></div>
<div class="wt-page" id="page-credentials">
<div class="wt-content">
<h2 class="wt-page-title">Credentials</h2>
<div class="wt-card">
<div class="wt-card-header"><span class="wt-card-title">HuggingFace</span></div>
<div class="wt-field">
<label>Access Token</label>
<div style="display:flex;gap:8px">
<input type="password" class="wt-input" id="credHfToken" placeholder="hf_..." style="flex:1" autocomplete="new-password">
<button class="wt-btn wt-btn-primary" onclick="saveCredential('hf_token','credHfToken','credHfStatus','credHfClear')">Save</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" id="credHfClear" style="display:none" onclick="clearCredential('hf_token','credHfToken','credHfStatus','credHfClear','hf_...')">Clear</button>
</div>
<div id="credHfStatus" style="font-size:12px;margin-top:4px"></div>
</div>
</div>
<div class="wt-card">
<div class="wt-card-header"><span class="wt-card-title">GitHub</span></div>
<div class="wt-field">
<label>Access Token</label>
<div style="display:flex;gap:8px">
<input type="password" class="wt-input" id="credGhToken" placeholder="ghp_..." style="flex:1" autocomplete="new-password">
<button class="wt-btn wt-btn-primary" onclick="saveCredential('github_token','credGhToken','credGhStatus','credGhClear')">Save</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" id="credGhClear" style="display:none" onclick="clearCredential('github_token','credGhToken','credGhStatus','credGhClear','ghp_...')">Clear</button>
</div>
<div id="credGhStatus" style="font-size:12px;margin-top:4px"></div>
</div>
</div>
</div></div>
<div class="wt-page" id="page-beta-testing">
<div class="wt-content">
<h2 class="wt-page-title">Testing & Optimization</h2>
<div id="betaTestSummary" class="wt-test-summary-bar">
<span style="font-weight:600;color:var(--wt-text-secondary)">Status:</span>
<span><span id="betaDotComponent" class="summary-dot"></span>Component</span>
<span><span id="betaDotPipeline" class="summary-dot"></span>Pipeline</span>
<span><span id="betaDotTools" class="summary-dot"></span>Tools</span>
<span><span id="betaDotResults" class="summary-dot"></span>Results</span>
</div>
<div class="wt-tab-bar" id="betaTestTabs" role="tablist">
<button class="wt-tab-btn active" role="tab" id="tab-beta-component" aria-selected="true" aria-controls="beta-component" onclick="switchBetaTab('beta-component')">Component Tests</button>
<button class="wt-tab-btn" role="tab" id="tab-beta-pipeline" aria-selected="false" aria-controls="beta-pipeline" onclick="switchBetaTab('beta-pipeline')">Pipeline Tests</button>
<button class="wt-tab-btn" role="tab" id="tab-beta-tools" aria-selected="false" aria-controls="beta-tools" onclick="switchBetaTab('beta-tools')">Tools</button>
<button class="wt-tab-btn" role="tab" id="tab-beta-results" aria-selected="false" aria-controls="beta-results" onclick="switchBetaTab('beta-results')">Test Results</button>
</div>
<div style="display:flex;gap:8px;margin-bottom:12px;justify-content:flex-end;align-items:center;flex-wrap:wrap">
<label style="font-size:12px;display:flex;align-items:center;gap:6px">TTS:
<select class="wt-select tts-pref-select" onchange="setTtsPreference(this.value)" style="font-size:12px;padding:2px 6px">
<option value="auto">Auto (Kokoro + NeuTTS)</option>
<option value="kokoro">Kokoro only</option>
<option value="neutts">NeuTTS only</option>
<option value="vits2">VITS2 only</option>
<option value="matcha">Matcha-TTS only</option>
<option value="moshi">Moshi</option>
<option value="moshi-rag">Moshi RAG</option>
</select>
</label>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="toggleAllCollapsibles(true)">Expand All</button>
<button class="wt-btn wt-btn-sm wt-btn-secondary" onclick="toggleAllCollapsibles(false)">Collapse All</button>
<button class="wt-btn wt-btn-sm wt-btn-primary" id="runAllTestsBtn" onclick="runAllBetaTests()">▶ Run All Tests</button>
</div>
<div id="runAllTestsStatus" style="display:none;margin-bottom:12px;padding:10px;background:var(--wt-card-bg);border:1px solid var(--wt-border);border-radius:var(--wt-radius);font-size:12px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px"><strong>Run All Progress:</strong> <span id="runAllProgress">0/0</span></div>
<div id="runAllDetails" style="font-size:11px;color:var(--wt-text-secondary)"></div>
</div>
<div class="wt-tab-panes" id="betaTestPanes">
<div class="wt-tab-pane active" id="beta-component" role="tabpanel" aria-labelledby="tab-beta-component">
<div class="wt-card">
<div class="wt-card-header" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" onclick="toggleCollapsible(this)" onkeydown="if(event.key==='Enter'||event.key===' ')toggleCollapsible(this)"><span class="wt-card-title">Test 1: SIP Client RTP Routing</span><span id="prereq-sip-rtp" style="margin-left:8px;font-size:10px;padding:2px 6px;border-radius:4px;background:var(--wt-text-secondary);color:#fff">...</span><span style="margin-left:auto;font-size:12px;color:var(--wt-text-secondary)">▶</span></div>
<div class="wt-collapsible">
<div style="padding:0 20px 16px">
<p style="font-size:13px;color:var(--wt-text-secondary);margin-bottom:12px">Test SIP Client RTP packet routing and TCP connection handling with IAP service.</p>
<div style="display:flex;gap:8px;margin-bottom:12px">
<button class="wt-btn wt-btn-primary" id="sipRtpStartBtn" onclick="startSipRtpTest()">▶ Start Test</button>
<button class="wt-btn wt-btn-danger" onclick="stopSipRtpTest()">■ Stop Test</button>
<button class="wt-btn wt-btn-secondary" onclick="refreshSipStats()">↻ Refresh Stats</button>
</div>
<div id="sipRtpTestStatus" style="margin-bottom:12px;font-size:13px"></div>
<div id="sipRtpMetrics">
<h4 style="font-size:14px;font-weight:600;margin:12px 0 8px">RTP Packet Metrics</h4>
<table class="wt-table" style="width:100%">
<thead>
<tr>
<th>Call ID</th>
<th>Line</th>
<th>RX Packets</th>
<th>TX Packets</th>
<th>Forwarded</th>
<th>Discarded</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="sipRtpStatsBody">
<tr><td colspan="7" style="text-align:center;color:var(--wt-text-secondary)">No active calls. Start SIP Client and inject audio to begin test.</td></tr>
</tbody>
</table>
<div style="margin-top:12px;font-size:13px">
<div><strong>TCP Connection Status:</strong> <span id="iapConnectionStatus">Unknown</span></div>
<div style="margin-top:4px"><strong>Test Instructions:</strong></div>
<ol style="margin:8px 0;padding-left:20px;font-size:12px;color:var(--wt-text-secondary)">
<li>Start SIP Client (without IAP) → Inject audio → Verify RTP packets received but discarded</li>
<li>Start IAP service → Verify TCP connection established</li>
<li>Re-inject audio → Verify packets forwarded to IAP</li>
<li>Stop/Start IAP multiple times to test reconnection handling</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<div class="wt-card">
<div class="wt-card-header" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" onclick="toggleCollapsible(this)" onkeydown="if(event.key==='Enter'||event.key===' ')toggleCollapsible(this)"><span class="wt-card-title">Test 2: IAP Codec Quality</span><span id="prereq-iap" style="margin-left:8px;font-size:10px;padding:2px 6px;border-radius:4px;background:var(--wt-success);color:#000">Ready</span><span style="margin-left:auto;font-size:12px;color:var(--wt-text-secondary)">▶</span></div>
<div class="wt-collapsible">
<div style="padding:0 20px 16px">
<p style="font-size:13px;color:var(--wt-text-secondary);margin-bottom:12px"><strong>Codec algorithm test</strong> (does not require IAP service). Runs the exact G.711 mu-law encode/decode + 15-tap FIR half-band 8kHz→16kHz upsample pipeline offline, measuring SNR and RMS Error per-packet. Service connectivity is tested in Test 1 above.</p>
<div class="wt-field">
<label>Select Test File</label>
<select class="wt-select" id="iapTestFileSelect" style="width:100%;padding:8px">
<option value="">-- Select a test file --</option>
</select>
</div>
<div style="display:flex;gap:8px;margin-bottom:12px">
<button class="wt-btn wt-btn-primary" id="iapRunBtn" onclick="runIapQualityTest()">▶ Run Quality Test</button>
<button class="wt-btn wt-btn-success" id="iapRunAllBtn" onclick="runAllIapQualityTests()">▶ Run All Files</button>
</div>
<div id="iapTestStatus" style="margin-bottom:12px;font-size:13px"></div>
<div id="iapTestResults">
<h4 style="font-size:14px;font-weight:600;margin:12px 0 8px">Latest Test Results</h4>
<table class="wt-table" style="width:100%">
<thead>
<tr>
<th>File</th>
<th>Avg Pkt Latency (ms)</th>
<th>Max Pkt Latency (ms)</th>
<th>SNR (dB)</th>
<th>RMS Error (%)</th>
<th>Status</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody id="iapResultsBody">
<tr><td colspan="7" style="text-align:center;color:var(--wt-text-secondary)">No test results yet. Run a test to see results here.</td></tr>
</tbody>
</table>
<div style="margin-top:12px;font-size:12px;color:var(--wt-text-secondary)">
<strong>Pass Criteria:</strong> SNR ≥ 25dB, RMS Error ≤ 10%, Per-Packet Latency ≤ 50ms. Uses shared IAP pipeline: G.711 μ-law encode/decode + 15-tap FIR half-band upsample (from interconnect.h).
</div>
</div>
<div id="iapTestChart" style="margin-top:16px;display:none">
<canvas id="iapMetricsChart" style="max-height:250px"></canvas>
</div>
</div>
</div>
</div>
<div class="wt-card">
<div class="wt-card-header" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" onclick="toggleCollapsible(this)" onkeydown="if(event.key==='Enter'||event.key===' ')toggleCollapsible(this)"><span class="wt-card-title">Whisper Accuracy Test</span><span id="prereq-whisper" style="margin-left:8px;font-size:10px;padding:2px 6px;border-radius:4px;background:var(--wt-text-secondary);color:#fff">...</span><span style="margin-left:auto;font-size:12px;color:var(--wt-text-secondary)">▶</span></div>
<div class="wt-collapsible">
<div style="padding:0 20px 16px">
<div class="wt-field">
<label>Test Files (hold Ctrl/Cmd to select multiple)</label>
<select class="wt-select" id="accuracyTestFiles" multiple style="width:100%;padding:8px;height:120px">
</select>
</div>
<div class="wt-field">
<label>Model</label>
<input class="wt-input" id="accuracyModel" value="current" readonly>
</div>
<div style="padding:10px;background:var(--wt-card-hover);border-radius:6px;margin-top:8px;border-left:3px solid var(--wt-primary)">
<div style="font-size:12px;font-weight:600;color:var(--wt-text-secondary);margin-bottom:6px">⚙ VAD Service Settings <span id="vadLiveIndicator" style="font-weight:400;font-size:11px;color:var(--wt-text-muted)">(loading...)</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr;gap:8px;font-size:12px">
<div><strong>Window:</strong> <span id="currentVadWindow" style="color:var(--wt-primary)">50</span> ms <span style="font-size:10px;color:var(--wt-text-muted)">(restart)</span></div>
<div><strong>Threshold:</strong> <span id="currentVadThreshold" style="color:var(--wt-primary)">2.0</span></div>
<div><strong>Silence:</strong> <span id="currentVadSilence" style="color:var(--wt-primary)">400</span> ms</div>
<div><strong>Max Chunk:</strong> <span id="currentVadMaxChunk" style="color:var(--wt-primary)">8000</span> ms</div>
<div><strong>Onset Gap:</strong> <span id="currentVadOnsetGap" style="color:var(--wt-primary)">1</span> frames</div>
<div><strong>Post-Idle Cooldown:</strong> <span id="currentVadPostIdleCooldown" style="color:var(--wt-primary)">1200</span> ms</div>
<div><strong>RMS Gate:</strong> <span id="currentVadRmsGate" style="color:var(--wt-primary)">0.04</span></div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:8px">
<div class="wt-field">
<label>VAD Window (ms): <span id="vadWindowValue">50</span> <span style="font-size:10px;color:var(--wt-text-muted)">(restart only)</span></label>
<input type="range" id="vadWindowSlider" min="10" max="200" value="50" step="10" style="width:100%" oninput="updateVadWindowDisplay(this.value)">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--wt-text-secondary);margin-top:2px">
<span>10ms</span><span>200ms</span>
</div>
</div>
<div class="wt-field">
<label>VAD Threshold: <span id="vadThresholdValue">2.0</span></label>
<input type="range" id="vadThresholdSlider" min="0.5" max="10.0" value="2.0" step="0.5" style="width:100%" oninput="updateVadThresholdDisplay(this.value)">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--wt-text-secondary);margin-top:2px">
<span>0.5</span><span>10.0</span>
</div>
</div>
<div class="wt-field">
<label>VAD Silence (ms): <span id="vadSilenceValue">400</span></label>
<input type="range" id="vadSilenceSlider" min="100" max="1500" value="400" step="50" style="width:100%" oninput="document.getElementById('vadSilenceValue').textContent=this.value">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--wt-text-secondary);margin-top:2px">
<span>100ms</span><span>1500ms</span>
</div>
</div>
<div class="wt-field">
<label>Max Chunk (ms): <span id="vadMaxChunkValue">8000</span></label>
<input type="range" id="vadMaxChunkSlider" min="1000" max="10000" value="8000" step="500" style="width:100%" oninput="document.getElementById('vadMaxChunkValue').textContent=this.value">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--wt-text-secondary);margin-top:2px">
<span>1000ms</span><span>10000ms</span>
</div>
</div>
<div class="wt-field">
<label>Onset Gap Tolerance: <span id="vadOnsetGapValue">1</span> frames</label>
<input type="range" id="vadOnsetGapSlider" min="0" max="5" value="1" step="1" style="width:100%" oninput="document.getElementById('vadOnsetGapValue').textContent=this.value">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--wt-text-secondary);margin-top:2px">
<span>0</span><span>5</span>
</div>
</div>
<div class="wt-field">
<label>Post-Idle Cooldown: <span id="vadPostIdleCooldownValue">1200</span>ms</label>
<input type="range" id="vadPostIdleCooldownSlider" min="0" max="3000" value="1200" step="100" style="width:100%" oninput="document.getElementById('vadPostIdleCooldownValue').textContent=this.value">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--wt-text-secondary);margin-top:2px">
<span>0ms</span><span>3000ms</span>
</div>
</div>
<div class="wt-field">
<label>RMS Gate: <span id="vadRmsGateValue">0.04</span></label>
<input type="range" id="vadRmsGateSlider" min="0" max="0.2" value="0.04" step="0.005" style="width:100%" oninput="document.getElementById('vadRmsGateValue').textContent=parseFloat(this.value).toFixed(3)">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--wt-text-secondary);margin-top:2px">
<span>0</span><span>0.2</span>
</div>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="wt-btn wt-btn-primary" onclick="runWhisperAccuracyTest()">▶ Run Accuracy Test</button>
<button class="wt-btn wt-btn-secondary" onclick="loadVadConfig()">↻ Load VAD</button>
<button class="wt-btn wt-btn-secondary" onclick="saveVadConfig()">💾 Save VAD</button>
</div>
<div id="accuracySummary" style="margin-top:12px;padding:12px;background:var(--wt-card-bg);border:1px solid var(--wt-border);border-radius:8px;display:none">
<h4 style="margin:0 0 8px 0;font-size:13px;font-weight:600">Test Summary</h4>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:8px;font-size:12px">
<div><strong>Total:</strong> <span id="summaryTotal">0</span></div>
<div><strong>PASS:</strong> <span id="summaryPass" style="color:var(--wt-success)">0</span></div>
<div><strong>WARN:</strong> <span id="summaryWarn" style="color:var(--wt-warning)">0</span></div>
<div><strong>FAIL:</strong> <span id="summaryFail" style="color:var(--wt-danger)">0</span></div>
<div><strong>Avg Accuracy:</strong> <span id="summaryAccuracy">0.0</span>%</div>
<div><strong>Avg Latency:</strong> <span id="summaryLatency">0</span>ms</div>
</div>
</div>
<div id="accuracyResults" style="margin-top:12px"></div>
<canvas id="accuracyTrendChart" style="margin-top:12px;display:none;max-height:200px"></canvas>
</div>
</div>
</div>
<div class="wt-card">
<div class="wt-card-header" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" onclick="toggleCollapsible(this)" onkeydown="if(event.key==='Enter'||event.key===' ')toggleCollapsible(this)"><span class="wt-card-title">Test 4: LLaMA Response Quality</span><span id="prereq-llama" style="margin-left:8px;font-size:10px;padding:2px 6px;border-radius:4px;background:var(--wt-text-secondary);color:#fff">...</span><span style="margin-left:auto;font-size:12px;color:var(--wt-text-secondary)">▶</span></div>
<div class="wt-collapsible">
<div style="padding:0 20px 16px">
<p style="font-size:12px;color:var(--wt-text-secondary);margin-bottom:10px">
Send test prompts directly to LLaMA service and evaluate response quality.
Requires: LLaMA service running. Does not require full pipeline.
</p>
<div class="wt-field">
<label>Test Prompts</label>
<select class="wt-select" id="llamaTestPrompts" multiple style="width:100%;padding:8px;height:100px">
</select>
</div>
<div class="wt-field">
<label>Custom Prompt (optional)</label>
<input class="wt-input" id="llamaCustomPrompt" placeholder="Type a custom German prompt...">
</div>
<div style="padding:10px;background:var(--wt-card-hover);border-radius:6px;margin-top:8px;border-left:3px solid var(--wt-primary)">
<div style="font-size:12px;font-weight:600;color:var(--wt-text-secondary);margin-bottom:6px">LLaMA Generation Settings</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;font-size:12px">
<div class="wt-field"><label>Temperature: <span id="llamaTempValue">0.3</span></label><input type="range" id="llamaTempSlider" min="0.0" max="2.0" value="0.3" step="0.1" style="width:100%" oninput="document.getElementById('llamaTempValue').textContent=parseFloat(this.value).toFixed(1)"><div style="display:flex;justify-content:space-between;font-size:10px;color:var(--wt-text-muted)"><span>0.0</span><span>2.0</span></div></div>
<div class="wt-field"><label>Top-P: <span id="llamaTopPValue">0.95</span></label><input type="range" id="llamaTopPSlider" min="0.1" max="1.0" value="0.95" step="0.05" style="width:100%" oninput="document.getElementById('llamaTopPValue').textContent=parseFloat(this.value).toFixed(2)"><div style="display:flex;justify-content:space-between;font-size:10px;color:var(--wt-text-muted)"><span>0.1</span><span>1.0</span></div></div>
<div class="wt-field"><label>Max Words: <span id="llamaMaxWordsValue">30</span></label><input type="range" id="llamaMaxWordsSlider" min="5" max="200" value="30" step="5" style="width:100%" oninput="document.getElementById('llamaMaxWordsValue').textContent=this.value"><div style="display:flex;justify-content:space-between;font-size:10px;color:var(--wt-text-muted)"><span>5</span><span>200</span></div></div>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="wt-btn wt-btn-primary" id="llamaQualityRunBtn" onclick="runLlamaQualityTest()">▶ Run Quality Test</button>
<button class="wt-btn wt-btn-secondary" onclick="runLlamaShutupTest()">🤐 Shut-up Test</button>
</div>
<div id="llamaTestStatus" style="margin-top:8px;font-size:12px"></div>
<div id="llamaTestResults" style="margin-top:12px"></div>
<div id="llamaShutupResult" style="margin-top:8px"></div>
</div>
</div>
</div>
<div class="wt-card">
<div class="wt-card-header" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" onclick="toggleCollapsible(this)" onkeydown="if(event.key==='Enter'||event.key===' ')toggleCollapsible(this)"><span class="wt-card-title">Test 5: Kokoro TTS Quality</span><span id="prereq-kokoro" style="margin-left:8px;font-size:10px;padding:2px 6px;border-radius:4px;background:var(--wt-text-secondary);color:#fff">...</span><span style="margin-left:auto;font-size:12px;color:var(--wt-text-secondary)">▶</span></div>
<div class="wt-collapsible">
<div style="padding:0 20px 16px">
<p style="font-size:12px;color:var(--wt-text-secondary);margin-bottom:10px">
Synthesize German phrases via Kokoro TTS and measure latency, RTF, audio quality.
Requires: Kokoro service running. Does not require full pipeline.
</p>
<div class="wt-field">
<label>Custom Phrase (optional)</label>
<input class="wt-input" id="kokoroCustomPhrase" placeholder="Type a German phrase to synthesize...">
</div>
<div style="padding:10px;background:var(--wt-card-hover);border-radius:6px;margin-top:8px;border-left:3px solid var(--wt-primary)">
<div style="font-size:12px;font-weight:600;color:var(--wt-text-secondary);margin-bottom:6px">Kokoro TTS Settings</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:12px">
<div class="wt-field"><label>Speed: <span id="kokoroSpeedValue">1.0</span>x</label><input type="range" id="kokoroSpeedSlider" min="0.5" max="2.0" value="1.0" step="0.1" style="width:100%" oninput="document.getElementById('kokoroSpeedValue').textContent=parseFloat(this.value).toFixed(1)"><div style="display:flex;justify-content:space-between;font-size:10px;color:var(--wt-text-muted)"><span>0.5x</span><span>2.0x</span></div></div>
<div class="wt-field"><label>Voice</label><select class="wt-select" id="kokoroVoiceSelect" style="width:100%;padding:6px"><option value="af_heart" selected>af_heart (default)</option><option value="af_sky">af_sky</option><option value="af_star">af_star</option></select></div>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="wt-btn wt-btn-primary" id="kokoroQualityRunBtn" onclick="runKokoroQualityTest()">▶ Run Quality Test</button>
<button class="wt-btn wt-btn-secondary" onclick="runKokoroBenchmark()">⏱ Benchmark</button>
<select class="wt-select" id="kokoroBenchIter" style="width:80px">
<option value="3">3 iter</option>
<option value="5" selected>5 iter</option>
<option value="10">10 iter</option>
</select>
</div>
<div id="kokoroTestStatus" style="margin-top:8px;font-size:12px"></div>
<div id="kokoroTestResults" style="margin-top:12px"></div>
<div id="kokoroBenchResult" style="margin-top:8px"></div>
</div>
</div>
</div>
</div><!-- end beta-component -->
<div class="wt-tab-pane" id="beta-pipeline" role="tabpanel" aria-labelledby="tab-beta-pipeline">
<div class="wt-card">
<div class="wt-card-header" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" onclick="toggleCollapsible(this)" onkeydown="if(event.key==='Enter'||event.key===' ')toggleCollapsible(this)"><span class="wt-card-title">Test 1: Shut-Up Mechanism (Pipeline)</span><span id="prereq-shutup-pipeline" style="margin-left:8px;font-size:10px;padding:2px 6px;border-radius:4px;background:var(--wt-text-secondary);color:#fff">...</span><span style="margin-left:auto;font-size:12px;color:var(--wt-text-secondary)">▶</span></div>
<div class="wt-collapsible">
<div style="padding:0 20px 16px">
<p style="font-size:12px;color:var(--wt-text-secondary);margin-bottom:10px">
Tests the LLaMA shut-up (interrupt / barge-in) mechanism via command port with configurable delays.
Measures generation-interrupt latency across scenarios: immediate, standard (200ms), late (1s), and rapid successive.
Also checks Kokoro and OAP status for signal propagation readiness.
Requires: LLaMA service running. Kokoro + OAP optional (status-checked only).
</p>
<div class="wt-field">
<label>Scenarios</label>
<select class="wt-select" id="shutupScenarios" multiple style="width:100%;padding:8px;height:80px">
<option value="basic" selected>Basic: 200ms delay, interrupt mid-generation</option>
<option value="early" selected>Early: 0ms delay, interrupt immediately</option>
<option value="late" selected>Late: 1000ms delay, interrupt near end</option>
<option value="rapid" selected>Rapid: 3 successive interrupts (100ms delay each)</option>
</select>
</div>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="wt-btn wt-btn-primary" id="shutupPipelineRunBtn" onclick="runShutupPipelineTest()">▶ Run Pipeline Shut-Up Test</button>
</div>
<div id="shutupPipelineStatus" style="margin-top:8px;font-size:12px"></div>
<div id="shutupPipelineResults" style="margin-top:12px"></div>
</div>
</div>
</div>
<div class="wt-card">
<div class="wt-card-header" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" onclick="toggleCollapsible(this)" onkeydown="if(event.key==='Enter'||event.key===' ')toggleCollapsible(this)"><span class="wt-card-title">Test 2: Full Pipeline Round-Trip</span><span id="prereq-roundtrip" style="margin-left:8px;font-size:10px;padding:2px 6px;border-radius:4px;background:var(--wt-text-secondary);color:#fff">...</span><span style="margin-left:auto;font-size:12px;color:var(--wt-text-secondary)">▶</span></div>
<div class="wt-collapsible">
<div style="padding:0 20px 16px">
<p style="font-size:12px;color:var(--wt-text-secondary);margin-bottom:10px">
Full pipeline loop: Phrase → Kokoro WAV → inject → SIP(L1) → IAP → VAD → Whisper → LLaMA → Kokoro → OAP → SIP → relay → SIP(L2) → IAP → VAD → Whisper.
Verifies transcription of injected phrase (Line 1) and LLaMA response (Line 2).
Requires: All services running + active call on test_sip_provider.
</p>
<div class="wt-field">
<label>Custom Phrases (optional, comma-separated)</label>
<input class="wt-input" id="ttsRoundtripPhrases" placeholder="e.g. Hallo Welt, Guten Morgen">
</div>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="wt-btn wt-btn-primary" id="ttsRoundtripBtn" onclick="runTtsRoundtrip()">▶ Run Round-Trip Test</button>
</div>
<div id="ttsRoundtripStatus" style="margin-top:8px;font-size:12px"></div>
<div id="ttsRoundtripResults" style="margin-top:12px"></div>
</div>
</div>
</div>
<div class="wt-card">
<div class="wt-card-header" style="cursor:pointer" role="button" tabindex="0" aria-expanded="false" onclick="toggleCollapsible(this)" onkeydown="if(event.key==='Enter'||event.key===' ')toggleCollapsible(this)"><span class="wt-card-title">Test 3: Full Loop File Test (WER)</span><span id="prereq-fullloop" style="margin-left:8px;font-size:10px;padding:2px 6px;border-radius:4px;background:var(--wt-text-secondary);color:#fff">...</span><span style="margin-left:auto;font-size:12px;color:var(--wt-text-secondary)">▶</span></div>
<div class="wt-collapsible">
<div style="padding:0 20px 16px">
<p id="fullLoopDesc" style="font-size:12px;color:var(--wt-text-secondary);margin-bottom:10px">
Injects test audio files through the pipeline and measures Word Error Rate (WER).