Andhs commited on
Commit
b8d11d8
·
verified ·
1 Parent(s): df717ea

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +40 -338
app.py CHANGED
@@ -10,337 +10,6 @@ from flask import Flask, request, jsonify, Response
10
  from transformers import AutoTokenizer, AutoModelForMaskedLM, AutoModelForCausalLM, TextIteratorStreamer
11
  from threading import Thread
12
 
13
- HTML_UI = """
14
- <!DOCTYPE html>
15
- <html lang="en">
16
- <head>
17
- <meta charset="UTF-8">
18
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
- <title>LLM API Tester</title>
20
- <style>
21
- * { box-sizing: border-box; }
22
- body {
23
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
24
- max-width: 900px;
25
- margin: 0 auto;
26
- padding: 20px;
27
- background: #f5f5f5;
28
- color: #333;
29
- }
30
- h1 { margin-top: 0; font-size: 1.5rem; }
31
- .card {
32
- background: #fff;
33
- border-radius: 8px;
34
- padding: 20px;
35
- margin-bottom: 16px;
36
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
37
- }
38
- label {
39
- display: block;
40
- font-weight: 600;
41
- margin-bottom: 6px;
42
- font-size: 0.9rem;
43
- }
44
- textarea, input, select {
45
- width: 100%;
46
- padding: 10px;
47
- border: 1px solid #ddd;
48
- border-radius: 6px;
49
- font-size: 0.95rem;
50
- font-family: inherit;
51
- }
52
- textarea { resize: vertical; min-height: 80px; }
53
- .row {
54
- display: grid;
55
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
56
- gap: 12px;
57
- margin-bottom: 12px;
58
- }
59
- .field { margin-bottom: 12px; }
60
- .field.inline {
61
- display: flex;
62
- align-items: center;
63
- gap: 8px;
64
- }
65
- .field.inline label { margin: 0; }
66
- .field.inline input, .field.inline select {
67
- width: auto;
68
- flex: 1;
69
- }
70
- button {
71
- background: #2563eb;
72
- color: #fff;
73
- border: none;
74
- padding: 10px 20px;
75
- border-radius: 6px;
76
- font-size: 1rem;
77
- cursor: pointer;
78
- font-weight: 600;
79
- }
80
- button:hover { background: #1d4ed8; }
81
- button:disabled { background: #93c5fd; cursor: not-allowed; }
82
- .output {
83
- background: #1e1e1e;
84
- color: #e4e4e4;
85
- padding: 16px;
86
- border-radius: 6px;
87
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
88
- font-size: 0.9rem;
89
- white-space: pre-wrap;
90
- word-break: break-word;
91
- min-height: 120px;
92
- max-height: 500px;
93
- overflow-y: auto;
94
- }
95
- .output:empty::before {
96
- content: "Response will appear here...";
97
- color: #666;
98
- }
99
- .status {
100
- font-size: 0.85rem;
101
- color: #666;
102
- margin-top: 8px;
103
- }
104
- .error { color: #dc2626; }
105
- .success { color: #16a34a; }
106
- .route-badge {
107
- display: inline-block;
108
- background: #e5e7eb;
109
- padding: 2px 8px;
110
- border-radius: 4px;
111
- font-size: 0.8rem;
112
- font-weight: 600;
113
- margin-bottom: 8px;
114
- }
115
- </style>
116
- </head>
117
- <body>
118
- <h1>LLM API Tester</h1>
119
-
120
- <div class="card">
121
- <div class="route-badge" id="routeBadge">/generate</div>
122
- <div class="field">
123
- <label for="route">Route</label>
124
- <select id="route">
125
- <option value="/generate">/generate (sync JSON)</option>
126
- <option value="/generate_stream">/generate_stream (sync JSON + intermediates)</option>
127
- <option value="/generate_sse">/generate_sse (streaming SSE)</option>
128
- </select>
129
- </div>
130
-
131
- <div class="field">
132
- <label for="prompt">Prompt</label>
133
- <textarea id="prompt" placeholder="Enter your prompt here...">Write a short poem about stars</textarea>
134
- </div>
135
-
136
- <div class="row">
137
- <div class="field">
138
- <label for="max_new_tokens">max_new_tokens</label>
139
- <input type="number" id="max_new_tokens" value="150" min="1" max="2048">
140
- </div>
141
- <div class="field">
142
- <label for="temperature">temperature</label>
143
- <input type="number" id="temperature" value="0.0" min="0" max="2" step="0.1">
144
- </div>
145
- <div class="field">
146
- <label for="steps">steps (diffusion)</label>
147
- <input type="number" id="steps" value="256" min="1">
148
- </div>
149
- <div class="field">
150
- <label for="block_size">block_size</label>
151
- <input type="number" id="block_size" value="32" min="1">
152
- </div>
153
- </div>
154
-
155
- <div class="row">
156
- <div class="field">
157
- <label for="cfg_scale">cfg_scale</label>
158
- <input type="number" id="cfg_scale" value="0.0" min="0" step="0.1">
159
- </div>
160
- <div class="field">
161
- <label for="remasking">remasking</label>
162
- <select id="remasking">
163
- <option value="low_confidence">low_confidence</option>
164
- <option value="random">random</option>
165
- </select>
166
- </div>
167
- <div class="field">
168
- <label for="capture_interval">capture_interval</label>
169
- <input type="number" id="capture_interval" value="10" min="1">
170
- </div>
171
- </div>
172
-
173
- <button id="sendBtn">Send Request</button>
174
- <div class="status" id="status"></div>
175
- </div>
176
-
177
- <div class="card">
178
- <label>Response</label>
179
- <div class="output" id="output"></div>
180
- </div>
181
-
182
- <script>
183
- const $ = id => document.getElementById(id);
184
- const routeSelect = $('route');
185
- const routeBadge = $('routeBadge');
186
- const sendBtn = $('sendBtn');
187
- const output = $('output');
188
- const status = $('status');
189
-
190
- routeSelect.addEventListener('change', () => {
191
- routeBadge.textContent = routeSelect.value;
192
- });
193
-
194
- function setStatus(msg, isError = false) {
195
- status.textContent = msg;
196
- status.className = 'status ' + (isError ? 'error' : 'success');
197
- }
198
-
199
- function appendOutput(text, clear = false) {
200
- if (clear) output.textContent = '';
201
- output.textContent += text;
202
- output.scrollTop = output.scrollHeight;
203
- }
204
-
205
- function getPayload() {
206
- return {
207
- prompt: $('prompt').value,
208
- max_new_tokens: parseInt($('max_new_tokens').value),
209
- temperature: parseFloat($('temperature').value),
210
- steps: parseInt($('steps').value),
211
- block_size: parseInt($('block_size').value),
212
- cfg_scale: parseFloat($('cfg_scale').value),
213
- remasking: $('remasking').value,
214
- capture_interval: parseInt($('capture_interval').value)
215
- };
216
- }
217
-
218
- async function handleGenerate() {
219
- const payload = getPayload();
220
- // Remove diffusion-only fields for non-diffusion if needed, but server ignores extras
221
- const t0 = performance.now();
222
- const res = await fetch('/generate', {
223
- method: 'POST',
224
- headers: { 'Content-Type': 'application/json' },
225
- body: JSON.stringify(payload)
226
- });
227
- const data = await res.json();
228
- const ms = Math.round(performance.now() - t0);
229
- if (res.ok) {
230
- appendOutput(`[${ms}ms]\n${data.generated_text || JSON.stringify(data, null, 2)}\n\n`, true);
231
- setStatus(`OK — ${ms}ms`);
232
- } else {
233
- appendOutput(`Error ${res.status}:\n${JSON.stringify(data, null, 2)}\n\n`, true);
234
- setStatus(`HTTP ${res.status}`, true);
235
- }
236
- }
237
-
238
- async function handleGenerateStream() {
239
- const payload = getPayload();
240
- const t0 = performance.now();
241
- const res = await fetch('/generate_stream', {
242
- method: 'POST',
243
- headers: { 'Content-Type': 'application/json' },
244
- body: JSON.stringify(payload)
245
- });
246
- const data = await res.json();
247
- const ms = Math.round(performance.now() - t0);
248
- if (res.ok) {
249
- let text = `[${ms}ms]\nGenerated text:\n${data.generated_text}\n\n`;
250
- if (data.intermediate_states && data.intermediate_states.length) {
251
- text += `Intermediate states (${data.intermediate_states.length}):\n`;
252
- data.intermediate_states.forEach((s, i) => {
253
- text += ` Step ${s.step}: ${s.text.substring(0, 120).replace(/\n/g, ' ')}...\n`;
254
- });
255
- }
256
- appendOutput(text + '\n', true);
257
- setStatus(`OK — ${ms}ms, ${data.intermediate_states?.length || 0} intermediates`);
258
- } else {
259
- appendOutput(`Error ${res.status}:\n${JSON.stringify(data, null, 2)}\n\n`, true);
260
- setStatus(`HTTP ${res.status}`, true);
261
- }
262
- }
263
-
264
- async function handleGenerateSSE() {
265
- const payload = getPayload();
266
- const t0 = performance.now();
267
- appendOutput('', true);
268
- setStatus('Connecting SSE...');
269
-
270
- const res = await fetch('/generate_sse', {
271
- method: 'POST',
272
- headers: { 'Content-Type': 'application/json' },
273
- body: JSON.stringify(payload)
274
- });
275
-
276
- if (!res.ok) {
277
- const data = await res.json().catch(() => ({}));
278
- appendOutput(`Error ${res.status}:\n${JSON.stringify(data, null, 2)}`, true);
279
- setStatus(`HTTP ${res.status}`, true);
280
- return;
281
- }
282
-
283
- const reader = res.body.getReader();
284
- const decoder = new TextDecoder();
285
- let buffer = '';
286
- let finalText = '';
287
- let eventCount = 0;
288
-
289
- while (true) {
290
- const { done, value } = await reader.read();
291
- if (done) break;
292
- buffer += decoder.decode(value, { stream: true });
293
- const lines = buffer.split('\n');
294
- buffer = lines.pop(); // keep incomplete line in buffer
295
-
296
- for (const line of lines) {
297
- if (!line.startsWith('data: ')) continue;
298
- const jsonStr = line.slice(6).trim();
299
- if (!jsonStr) continue;
300
- try {
301
- const event = JSON.parse(jsonStr);
302
- eventCount++;
303
- if (event.type === 'final') {
304
- finalText = event.text;
305
- const ms = Math.round(performance.now() - t0);
306
- appendOutput(`[${ms}ms | ${eventCount} events]\n${finalText}\n`, true);
307
- setStatus(`Done — ${ms}ms, ${eventCount} events, ${event.total_steps || '?'} steps`);
308
- } else if (event.type === 'intermediate' || event.type === 'token') {
309
- // Live update: overwrite with latest accumulated text
310
- appendOutput(`${event.text}`, true);
311
- setStatus(`Streaming... (${eventCount} events)`);
312
- }
313
- } catch (e) {
314
- // ignore malformed lines
315
- }
316
- }
317
- }
318
-
319
- if (!finalText && eventCount === 0) {
320
- setStatus('Stream ended with no events', true);
321
- }
322
- }
323
-
324
- sendBtn.addEventListener('click', async () => {
325
- sendBtn.disabled = true;
326
- setStatus('Sending...');
327
- try {
328
- const route = routeSelect.value;
329
- if (route === '/generate') await handleGenerate();
330
- else if (route === '/generate_stream') await handleGenerateStream();
331
- else if (route === '/generate_sse') await handleGenerateSSE();
332
- } catch (err) {
333
- appendOutput(`Network/JS Error:\n${err.message}\n\n`, true);
334
- setStatus(err.message, true);
335
- } finally {
336
- sendBtn.disabled = false;
337
- }
338
- });
339
- </script>
340
- </body>
341
- </html>
342
- """
343
-
344
  # 1. Environment Parsing & Architecture Strategy Mapping
