TULLUS commited on
Commit
9c36462
·
verified ·
1 Parent(s): 49e6650

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +663 -0
app.py ADDED
@@ -0,0 +1,663 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TULLUS Space Station — CodeParrot Mini Lab.
2
+
3
+ A small Gradio coding-completion interface around the published
4
+ TULLUS/codeparrot-small-multi checkpoint. This is inference only.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import threading
11
+ from typing import Any
12
+
13
+ import gradio as gr
14
+ import spaces
15
+ import torch
16
+ from transformers import (
17
+ AutoModelForCausalLM,
18
+ AutoTokenizer,
19
+ StoppingCriteria,
20
+ StoppingCriteriaList,
21
+ )
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Model/runtime setup. ZeroGPU's documented pattern places the model on CUDA
26
+ # at module import time; only calls to generate() are inside @spaces.GPU.
27
+ # ---------------------------------------------------------------------------
28
+
29
+ MODEL_ID = "TULLUS/codeparrot-small-multi"
30
+ CHECKPOINT_DTYPE = torch.float32 # Preserve the repository config.json dtype: F32.
31
+ MAX_NEW_TOKENS_UI = 384
32
+ MAX_CONTEXT_PROMPT_CHARS = 40_000 # Secondary guard before tokenization.
33
+
34
+ logging.basicConfig(level=logging.INFO)
35
+ LOGGER = logging.getLogger("tullus_codeparrot_mini_lab")
36
+
37
+ TOKENIZER = None
38
+ MODEL = None
39
+ MODEL_DEVICE = "unavailable"
40
+ MODEL_LOAD_ERROR: str | None = None
41
+ CONTEXT_TOKENS = 1024 # Safe display fallback; replaced from loaded config.
42
+ PARAMETER_COUNT = 0
43
+ RUNTIME_DTYPE = "unavailable"
44
+ HAS_CHAT_TEMPLATE = False
45
+
46
+ # A stop event per Gradio session lets the Stop button request cooperative
47
+ # cancellation during model.generate(), without one visitor stopping another.
48
+ _STOP_EVENTS: dict[str, threading.Event] = {}
49
+ _STOP_EVENTS_LOCK = threading.Lock()
50
+
51
+ LANGUAGES = [
52
+ "Auto",
53
+ "Python",
54
+ "JavaScript",
55
+ "PHP",
56
+ "Java",
57
+ "C#",
58
+ "C++",
59
+ "Go",
60
+ "Ruby",
61
+ "TypeScript",
62
+ ]
63
+ COMMENT_PREFIX = {
64
+ "Python": "#",
65
+ "Ruby": "#",
66
+ "JavaScript": "//",
67
+ "PHP": "//",
68
+ "Java": "//",
69
+ "C#": "//",
70
+ "C++": "//",
71
+ "Go": "//",
72
+ "TypeScript": "//",
73
+ }
74
+
75
+
76
+ class StopOnEvent(StoppingCriteria):
77
+ """Stop generation once the current session's Stop button is pressed."""
78
+
79
+ def __init__(self, event: threading.Event):
80
+ self.event = event
81
+
82
+ def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs: Any) -> bool:
83
+ return self.event.is_set()
84
+
85
+
86
+ def _get_context_capacity(config: Any) -> int:
87
+ """Read the positional limit from the loaded architecture config."""
88
+ for attribute in ("max_position_embeddings", "n_positions", "n_ctx"):
89
+ value = getattr(config, attribute, None)
90
+ if isinstance(value, int) and 0 < value < 1_000_000:
91
+ return value
92
+ return 1024
93
+
94
+
95
+ def _load_model_once() -> None:
96
+ """Initialize shared model/tokenizer once per Space process."""
97
+ global TOKENIZER, MODEL, MODEL_DEVICE, MODEL_LOAD_ERROR
98
+ global CONTEXT_TOKENS, PARAMETER_COUNT, RUNTIME_DTYPE, HAS_CHAT_TEMPLATE
99
+
100
+ try:
101
+ LOGGER.info("Loading tokenizer from %s", MODEL_ID)
102
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
103
+
104
+ LOGGER.info("Loading published Safetensors checkpoint from %s", MODEL_ID)
105
+ model = AutoModelForCausalLM.from_pretrained(
106
+ MODEL_ID,
107
+ torch_dtype=CHECKPOINT_DTYPE,
108
+ use_safetensors=True,
109
+ )
110
+
111
+ # Hugging Face ZeroGPU emulates CUDA at process initialization. The CPU
112
+ # branch keeps import/startup diagnosable in non-ZeroGPU environments;
113
+ # intended hosted deployment is a Space configured for ZeroGPU.
114
+ if torch.cuda.is_available():
115
+ model = model.to("cuda")
116
+ MODEL_DEVICE = "cuda"
117
+ else:
118
+ model = model.to("cpu")
119
+ MODEL_DEVICE = "cpu"
120
+
121
+ model.eval()
122
+ TOKENIZER = tokenizer
123
+ MODEL = model
124
+ CONTEXT_TOKENS = _get_context_capacity(model.config)
125
+ PARAMETER_COUNT = sum(parameter.numel() for parameter in model.parameters())
126
+ RUNTIME_DTYPE = str(next(model.parameters()).dtype).replace("torch.", "")
127
+ HAS_CHAT_TEMPLATE = bool(getattr(tokenizer, "chat_template", None))
128
+ LOGGER.info(
129
+ "Model ready: parameters=%s context=%s dtype=%s device=%s chat_template=%s",
130
+ f"{PARAMETER_COUNT:,}",
131
+ CONTEXT_TOKENS,
132
+ RUNTIME_DTYPE,
133
+ MODEL_DEVICE,
134
+ HAS_CHAT_TEMPLATE,
135
+ )
136
+ except Exception as exc: # Keep the interface alive and show a useful status.
137
+ MODEL_LOAD_ERROR = f"{type(exc).__name__}: {exc}"
138
+ LOGGER.exception("Model initialization failed")
139
+ TOKENIZER = None
140
+ MODEL = None
141
+ MODEL_DEVICE = "unavailable"
142
+ RUNTIME_DTYPE = "unavailable"
143
+
144
+
145
+ _load_model_once()
146
+
147
+
148
+ def _startup_status() -> str:
149
+ if MODEL is None or TOKENIZER is None:
150
+ return (
151
+ "🟠 **MODEL OFFLINE** — the interface loaded, but model initialization failed. "
152
+ "Check the Space logs for the specific loading error."
153
+ )
154
+ if MODEL_DEVICE == "cuda":
155
+ return "🟢 **SYSTEMS ONLINE** · model loaded · ZeroGPU inference ready"
156
+ return (
157
+ "🟡 **MODEL LOADED ON CPU** · the app did not detect CUDA at startup. "
158
+ "For hosted GPU inference, set this Space's hardware to ZeroGPU."
159
+ )
160
+
161
+
162
+ def _as_text(content: Any) -> str:
163
+ """Convert Gradio message content to ordinary text without role templating."""
164
+ if isinstance(content, str):
165
+ return content
166
+ if isinstance(content, list):
167
+ chunks = []
168
+ for item in content:
169
+ if isinstance(item, dict) and item.get("type") == "text":
170
+ chunks.append(str(item.get("text", "")))
171
+ return "\n".join(chunks)
172
+ return str(content or "")
173
+
174
+
175
+ def _recent_completed_pairs(history: list[dict[str, Any]]) -> list[tuple[str, str]]:
176
+ """Extract only completed user/assistant pairs from recent UI history."""
177
+ pairs: list[tuple[str, str]] = []
178
+ pending_user: str | None = None
179
+ for message in history or []:
180
+ role = message.get("role")
181
+ body = _as_text(message.get("content", "")).strip("\n")
182
+ if role == "user":
183
+ pending_user = body
184
+ elif role == "assistant" and pending_user is not None:
185
+ # Don't inject prior application error messages as model context.
186
+ if body and not body.startswith("⚠️"):
187
+ pairs.append((pending_user, body))
188
+ pending_user = None
189
+ return pairs[-2:]
190
+
191
+
192
+ def _language_prefix(language: str) -> str:
193
+ if language == "Auto" or language not in COMMENT_PREFIX:
194
+ return ""
195
+ marker = COMMENT_PREFIX[language]
196
+ return f"{marker} Target language: {language}\n"
197
+
198
+
199
+ def _make_prompt(
200
+ current_message: str,
201
+ history: list[dict[str, Any]],
202
+ language: str,
203
+ use_recent_context: bool,
204
+ ) -> tuple[str, bool]:
205
+ """Build a transparent completion prompt; never apply a chat template.
206
+
207
+ Optional history is represented as commented request headings and raw prior
208
+ completions. Oldest context is removed first if required by the real limit.
209
+ The current prompt is never silently truncated.
210
+ """
211
+ language_prefix = _language_prefix(language)
212
+ base_prompt = language_prefix + current_message
213
+ base_ids = TOKENIZER(base_prompt, add_special_tokens=True, truncation=False)["input_ids"]
214
+ if len(base_ids) >= CONTEXT_TOKENS:
215
+ raise ValueError(
216
+ f"Your current prompt uses {len(base_ids)} tokens, but this model has a "
217
+ f"{CONTEXT_TOKENS}-token context. Shorten the current prompt to leave room "
218
+ "for generated tokens, then try again. The app did not truncate your code."
219
+ )
220
+
221
+ pairs = _recent_completed_pairs(history) if use_recent_context else []
222
+ marker = COMMENT_PREFIX.get(language, "#")
223
+ trimmed = False
224
+
225
+ def assemble(selected_pairs: list[tuple[str, str]]) -> str:
226
+ if not selected_pairs:
227
+ return base_prompt
228
+ context_sections: list[str] = [f"{marker} Earlier coding context (reference only):"]
229
+ for request_text, completion_text in selected_pairs:
230
+ context_sections.append(f"{marker} Earlier request:")
231
+ context_sections.extend(
232
+ f"{marker} {line}" if line else marker
233
+ for line in request_text.splitlines()
234
+ )
235
+ context_sections.append(f"{marker} Earlier completion follows:")
236
+ context_sections.append(completion_text)
237
+ context_sections.append(f"\n{marker} End earlier completion.")
238
+ context_sections.append(f"{marker} Current request follows:")
239
+ return "\n".join(context_sections) + "\n" + base_prompt
240
+
241
+ prompt = assemble(pairs)
242
+ while pairs:
243
+ count = len(TOKENIZER(prompt, add_special_tokens=True, truncation=False)["input_ids"])
244
+ if count < CONTEXT_TOKENS:
245
+ break
246
+ pairs.pop(0)
247
+ trimmed = True
248
+ prompt = assemble(pairs)
249
+
250
+ prompt_count = len(TOKENIZER(prompt, add_special_tokens=True, truncation=False)["input_ids"])
251
+ if prompt_count >= CONTEXT_TOKENS:
252
+ # This should only happen if the current prompt itself fills the window;
253
+ # retain an explicit guard if tokenizer behavior changes.
254
+ raise ValueError(
255
+ f"The assembled prompt uses {prompt_count} tokens; context capacity is "
256
+ f"{CONTEXT_TOKENS}. Shorten the current prompt."
257
+ )
258
+ return prompt, trimmed
259
+
260
+
261
+ def _error_result(history: list[dict[str, Any]], message: str, status: str):
262
+ updated = list(history or [])
263
+ updated.append({"role": "user", "content": message})
264
+ updated.append({"role": "assistant", "content": f"⚠️ {status}"})
265
+ return updated, "", f"🟠 **INPUT / INFERENCE ISSUE** · {status}"
266
+
267
+
268
+ @spaces.GPU(duration=60)
269
+ def respond(
270
+ message: str,
271
+ history: list[dict[str, Any]] | None,
272
+ language: str,
273
+ max_new_tokens: int,
274
+ temperature: float,
275
+ top_p: float,
276
+ repetition_penalty: float,
277
+ sample: bool,
278
+ use_recent_context: bool,
279
+ request: gr.Request,
280
+ progress=gr.Progress(),
281
+ ):
282
+ """Generate a completion inside a ZeroGPU-decorated function."""
283
+ chat_history = list(history or [])
284
+ message = message if isinstance(message, str) else str(message or "")
285
+
286
+ if not message.strip():
287
+ return chat_history, "", "🟡 **READY** · Enter a code prefix or a coding request to begin."
288
+ if len(message) > MAX_CONTEXT_PROMPT_CHARS:
289
+ return _error_result(
290
+ chat_history,
291
+ message,
292
+ f"The prompt is longer than the {MAX_CONTEXT_PROMPT_CHARS:,}-character safety limit. "
293
+ "Please shorten it before retrying.",
294
+ )
295
+ if MODEL is None or TOKENIZER is None:
296
+ return _error_result(
297
+ chat_history,
298
+ message,
299
+ "The model did not initialize. Check the Space logs, then restart the Space.",
300
+ )
301
+
302
+ session_id = getattr(request, "session_hash", None) or "local-session"
303
+ stop_event = threading.Event()
304
+ with _STOP_EVENTS_LOCK:
305
+ _STOP_EVENTS[session_id] = stop_event
306
+
307
+ try:
308
+ progress(0.05, desc="Preparing the coding prompt")
309
+ prompt, prior_context_trimmed = _make_prompt(
310
+ message,
311
+ chat_history,
312
+ language,
313
+ bool(use_recent_context),
314
+ )
315
+ tokenized = TOKENIZER(prompt, return_tensors="pt", add_special_tokens=True, truncation=False)
316
+ input_length = int(tokenized["input_ids"].shape[-1])
317
+ if input_length >= CONTEXT_TOKENS:
318
+ raise ValueError(
319
+ f"Prompt length is {input_length} tokens; context capacity is {CONTEXT_TOKENS}. "
320
+ "Shorten the current prompt and retry."
321
+ )
322
+
323
+ requested_new_tokens = int(max_new_tokens)
324
+ requested_new_tokens = max(1, min(requested_new_tokens, MAX_NEW_TOKENS_UI))
325
+ available_new_tokens = CONTEXT_TOKENS - input_length
326
+ actual_new_tokens = min(requested_new_tokens, available_new_tokens)
327
+ if actual_new_tokens < 1:
328
+ raise ValueError(
329
+ f"No room remains to generate tokens: prompt={input_length}, "
330
+ f"context={CONTEXT_TOKENS}. Shorten the prompt."
331
+ )
332
+
333
+ # Inputs are transferred to the device chosen at module initialization.
334
+ model_inputs = {key: value.to(MODEL_DEVICE) for key, value in tokenized.items()}
335
+ generation_kwargs: dict[str, Any] = {
336
+ "max_new_tokens": actual_new_tokens,
337
+ "repetition_penalty": float(repetition_penalty),
338
+ "do_sample": bool(sample),
339
+ "stopping_criteria": StoppingCriteriaList([StopOnEvent(stop_event)]),
340
+ "use_cache": True,
341
+ }
342
+ if sample:
343
+ generation_kwargs["temperature"] = float(temperature)
344
+ generation_kwargs["top_p"] = float(top_p)
345
+
346
+ eos_id = TOKENIZER.eos_token_id
347
+ if eos_id is None:
348
+ eos_id = getattr(MODEL.config, "eos_token_id", None)
349
+ pad_id = TOKENIZER.pad_token_id if TOKENIZER.pad_token_id is not None else eos_id
350
+ if eos_id is not None:
351
+ generation_kwargs["eos_token_id"] = eos_id
352
+ if pad_id is not None:
353
+ generation_kwargs["pad_token_id"] = pad_id
354
+
355
+ progress(0.25, desc="Requesting ZeroGPU and generating")
356
+ with torch.inference_mode():
357
+ generated = MODEL.generate(**model_inputs, **generation_kwargs)
358
+
359
+ # Slice by the tokenized input length, never by fragile string removal.
360
+ new_token_ids = generated[0, input_length:]
361
+ completion = TOKENIZER.decode(
362
+ new_token_ids,
363
+ skip_special_tokens=True,
364
+ clean_up_tokenization_spaces=False,
365
+ )
366
+ was_stopped = stop_event.is_set()
367
+ if not completion:
368
+ completion = (
369
+ "(Generation stopped before visible text was produced.)"
370
+ if was_stopped
371
+ else "(The model ended without producing visible text.)"
372
+ )
373
+
374
+ chat_history.append({"role": "user", "content": message})
375
+ chat_history.append({"role": "assistant", "content": completion})
376
+
377
+ notes = []
378
+ if prior_context_trimmed:
379
+ notes.append("older context was dropped to fit the model window")
380
+ if actual_new_tokens < requested_new_tokens:
381
+ notes.append(f"output capped at {actual_new_tokens} tokens by the context limit")
382
+ if was_stopped:
383
+ notes.append("stop signal received")
384
+ note_text = " · " + " · ".join(notes) if notes else ""
385
+ state_word = "STOPPED" if was_stopped else "COMPLETE"
386
+ progress(1.0, desc="Completion ready")
387
+ return (
388
+ chat_history,
389
+ "",
390
+ f"🟢 **{state_word}** · input {input_length}/{CONTEXT_TOKENS} tokens · "
391
+ f"new-token limit {actual_new_tokens} · runtime {RUNTIME_DTYPE}{note_text}",
392
+ )
393
+ except ValueError as exc:
394
+ LOGGER.info("Prompt rejected: %s", exc)
395
+ return _error_result(chat_history, message, str(exc))
396
+ except Exception:
397
+ LOGGER.exception("Generation failed")
398
+ return _error_result(
399
+ chat_history,
400
+ message,
401
+ "Generation failed unexpectedly. Please try again; if it repeats, check the Space logs.",
402
+ )
403
+ finally:
404
+ with _STOP_EVENTS_LOCK:
405
+ if _STOP_EVENTS.get(session_id) is stop_event:
406
+ _STOP_EVENTS.pop(session_id, None)
407
+
408
+
409
+ def stop_generation(request: gr.Request) -> str:
410
+ """Set this browser session's event; StoppingCriteria checks it per token."""
411
+ session_id = getattr(request, "session_hash", None) or "local-session"
412
+ with _STOP_EVENTS_LOCK:
413
+ event = _STOP_EVENTS.get(session_id)
414
+ if event is None:
415
+ return "🟡 **NO ACTIVE GENERATION** · nothing to stop right now."
416
+ event.set()
417
+ return "🟠 **STOP REQUESTED** · the current generation will stop at its next token check."
418
+
419
+
420
+ def clear_conversation():
421
+ return [], "", _startup_status()
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # Space Station UI
426
+ # ---------------------------------------------------------------------------
427
+
428
+ THEME = gr.themes.Base(
429
+ primary_hue=gr.themes.colors.cyan,
430
+ secondary_hue=gr.themes.colors.orange,
431
+ neutral_hue=gr.themes.colors.slate,
432
+ font=["Inter", "ui-sans-serif", "sans-serif"],
433
+ font_mono=["ui-monospace", "SFMono-Regular", "Menlo", "Consolas", "monospace"],
434
+ ).set(
435
+ body_background_fill="#070b12",
436
+ body_text_color="#e4edf6",
437
+ block_background_fill="#101923",
438
+ block_border_color="#243545",
439
+ block_label_text_color="#a9c2d2",
440
+ input_background_fill="#0b111a",
441
+ input_border_color="#294152",
442
+ button_primary_background_fill="#0c7d8d",
443
+ button_primary_background_fill_hover="#1195a6",
444
+ button_primary_text_color="#f4fdff",
445
+ button_secondary_background_fill="#192633",
446
+ button_secondary_text_color="#d4e3ed",
447
+ )
448
+
449
+ CSS = r"""
450
+ :root {
451
+ --space-bg: #070b12;
452
+ --space-panel: rgba(12, 20, 30, 0.94);
453
+ --space-line: #234151;
454
+ --space-cyan: #69e4f1;
455
+ --space-muted: #8ca5b6;
456
+ --space-amber: #f0b86a;
457
+ }
458
+ html, body, .gradio-container {
459
+ background-color: var(--space-bg) !important;
460
+ color: #e4edf6 !important;
461
+ }
462
+ body {
463
+ background-image:
464
+ radial-gradient(1px 1px at 12% 16%, rgba(141, 224, 239, .45) 50%, transparent 100%),
465
+ radial-gradient(1px 1px at 83% 22%, rgba(240, 184, 106, .45) 50%, transparent 100%),
466
+ radial-gradient(1px 1px at 72% 79%, rgba(141, 224, 239, .28) 50%, transparent 100%),
467
+ radial-gradient(1px 1px at 27% 68%, rgba(141, 224, 239, .25) 50%, transparent 100%),
468
+ radial-gradient(ellipse at 50% -12%, rgba(16, 77, 105, .26), transparent 54%);
469
+ }
470
+ .gradio-container { max-width: 1440px !important; }
471
+ #station { max-width: 1180px; margin: 0 auto; }
472
+ #station-header {
473
+ position: relative; overflow: hidden; border: 1px solid #2d5262;
474
+ border-radius: 18px; padding: 25px 28px 22px;
475
+ background: linear-gradient(115deg, rgba(14, 32, 45, .98), rgba(10, 17, 27, .97) 63%, rgba(31, 27, 24, .93));
476
+ box-shadow: 0 14px 50px rgba(0,0,0,.25), inset 0 1px rgba(255,255,255,.025);
477
+ margin-bottom: 12px;
478
+ }
479
+ #station-header:after {
480
+ content: ""; position: absolute; right: -38px; top: -76px; width: 270px; height: 270px;
481
+ border: 1px solid rgba(105, 228, 241, .2); border-radius: 50%;
482
+ box-shadow: 0 0 0 18px rgba(105,228,241,.025), 0 0 0 44px rgba(105,228,241,.018);
483
+ pointer-events: none;
484
+ }
485
+ #station-kicker { color: var(--space-amber); letter-spacing: .2em; font: 600 11px ui-monospace, monospace; text-transform: uppercase; }
486
+ #station-title { margin: 7px 0 4px; font-size: clamp(25px, 4vw, 39px); line-height: 1.08; font-weight: 700; letter-spacing: .035em; color: #f1f8fc; }
487
+ #station-title span { color: var(--space-cyan); }
488
+ #station-subtitle { color: #a9c3d0; font-size: 14px; margin-top: 8px; }
489
+ #telemetry { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; position: relative; z-index: 1; }
490
+ .telemetry-chip { border: 1px solid #284c5a; background: rgba(5,13,21,.58); color: #a7c6d2; border-radius: 5px; padding: 6px 9px; font: 11px ui-monospace, monospace; }
491
+ .telemetry-chip b { color: var(--space-cyan); font-weight: 500; }
492
+ #system-status { border: 1px solid #284858; background: rgba(9, 18, 28, .9); border-radius: 9px; padding: 10px 14px; font-size: 13px; margin: 10px 0 14px; }
493
+ #conversation { border: 1px solid #284252; border-radius: 14px; overflow: hidden; background: rgba(9, 15, 23, .96); }
494
+ #conversation .wrap { border: 0 !important; }
495
+ #conversation .message { font-size: 14px; line-height: 1.55; }
496
+ #conversation pre, #conversation code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; }
497
+ #conversation pre { border: 1px solid #243d4b !important; border-radius: 8px !important; background: #080e16 !important; }
498
+ #conversation .message.user { border-color: #25505d !important; }
499
+ #prompt-box textarea { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; line-height: 1.55 !important; }
500
+ #prompt-box textarea:focus { border-color: var(--space-cyan) !important; box-shadow: 0 0 0 1px rgba(105,228,241,.16) !important; }
501
+ #send-button { min-height: 47px; font-weight: 650; letter-spacing: .035em; }
502
+ #stop-button { min-height: 47px; border-color: #725232 !important; color: #f0bf7e !important; }
503
+ #clear-button { min-height: 47px; }
504
+ #lab-panel { border: 1px solid #263d4b; border-radius: 12px; background: rgba(10,17,26,.85); padding: 8px 12px; }
505
+ #model-card { border-top: 1px solid #263b49; margin-top: 14px; padding-top: 14px; color: var(--space-muted); font-size: 12px; line-height: 1.7; }
506
+ #model-card strong { color: #d1e7ef; }
507
+ #model-card a { color: var(--space-cyan); }
508
+ #station .gr-accordion { border-color: #284252 !important; background: rgba(12,20,30,.8) !important; }
509
+ #station footer { opacity: .65; }
510
+ @media (max-width: 700px) {
511
+ #station-header { padding: 20px 17px; }
512
+ #station-title { font-size: 27px; }
513
+ #station-header:after { right: -150px; opacity: .5; }
514
+ #conversation { min-height: 390px; }
515
+ }
516
+ """
517
+
518
+ parameter_display = f"{PARAMETER_COUNT / 1_000_000:.1f}M" if PARAMETER_COUNT else "~111M (model-page metadata)"
519
+ model_info_html = f"""
520
+ <div id="model-card">
521
+ <strong>MODEL BAY / REFERENCE CARD</strong><br>
522
+ Source checkpoint: <a href="https://huggingface.co/{MODEL_ID}" target="_blank" rel="noreferrer">{MODEL_ID}</a><br>
523
+ Architecture: GPT-2 causal language model · Parameter count: {parameter_display}<br>
524
+ Published artifact: Safetensors, listed as F16 on the model page · Runtime dtype: {RUNTIME_DTYPE}<br>
525
+ Context capacity: {CONTEXT_TOKENS:,} tokens from the loaded configuration · Device at startup: {MODEL_DEVICE}<br>
526
+ No chat template is used. Detected tokenizer chat-template metadata: {"present (not applied)" if HAS_CHAT_TEMPLATE else "not present"}.<br>
527
+ Code completion is the primary behavior; instruction-following and correctness are not guaranteed.
528
+ Model metadata identifies Apache-2.0 licensing and the CodeParrot code / HumanEval datasets.
529
+ </div>
530
+ """
531
+
532
+ with gr.Blocks(
533
+ title="TULLUS Space Station · CodeParrot Mini Lab",
534
+ analytics_enabled=False,
535
+ ) as demo:
536
+ with gr.Column(elem_id="station"):
537
+ gr.HTML(
538
+ """
539
+ <header id="station-header">
540
+ <div id="station-kicker">TULLUS / ORBITAL RESEARCH DIVISION · LAB 09</div>
541
+ <div id="station-title">TULLUS SPACE <span>STATION</span></div>
542
+ <div style="font:600 12px ui-monospace,monospace;letter-spacing:.17em;color:#c3dce5">CODEPARROT MINI LAB</div>
543
+ <div id="station-subtitle">Code generation from orbit. Laboratory systems online. 🛸</div>
544
+ <div id="telemetry">
545
+ <div class="telemetry-chip"><b>SYS</b> / CODE COMPLETION</div>
546
+ <div class="telemetry-chip"><b>CORE</b> / GPT-2 · 111M CLASS</div>
547
+ <div class="telemetry-chip"><b>LINK</b> / HUGGING FACE</div>
548
+ <div class="telemetry-chip"><b>AMBIENCE</b> / SPACE JAZZ ▪︎ LOW</div>
549
+ </div>
550
+ </header>
551
+ """
552
+ )
553
+ status = gr.Markdown(_startup_status(), elem_id="system-status")
554
+
555
+ gr.Markdown("### ◉ Mission console / coding channel")
556
+ chatbot = gr.Chatbot(
557
+ label="Conversation log",
558
+ height=450,
559
+ elem_id="conversation",
560
+ buttons=["copy", "copy_all"],
561
+ render_markdown=True,
562
+ placeholder=(
563
+ "<div style='text-align:center;padding:30px 10px;color:#8ca5b6'>"
564
+ "<div style='font:12px monospace;letter-spacing:.15em;color:#69e4f1'>CHANNEL OPEN</div>"
565
+ "<h3 style='color:#e4edf6'>Awaiting your first coding task</h3>"
566
+ "Drop in a function prefix, existing code, or a plain-language request."
567
+ "</div>"
568
+ ),
569
+ )
570
+
571
+ with gr.Row(equal_height=True):
572
+ language = gr.Dropdown(
573
+ choices=LANGUAGES,
574
+ value="Auto",
575
+ label="Language hint",
576
+ info="Auto passes your prompt unchanged. A language selection adds a visible plain-text comment prefix (not a special token).",
577
+ scale=1,
578
+ )
579
+ prompt_box = gr.Textbox(
580
+ label="Prompt / code prefix",
581
+ placeholder="Paste code, start a function, or describe the snippet you want to explore…",
582
+ lines=5,
583
+ max_lines=12,
584
+ elem_id="prompt-box",
585
+ autofocus=True,
586
+ )
587
+ with gr.Row():
588
+ send_button = gr.Button("↗ TRANSMIT PROMPT", variant="primary", elem_id="send-button", scale=3)
589
+ stop_button = gr.Button("■ STOP GENERATION", variant="secondary", elem_id="stop-button", scale=1)
590
+ clear_button = gr.Button("↺ NEW TASK", variant="secondary", elem_id="clear-button", scale=1)
591
+
592
+ with gr.Accordion("Generation controls / lab instruments", open=False, elem_id="lab-panel"):
593
+ with gr.Row():
594
+ max_new_tokens = gr.Slider(
595
+ minimum=16, maximum=MAX_NEW_TOKENS_UI, value=128, step=16,
596
+ label="Maximum new tokens", info="The context window may reduce this for long prompts.",
597
+ )
598
+ sample = gr.Checkbox(value=True, label="Use sampling", info="Turn off for greedy decoding.")
599
+ with gr.Row():
600
+ temperature = gr.Slider(minimum=0.1, maximum=1.5, value=0.2, step=0.05, label="Temperature", info="Lower is less varied.")
601
+ top_p = gr.Slider(minimum=0.1, maximum=1.0, value=0.95, step=0.05, label="Top-p", info="Sampling nucleus size.")
602
+ repetition_penalty = gr.Slider(minimum=1.0, maximum=1.5, value=1.1, step=0.05, label="Repetition penalty", info="Higher discourages repeated text.")
603
+ use_recent_context = gr.Checkbox(
604
+ value=False,
605
+ label="Include recent conversation context",
606
+ info="Off by default. When enabled, up to two completed exchanges may be included as commented reference context; older context is dropped first to fit the model window.",
607
+ )
608
+
609
+ gr.Examples(
610
+ examples=[
611
+ ["def fibonacci(n):", "Python"],
612
+ ["function reverseString(str) {", "JavaScript"],
613
+ ["public static int factorial(int n) {", "Java"],
614
+ ["int calculate_sum(", "C++"],
615
+ ["function formatUser(user: User) {", "TypeScript"],
616
+ ["Write a Python function that checks whether a string is a palindrome.", "Python"],
617
+ ],
618
+ inputs=[prompt_box, language],
619
+ label="Docking bay / starter prompts",
620
+ )
621
+
622
+ gr.HTML(model_info_html)
623
+ gr.Markdown(
624
+ "*Experimental compact code-generation model, not a frontier reasoning chatbot. "
625
+ "Review, test, and validate generated code before use.*"
626
+ )
627
+
628
+ generation_inputs = [
629
+ prompt_box,
630
+ chatbot,
631
+ language,
632
+ max_new_tokens,
633
+ temperature,
634
+ top_p,
635
+ repetition_penalty,
636
+ sample,
637
+ use_recent_context,
638
+ ]
639
+ generation_outputs = [chatbot, prompt_box, status]
640
+ send_event = prompt_box.submit(
641
+ fn=respond,
642
+ inputs=generation_inputs,
643
+ outputs=generation_outputs,
644
+ concurrency_id="model-generation",
645
+ concurrency_limit=1,
646
+ )
647
+ send_button.click(
648
+ fn=respond,
649
+ inputs=generation_inputs,
650
+ outputs=generation_outputs,
651
+ concurrency_id="model-generation",
652
+ concurrency_limit=1,
653
+ )
654
+ stop_button.click(fn=stop_generation, inputs=None, outputs=status, concurrency_limit=4)
655
+ clear_button.click(fn=clear_conversation, inputs=None, outputs=[chatbot, prompt_box, status])
656
+
657
+ # Keep a small amount of concurrency available so the Stop event can be handled
658
+ # while the model-generation event is running. GPU generation itself is limited
659
+ # to one concurrent request above.
660
+ demo.queue(default_concurrency_limit=4)
661
+
662
+ if __name__ == "__main__":
663
+ demo.launch(theme=THEME, css=CSS)