Spaces:
Sleeping
Sleeping
| """ | |
| Nova-1 Chat β Powered by gradio.Server (Headless Gradio) | |
| Custom ChatGPT-style frontend with ZeroGPU streaming support. | |
| Requirements: | |
| pip install gradio>=6.0 transformers torch spaces | |
| Important for HF Spaces: | |
| - Set environment variable GRADIO_SSR_MODE=false | |
| - The file index.html must be in the same directory as app.py | |
| """ | |
| import os | |
| import traceback | |
| from threading import Thread | |
| import spaces | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer | |
| from gradio import Server | |
| from fastapi.responses import HTMLResponse | |
| # βββ Model Loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| model_name = "Smilyai-labs/Nova-1-Standard-Preview" | |
| print("π Loading tokenizer...") | |
| tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| print("β Tokenizer loaded!") | |
| print("π Loading model...") | |
| model = AutoModelForCausalLM.from_pretrained( | |
| model_name, | |
| trust_remote_code=True, | |
| dtype=torch.bfloat16, | |
| device_map="auto", | |
| low_cpu_mem_usage=True, | |
| ) | |
| model.eval() | |
| print("β Model loaded!") | |
| # βββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def format_chatml(messages, system_prompt: str) -> str: | |
| out = "" | |
| if system_prompt and system_prompt.strip(): | |
| out += f"<|im_start|>system\n{system_prompt}<|im_end|>\n" | |
| for m in messages: | |
| if m["role"] == "user": | |
| out += f"<|im_start|>user\n{m['content']}<|im_end|>\n" | |
| elif m["role"] == "assistant": | |
| out += f"<|im_start|>assistant\n{m['content']}<|im_end|>\n" | |
| out += "<|im_start|>assistant\n" | |
| return out | |
| def extract_text(content) -> str: | |
| if isinstance(content, str): | |
| return content | |
| if isinstance(content, list): | |
| parts = [] | |
| for item in content: | |
| if isinstance(item, str): | |
| parts.append(item) | |
| elif isinstance(item, dict) and "text" in item: | |
| parts.append(item["text"]) | |
| return " ".join(parts) | |
| return "" | |
| # βββ Server ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = Server(title="Nova-1 Chat") | |
| # βββ YaRN Context Stretch ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def yarn(ctx: str) -> str: | |
| """Apply YaRN RoPE scaling to extend the context window.""" | |
| native_len = 2048 | |
| if "2K" in ctx: | |
| target_len, factor = 2048, 1.0 | |
| elif "4K" in ctx: | |
| target_len, factor = 4096, 2.0 | |
| elif "8K" in ctx: | |
| target_len, factor = 8192, 4.0 | |
| elif "16K" in ctx: | |
| target_len, factor = 16384, 8.0 | |
| else: | |
| target_len, factor = 2048, 1.0 | |
| if factor == 1.0: | |
| model.config.max_len = native_len | |
| if hasattr(model.config, "rope_scaling"): | |
| model.config.rope_scaling = None | |
| if hasattr(model, "_rope_cache"): | |
| model._rope_cache = None | |
| return "β Native 2K context active." | |
| model.config.max_len = target_len | |
| model.config.rope_scaling = {"type": "yarn", "factor": factor} | |
| if hasattr(model, "_rope_cache"): | |
| model._rope_cache = None | |
| k = target_len // 1024 | |
| return f"β Stretched to {k}K (Factor {factor}.0)" | |
| # βββ GPU Generation (separate from @app.api β do NOT stack @spaces.GPU+@app.api) β | |
| def _generate_gpu(text, max_tokens, temperature, top_p, im_end_id, eos_id): | |
| """Runs on allocated ZeroGPU. Generator yields text chunks.""" | |
| inputs = tokenizer( | |
| text, | |
| return_tensors="pt", | |
| truncation=True, | |
| max_length=model.config.max_len, | |
| add_special_tokens=False, | |
| ) | |
| inputs = {k: v.to(model.device) for k, v in inputs.items()} | |
| streamer = TextIteratorStreamer( | |
| tokenizer, | |
| skip_prompt=True, | |
| skip_special_tokens=True, | |
| timeout=60.0, | |
| ) | |
| generation_kwargs = dict( | |
| **inputs, | |
| max_new_tokens=max_tokens, | |
| temperature=temperature, | |
| top_k=50, | |
| top_p=top_p, | |
| do_sample=True, | |
| pad_token_id=eos_id, | |
| eos_token_id=[im_end_id, eos_id], | |
| repetition_penalty=1.15, | |
| streamer=streamer, | |
| ) | |
| thread = Thread(target=model.generate, kwargs=generation_kwargs) | |
| thread.start() | |
| for chunk in streamer: | |
| yield chunk | |
| thread.join() | |
| # βββ Generate Endpoint (streaming via SSE) ββββββββββββββββββββββββββββββββββββ | |
| def generate( | |
| message: str, | |
| history: list, | |
| system_prompt: str, | |
| max_tokens: int, | |
| temperature: float, | |
| top_p: float, | |
| mode: str, | |
| ): | |
| """Streaming chat generation. Yields text chunks via SSE.""" | |
| try: | |
| if not message or not isinstance(message, str) or not message.strip(): | |
| yield "β οΈ Please enter a message." | |
| return | |
| if len(message) > 8000: | |
| yield "β οΈ Message too long (8000 char max)." | |
| return | |
| clean_history = [] | |
| for h in (history or []): | |
| if isinstance(h, dict): | |
| clean_history.append({ | |
| "role": h["role"], | |
| "content": extract_text(h.get("content", "")), | |
| }) | |
| max_hist_len = getattr(model.config, "max_len", 2048) | |
| if len(clean_history) > max_hist_len // 4: | |
| clean_history = clean_history[-(max_hist_len // 4):] | |
| if mode == "Chat": | |
| text = format_chatml(clean_history, system_prompt) | |
| else: | |
| text = "" | |
| if system_prompt and system_prompt.strip(): | |
| text += system_prompt.strip() + "\n\n" | |
| for m in clean_history: | |
| text += m["content"] + "\n" | |
| text += message.strip() | |
| temperature = max(0.01, min(2.0, float(temperature))) | |
| top_p = max(0.01, min(1.0, float(top_p))) | |
| max_tokens = max(1, min(2048, int(max_tokens))) | |
| im_end_id = tokenizer.convert_tokens_to_ids("<|im_end|>") | |
| eos_id = tokenizer.convert_tokens_to_ids("</s>") | |
| if im_end_id == tokenizer.unk_token_id: | |
| im_end_id = 50258 | |
| if eos_id == tokenizer.unk_token_id: | |
| eos_id = 50256 | |
| yield from _generate_gpu(text, max_tokens, temperature, top_p, im_end_id, eos_id) | |
| except torch.cuda.OutOfMemoryError: | |
| torch.cuda.empty_cache() | |
| yield "β οΈ GPU OOM β try shorter input or fewer tokens." | |
| except Exception as e: | |
| print(f"β Generation error: {e}\n{traceback.format_exc()}") | |
| yield f"β οΈ Error: {e}" | |
| # βββ Serve Custom Frontend ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def homepage(): | |
| html_path = os.path.join( | |
| os.path.dirname(os.path.abspath(__file__)), "index.html" | |
| ) | |
| with open(html_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| # βββ Launch βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| demo = app # HF Spaces runtime expects variable named `demo` | |
| if __name__ == "__main__": | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| show_error=True, | |
| ) |