345
  MODEL_NAME = os.getenv("MODEL_NAME", "dllm-hub/Qwen3-0.6B-diffusion-bd3lm-v0.1")
346
  IS_DIFFUSION = "diffusion" in MODEL_NAME.lower()
@@ -410,13 +79,11 @@ def clone_past_key_values(pkv):
410
  """Clone KV-cache. Fast path for tuples and Cache objects; falls back to deepcopy."""
411
  if pkv is None:
412
  return None
413
- # Fast path: legacy tuple format
414
  if isinstance(pkv, tuple):
415
  return tuple(
416
  (k.clone() if k is not None else None, v.clone() if v is not None else None)
417
  for k, v in pkv
418
  )
419
- # Fast path: transformers Cache objects (DynamicCache, etc.)
420
  if hasattr(pkv, 'key_cache') and hasattr(pkv, 'value_cache'):
421
  try:
422
  new_cache = pkv.__class__()
@@ -428,7 +95,6 @@ def clone_past_key_values(pkv):
428
  return new_cache
429
  except Exception:
430
  pass
431
- # Fallback
432
  return copy.deepcopy(pkv)
433
 
434
 
@@ -666,7 +332,6 @@ def load_model():
666
  torch_dtype=torch.bfloat16,
667
  trust_remote_code=False
