""" Gradio UI for the Qwen2.5 Text-to-SQL LoRA model.""" from __future__ import annotations import html import os import threading from pathlib import Path from typing import Any # ZeroGPU bootstrap IS_HF_SPACE = bool(os.getenv("SPACE_ID")) try: import spaces except ImportError: if IS_HF_SPACE: raise # Local-development fallback when the `spaces` package is not installed class _SpacesShim: @staticmethod def GPU(duration: int = 60, **_kwargs): def decorator(function): return function return decorator spaces = _SpacesShim() import gradio as gr import sqlglot import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer from config import MODEL_ID, OUTPUT_DIR, SYSTEM_PROMPT # Runtime configuration ADAPTER_ID = os.getenv("ADAPTER_ID", OUTPUT_DIR).strip() HF_TOKEN = os.getenv("HF_TOKEN") or None MODEL: Any | None = None TOKENIZER: Any | None = None MODEL_LOCK = threading.Lock() MODEL_MODE = "not loaded" MODEL_SOURCE = "" DIALECT_MAP = { "Auto / Generic SQL": None, "SQLite": "sqlite", "PostgreSQL": "postgres", "MySQL": "mysql", "Microsoft SQL Server": "tsql", } # Visual design CSS = r""" :root { --surface: rgba(24, 19, 8, .80); --surface-2: rgba(38, 29, 8, .72); --surface-3: rgba(255, 255, 255, .035); --line: rgba(250, 204, 21, .17); --line-strong: rgba(250, 204, 21, .34); --muted: #b9ad8c; --text: #fffaf0; --accent: #facc15; --accent-2: #f59e0b; --accent-3: #fde68a; --success: #86efac; --danger: #fca5a5; } html, body { background: #090704 !important; } .gradio-container { max-width: 1500px !important; margin: 0 auto !important; color: var(--text) !important; background: radial-gradient(circle at 7% 7%, rgba(250, 204, 21, .20), transparent 30%), radial-gradient(circle at 91% 12%, rgba(245, 158, 11, .16), transparent 28%), radial-gradient(circle at 52% 92%, rgba(234, 179, 8, .08), transparent 33%), linear-gradient(145deg, #070603 0%, #100c04 46%, #171006 100%) !important; min-height: 100vh; } .main-shell { padding: 28px 24px 44px; } .hero { position: relative; overflow: hidden; border: 1px solid var(--line); background: linear-gradient(135deg, rgba(38, 29, 8, .95), rgba(15, 12, 6, .89)); border-radius: 25px; padding: 31px 33px; box-shadow: 0 30px 85px rgba(0, 0, 0, .35); margin-bottom: 18px; } .hero::before { content: ""; position: absolute; width: 390px; height: 390px; left: -185px; bottom: -275px; background: radial-gradient(circle, rgba(250, 204, 21, .20), transparent 66%); } .hero::after { content: ""; position: absolute; width: 350px; height: 350px; right: -120px; top: -175px; background: radial-gradient(circle, rgba(245, 158, 11, .25), transparent 66%); } .eyebrow { color: #fde68a; font-size: 12px; font-weight: 900; letter-spacing: .17em; text-transform: uppercase; } .hero h1 { margin: 8px 0 7px; font-size: clamp(34px, 5vw, 59px); line-height: 1.01; letter-spacing: -.048em; color: #fffdf5; } .hero .gradient-word { background: linear-gradient(110deg, #fff7ae 0%, #facc15 42%, #f59e0b 100%); -webkit-background-clip: text; background-clip: text; color: transparent; } .hero p { position: relative; z-index: 1; max-width: 900px; color: #c8bda1; font-size: 16px; line-height: 1.65; margin: 0; } .badges { position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 9px; margin-top: 19px; } .badge { border: 1px solid var(--line); background: rgba(255, 255, 255, .035); padding: 7px 11px; border-radius: 999px; color: #d8ccb0; font-size: 12px; backdrop-filter: blur(8px); } .badge strong { color: #fff8da; margin-right: 4px; } .app-panel { background: var(--surface) !important; border: 1px solid var(--line) !important; border-radius: 21px !important; box-shadow: 0 20px 55px rgba(0, 0, 0, .25); overflow: hidden; } .input-card { padding: 4px 4px 0; } .sidebar-card { background: var(--surface-2); border: 1px solid var(--line); border-radius: 18px; padding: 18px; margin-bottom: 14px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .02); } .sidebar-card h3 { margin: 0 0 8px; color: #fff8dc; font-size: 14px; } .sidebar-card p, .sidebar-card li { color: var(--muted); font-size: 13px; line-height: 1.58; } .sidebar-card ol { margin: 9px 0 0; padding-left: 20px; } .model-source { color: #fde68a; overflow-wrap: anywhere; } #schema textarea, #question textarea { font-size: 14px !important; line-height: 1.55 !important; } #sql-output { min-height: 285px; } #sql-output .cm-editor, #sql-output textarea { font-size: 14px !important; } #generate-button { min-width: 155px; font-weight: 900; } button.primary, #generate-button { background: linear-gradient(135deg, #eab308, #f59e0b) !important; color: #1b1302 !important; border: 1px solid rgba(255, 235, 120, .28) !important; box-shadow: 0 8px 24px rgba(234, 179, 8, .15) !important; } button.primary:hover, #generate-button:hover { filter: brightness(1.07); } .status-card { border: 1px solid var(--line); background: rgba(255, 255, 255, .025); border-radius: 14px; padding: 12px 14px; color: #c9bda0; font-size: 12px; line-height: 1.55; } .status-card strong { color: #fff6cd; } .status-ok { color: var(--success); } .status-warn { color: #fde68a; } .status-error { color: var(--danger); } .accordion { background: rgba(255, 255, 255, .02) !important; border-color: var(--line) !important; } .footer-note { color: #8f8264; font-size: 11px; text-align: center; margin-top: 17px; } .footer-note code, .sidebar-card code { color: #fde68a; } @media (max-width: 800px) { .main-shell { padding: 14px 10px 28px; } .hero { padding: 23px 20px; border-radius: 18px; } .hero h1 { font-size: 37px; } #sql-output { min-height: 230px; } } """ HEAD = """ """ # Model loading and inference def _local_adapter_available(source: str) -> bool: path = Path(source) return path.is_dir() and (path / "adapter_config.json").exists() def _adapter_is_configured(source: str) -> bool: """Treat a local adapter path or non-default Hub model ID as configured.""" if _local_adapter_available(source): return True # OUTPUT_DIR is the default local path produced by train.py. If it does not # exist, do not ask the Hub for a repo literally named './qwen-text-to-sql-lora' return source not in {"", OUTPUT_DIR, f"./{Path(OUTPUT_DIR).name}"} def get_model(): """Load the model once and reuse it across generations.""" global MODEL, TOKENIZER, MODEL_MODE, MODEL_SOURCE if MODEL is not None and TOKENIZER is not None: return MODEL, TOKENIZER with MODEL_LOCK: if MODEL is not None and TOKENIZER is not None: return MODEL, TOKENIZER adapter_configured = _adapter_is_configured(ADAPTER_ID) # The LoRA adapter does not require a separate tokenizer vocabulary for # this project, so use the original Qwen tokenizer directly. This also # avoids depending on a duplicate large tokenizer.json inside the adapter print(f"[startup] Loading tokenizer: {MODEL_ID}", flush=True) TOKENIZER = AutoTokenizer.from_pretrained( MODEL_ID, token=HF_TOKEN, ) if TOKENIZER.pad_token is None: TOKENIZER.pad_token = TOKENIZER.eos_token # ZeroGPU supports CUDA placement at module startup through CUDA # emulation. FP16 is sufficient for inference and avoids probing CUDA # capabilities before the real ZeroGPU device is attached if IS_HF_SPACE: dtype = torch.float16 target_device = "cuda" elif torch.cuda.is_available(): dtype = ( torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16 ) target_device = "cuda" else: dtype = torch.float32 target_device = "cpu" print( f"[startup] Loading base model: {MODEL_ID} " f"(dtype={dtype}, target_device={target_device})", flush=True, ) base_model = AutoModelForCausalLM.from_pretrained( MODEL_ID, dtype=dtype, token=HF_TOKEN, low_cpu_mem_usage=True, ) if adapter_configured: print(f"[startup] Loading LoRA adapter: {ADAPTER_ID}", flush=True) MODEL = PeftModel.from_pretrained( base_model, ADAPTER_ID, token=HF_TOKEN, torch_device="cpu", ) MODEL_MODE = "LoRA adapter" MODEL_SOURCE = ADAPTER_ID else: print( "[startup] LoRA adapter was not found; using base-model fallback.", flush=True, ) MODEL = base_model MODEL_MODE = "Base model fallback" MODEL_SOURCE = MODEL_ID MODEL = MODEL.to(target_device) MODEL.eval() print( f"[startup] Model ready: mode={MODEL_MODE}, source={MODEL_SOURCE}, " f"device={next(MODEL.parameters()).device}", flush=True, ) return MODEL, TOKENIZER # ZeroGPU startup model placement if IS_HF_SPACE: get_model() def _build_system_prompt(dialect_label: str) -> str: prompt = SYSTEM_PROMPT if dialect_label != "Auto / Generic SQL": prompt += ( f"\n5. Generate SQL compatible with {dialect_label}." " Prefer syntax natural to that dialect when dialect-specific syntax is needed." ) return prompt def _clean_sql(text: str) -> str: """Remove common Markdown wrappers while preserving generated SQL.""" clean = (text or "").strip() if clean.startswith("```"): clean = clean.removeprefix("```sql").removeprefix("```SQL").removeprefix("```") clean = clean.removesuffix("```").strip() return clean def _validate_sql(sql: str, dialect_label: str) -> tuple[bool, str]: if not sql.strip(): return False, "No SQL was generated." dialect = DIALECT_MAP.get(dialect_label) try: sqlglot.parse_one(sql, read=dialect) return True, "Parsed successfully with SQLGlot." except Exception as exc: return False, str(exc).split("\n", 1)[0][:220] @spaces.GPU(duration=60) def generate_sql_ui( schema: str, question: str, dialect_label: str, temperature: float, max_new_tokens: int, ): """Generate SQL from a schema and natural-language request.""" clean_schema = (schema or "").strip() clean_question = (question or "").strip() if not clean_schema or not clean_question: missing = "database schema/context" if not clean_schema else "natural-language request" status = ( "
Generate executable SQL from a database schema and plain-English request using a Qwen2.5-Coder model fine-tuned for Text-to-SQL with Hugging Face PEFT LoRA.