668
  ).to(device).eval()
669
- # Compile model for faster inference — ONLY for standard causal models
670
  if not IS_DIFFUSION:
671
  try:
672
  model = torch.compile(model, mode="reduce-overhead", fullgraph=False)
@@ -679,7 +344,7 @@ def load_model():
679
  MODEL_NAME,
680
  trust_remote_code=IS_DIFFUSION
681
  )
682
- print("Model compilation completed and loaded into memory workspace.")
683
 
684
 
685
  @app.route('/health', methods=['GET'])
@@ -702,7 +367,6 @@ def generate_text():
702
  {"role": "system", "content": system_prompt},
703
  {"role": "user", "content": prompt}
704
  ]
705
- # enable_thinking=False for ALL routes to prevent Qwen3 from leaking internal monologue
706
  encoded = tokenizer.apply_chat_template(
707
  messages,
708
  add_generation_prompt=True,
@@ -847,7 +511,7 @@ def generate_text_sse():
847
 
848
  accumulated = []
849
  for text in streamer:
850
- if not text: # skip empty chunks
851
  continue
852
  accumulated.append(text)
853
  current = "".join(accumulated)
@@ -862,10 +526,48 @@ def generate_text_sse():
862
  )
863
 
864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
  @app.route('/')
866
  def index():
867
  return Response(HTML_UI, mimetype='text/html')
868
 
 
869
  if __name__ == '__main__':
870
  load_model()
871
  app.run(host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
 
10
  from transformers import AutoTokenizer, AutoModelForMaskedLM, AutoModelForCausalLM, TextIteratorStreamer
11
  from threading import Thread
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  # 1. Environment Parsing & Architecture Strategy Mapping
14
  MODEL_NAME = os.getenv("MODEL_NAME", "dllm-hub/Qwen3-0.6B-diffusion-bd3lm-v0.1")
15
  IS_DIFFUSION = "diffusion" in MODEL_NAME.lower()
 
79
  """Clone KV-cache. Fast path for tuples and Cache objects; falls back to deepcopy."""
80
  if pkv is None:
81
  return None
 
82
  if isinstance(pkv, tuple):
83
  return tuple(
84
  (k.clone() if k is not None else None, v.clone() if v is not None else None)
85
  for k, v in pkv
86
  )
 
87
  if hasattr(pkv, 'key_cache') and hasattr(pkv, 'value_cache'):
88
  try:
89
  new_cache = pkv.__class__()
 
95
  return new_cache
96
  except Exception:
97
  pass
 
98
  return copy.deepcopy(pkv)
99
 
100
 
 
332
  torch_dtype=torch.bfloat16,
333
  trust_remote_code=False
334
  ).to(device).eval()
 
335
  if not IS_DIFFUSION:
336
  try:
337
  model = torch.compile(model, mode="reduce-overhead", fullgraph=False)
 
344
  MODEL_NAME,
345
  trust_remote_code=IS_DIFFUSION
346
  )
347
+ print("Model loaded into memory workspace.")
348
 
349
 
350
  @app.route('/health', methods=['GET'])
 
367
  {"role": "system", "content": system_prompt},
368
  {"role": "user", "content": prompt}
369
  ]
 
370
  encoded = tokenizer.apply_chat_template(
371
  messages,
372
  add_generation_prompt=True,
 
511
 
512
  accumulated = []
513
  for text in streamer:
514
+ if not text:
515
  continue
516
  accumulated.append(text)
517
  current = "".join(accumulated)
 
526
  )
527
 
528
 
529
+ HTML_UI = """<!DOCTYPE html>
530
+ <html lang="en">
531
+ <head>
532
+ <meta charset="UTF-8">
533
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
534
+ <title>LLM API Tester</title>
535
+ <style>
536
+ *{box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:900px;margin:0 auto;padding:20px;background:#f5f5f5;color:#333}h1{margin-top:0;font-size:1.5rem}.card{background:#fff;border-radius:8px;padding:20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.1)}label{display:block;font-weight:600;margin-bottom:6px;font-size:.9rem}textarea,input,select{width:100%;padding:10px;border:1px solid #ddd;border-radius:6px;font-size:.95rem;font-family:inherit}textarea{resize:vertical;min-height:80px}.row{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:12px}.field{margin-bottom:12px}button{background:#2563eb;color:#fff;border:none;padding:10px 20px;border-radius:6px;font-size:1rem;cursor:pointer;font-weight:600}button:hover{background:#1d4ed8}button:disabled{background:#93c5fd;cursor:not-allowed}.output{background:#1e1e1e;color:#e4e4e4;padding:16px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;font-size:.9rem;white-space:pre-wrap;word-break:break-word;min-height:120px;max-height:500px;overflow-y:auto}.output:empty::before{content:"Response will appear here...";color:#666}.status{font-size:.85rem;color:#666;margin-top:8px}.error{color:#dc2626}.success{color:#16a34a}.route-badge{display:inline-block;background:#e5e7eb;padding:2px 8px;border-radius:4px;font-size:.8rem;font-weight:600;margin-bottom:8px}
537
+ </style>
538
+ </head>
539
+ <body>
540
+ <h1>LLM API Tester</h1>
541
+ <div class="card">
542
+ <div class="route-badge" id="routeBadge">/generate</div>
543
+ <div class="field"><label for="route">Route</label><select id="route"><option value="/generate">/generate (sync JSON)</option><option value="/generate_stream">/generate_stream (sync JSON + intermediates)</option><option value="/generate_sse">/generate_sse (streaming SSE)</option></select></div>
544
+ <div class="field"><label for="prompt">Prompt</label><textarea id="prompt" placeholder="Enter your prompt here...">Write a short poem about stars</textarea></div>
545
+ <div class="row"><div class="field"><label for="max_new_tokens">max_new_tokens</label><input type="number" id="max_new_tokens" value="150" min="1" max="2048"></div><div class="field"><label for="temperature">temperature</label><input type="number" id="temperature" value="0.0" min="0" max="2" step="0.1"></div><div class="field"><label for="steps">steps (diffusion)</label><input type="number" id="steps" value="256" min="1"></div><div class="field"><label for="block_size">block_size</label><input type="number" id="block_size" value="32" min="1"></div></div>
546
+ <div class="row"><div class="field"><label for="cfg_scale">cfg_scale</label><input type="number" id="cfg_scale" value="0.0" min="0" step="0.1"></div><div class="field"><label for="remasking">remasking</label><select id="remasking"><option value="low_confidence">low_confidence</option><option value="random">random</option></select></div><div class="field"><label for="capture_interval">capture_interval</label><input type="number" id="capture_interval" value="10" min="1"></div></div>
547
+ <button id="sendBtn">Send Request</button>
548
+ <div class="status" id="status"></div>
549
+ </div>
550
+ <div class="card"><label>Response</label><div class="output" id="output"></div></div>
551
+ <script>
552
+ const $=id=>document.getElementById(id);
553
+ const routeSelect=$('route');const routeBadge=$('routeBadge');const sendBtn=$('sendBtn');const output=$('output');const status=$('status');
554
+ routeSelect.addEventListener('change',()=>{routeBadge.textContent=routeSelect.value;});
555
+ function setStatus(msg,isError=false){status.textContent=msg;status.className='status '+(isError?'error':'success');}
556
+ function appendOutput(text,clear=false){if(clear)output.textContent='';output.textContent+=text;output.scrollTop=output.scrollHeight;}
557
+ function getPayload(){return{prompt:$('prompt').value,max_new_tokens:parseInt($('max_new_tokens').value),temperature:parseFloat($('temperature').value),steps:parseInt($('steps').value),block_size:parseInt($('block_size').value),cfg_scale:parseFloat($('cfg_scale').value),remasking:$('remasking').value,capture_interval:parseInt($('capture_interval').value)};}
558
+ async function handleGenerate(){const payload=getPayload();const t0=performance.now();const res=await fetch('/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});const data=await res.json();const ms=Math.round(performance.now()-t0);if(res.ok){appendOutput(`[${ms}ms]\n${data.generated_text||JSON.stringify(data,null,2)}\n\n`,true);setStatus(`OK — ${ms}ms`);}else{appendOutput(`Error ${res.status}:\n${JSON.stringify(data,null,2)}\n\n`,true);setStatus(`HTTP ${res.status}`,true);}}
559
+ async function handleGenerateStream(){const payload=getPayload();const t0=performance.now();const res=await fetch('/generate_stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});const data=await res.json();const ms=Math.round(performance.now()-t0);if(res.ok){let text=`[${ms}ms]\nGenerated text:\n${data.generated_text}\n\n`;if(data.intermediate_states&&data.intermediate_states.length){text+=`Intermediate states (${data.intermediate_states.length}):\n`;data.intermediate_states.forEach((s,i)=>{text+=` Step ${s.step}: ${s.text.substring(0,120).replace(/\n/g,' ')}...\n`;});}appendOutput(text+'\n',true);setStatus(`OK — ${ms}ms, ${data.intermediate_states?.length||0} intermediates`);}else{appendOutput(`Error ${res.status}:\n${JSON.stringify(data,null,2)}\n\n`,true);setStatus(`HTTP ${res.status}`,true);}}
560
+ async function handleGenerateSSE(){const payload=getPayload();const t0=performance.now();appendOutput('',true);setStatus('Connecting SSE...');const res=await fetch('/generate_sse',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});if(!res.ok){const data=await res.json().catch(()=>({}));appendOutput(`Error ${res.status}:\n${JSON.stringify(data,null,2)}`,true);setStatus(`HTTP ${res.status}`,true);return;}const reader=res.body.getReader();const decoder=new TextDecoder();let buffer='';let finalText='';let eventCount=0;while(true){const {done,value}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});const lines=buffer.split('\n');buffer=lines.pop();for(const line of lines){if(!line.startsWith('data: '))continue;const jsonStr=line.slice(6).trim();if(!jsonStr)continue;try{const event=JSON.parse(jsonStr);eventCount++;if(event.type==='final'){finalText=event.text;const ms=Math.round(performance.now()-t0);appendOutput(`[${ms}ms | ${eventCount} events]\n${finalText}\n`,true);setStatus(`Done — ${ms}ms, ${eventCount} events, ${event.total_steps||'?'} steps`);}else if(event.type==='intermediate'||event.type==='token'){appendOutput(`${event.text}`,true);setStatus(`Streaming... (${eventCount} events)`);}}catch(e){}}}if(!finalText&&eventCount===0){setStatus('Stream ended with no events',true);}}
561
+ sendBtn.addEventListener('click',async()=>{sendBtn.disabled=true;setStatus('Sending...');try{const route=routeSelect.value;if(route==='/generate')await handleGenerate();else if(route==='/generate_stream')await handleGenerateStream();else if(route==='/generate_sse')await handleGenerateSSE();}catch(err){appendOutput(`Network/JS Error:\n${err.message}\n\n`,true);setStatus(err.message,true);}finally{sendBtn.disabled=false;}});
562
+ </script>
563
+ </body>
564
+ </html>"""
565
+
566
  @app.route('/')
567
  def index():
568
  return Response(HTML_UI, mimetype='text/html')
569
 
570
+
571
  if __name__ == '__main__':
572
  load_model()
573
  app.run(host='0.0.0.0', port=int(os.getenv('PORT', 7860)))