Upload folder using huggingface_hub
Browse files- app.py +8 -6
- code/config/constants.py +30 -1
- code/model/inference.py +186 -9
- code/model/loader.py +190 -37
- code/server/routes.py +94 -9
- index.html +194 -2
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -1,20 +1,22 @@
|
|
| 1 |
"""Fullstack Code Builder β entry point.
|
| 2 |
|
| 3 |
-
Uses MiniCPM5-1B
|
|
|
|
| 4 |
Supports generating fullstack applications in any language.
|
| 5 |
Can push generated projects to HuggingFace Hub.
|
| 6 |
Web search via Google scraping (no API keys needed).
|
| 7 |
Gradio app support for Python.
|
|
|
|
| 8 |
|
| 9 |
Project structure:
|
| 10 |
code/
|
| 11 |
-
βββ config/constants.py App constants,
|
| 12 |
-
βββ model/loader.py
|
| 13 |
-
βββ model/inference.py Streaming
|
| 14 |
βββ execution/code_extractor.py Code extraction & language normalization
|
| 15 |
βββ execution/python_runner.py Sandboxed Python execution
|
| 16 |
βββ execution/gradio_runner.py Gradio app subprocess runner
|
| 17 |
-
βββ websearch/google_scraper.py
|
| 18 |
βββ huggingface/push.py HuggingFace Hub push & ZIP packaging
|
| 19 |
βββ server/chat_helpers.py Chat history & prompt building
|
| 20 |
βββ server/routes.py FastAPI / Gradio server routes
|
|
@@ -30,7 +32,7 @@ from code.server.routes import get_app
|
|
| 30 |
logging.basicConfig(level=logging.INFO)
|
| 31 |
logger = logging.getLogger(__name__)
|
| 32 |
|
| 33 |
-
# Start loading model in background
|
| 34 |
start_background_load()
|
| 35 |
|
| 36 |
# Launch the server
|
|
|
|
| 1 |
"""Fullstack Code Builder β entry point.
|
| 2 |
|
| 3 |
+
Uses MiniCPM5-1B (text) or MiniCPM-V-4.6 (vision+text) for local inference.
|
| 4 |
+
No external APIs required.
|
| 5 |
Supports generating fullstack applications in any language.
|
| 6 |
Can push generated projects to HuggingFace Hub.
|
| 7 |
Web search via Google scraping (no API keys needed).
|
| 8 |
Gradio app support for Python.
|
| 9 |
+
Image understanding with MiniCPM-V-4.6.
|
| 10 |
|
| 11 |
Project structure:
|
| 12 |
code/
|
| 13 |
+
βββ config/constants.py App constants, model configs, system prompt
|
| 14 |
+
βββ model/loader.py Dual model loading & switching
|
| 15 |
+
βββ model/inference.py Streaming inference (text + VLM)
|
| 16 |
βββ execution/code_extractor.py Code extraction & language normalization
|
| 17 |
βββ execution/python_runner.py Sandboxed Python execution
|
| 18 |
βββ execution/gradio_runner.py Gradio app subprocess runner
|
| 19 |
+
βββ websearch/google_scraper.py Web search scraping (no API)
|
| 20 |
βββ huggingface/push.py HuggingFace Hub push & ZIP packaging
|
| 21 |
βββ server/chat_helpers.py Chat history & prompt building
|
| 22 |
βββ server/routes.py FastAPI / Gradio server routes
|
|
|
|
| 32 |
logging.basicConfig(level=logging.INFO)
|
| 33 |
logger = logging.getLogger(__name__)
|
| 34 |
|
| 35 |
+
# Start loading default model in background
|
| 36 |
start_background_load()
|
| 37 |
|
| 38 |
# Launch the server
|
code/config/constants.py
CHANGED
|
@@ -7,9 +7,36 @@ import re
|
|
| 7 |
# βββ App Identity ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
|
| 9 |
APP_TITLE = "Fullstack Code Builder"
|
| 10 |
-
MODEL_ID = "openbmb/MiniCPM5-1B"
|
| 11 |
MODEL_URL = "https://huggingface.co/openbmb/MiniCPM5-1B"
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# βββ Runtime Defaults βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
|
| 15 |
DEFAULT_TEMPERATURE = 0.4
|
|
@@ -93,6 +120,8 @@ When generating Gradio apps, create a complete app.py with:
|
|
| 93 |
For Python, prefer standard library or common packages. Do not use network calls, subprocesses, shell commands, or long-running loops in demo code (except Gradio apps which are server-based).
|
| 94 |
|
| 95 |
If web search results are provided in the context, use them to inform your code generation. Incorporate relevant information from the search results into the generated code.
|
|
|
|
|
|
|
| 96 |
"""
|
| 97 |
|
| 98 |
# βββ Example Prompts ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 7 |
# βββ App Identity ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
|
| 9 |
APP_TITLE = "Fullstack Code Builder"
|
|
|
|
| 10 |
MODEL_URL = "https://huggingface.co/openbmb/MiniCPM5-1B"
|
| 11 |
|
| 12 |
+
# βββ Model Configs βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
+
|
| 14 |
+
MODEL_CONFIGS = {
|
| 15 |
+
"minicpm5-1b": {
|
| 16 |
+
"id": "openbmb/MiniCPM5-1B",
|
| 17 |
+
"name": "MiniCPM5-1B",
|
| 18 |
+
"type": "text",
|
| 19 |
+
"description": "Text-only, fast code generation",
|
| 20 |
+
"auto_class": "AutoModelForCausalLM",
|
| 21 |
+
"tokenizer_class": "AutoTokenizer",
|
| 22 |
+
"size_gb": 2.17,
|
| 23 |
+
},
|
| 24 |
+
"minicpm-v-4.6": {
|
| 25 |
+
"id": "openbmb/MiniCPM-V-4.6",
|
| 26 |
+
"name": "MiniCPM-V-4.6",
|
| 27 |
+
"type": "vlm",
|
| 28 |
+
"description": "Vision + Text, image understanding & code",
|
| 29 |
+
"auto_class": "AutoModelForImageTextToText",
|
| 30 |
+
"processor_class": "AutoProcessor",
|
| 31 |
+
"size_gb": 2.8,
|
| 32 |
+
},
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
DEFAULT_MODEL_KEY = "minicpm5-1b"
|
| 36 |
+
|
| 37 |
+
# Keep backward compat aliases
|
| 38 |
+
MODEL_ID = MODEL_CONFIGS[DEFAULT_MODEL_KEY]["id"]
|
| 39 |
+
|
| 40 |
# βββ Runtime Defaults βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
|
| 42 |
DEFAULT_TEMPERATURE = 0.4
|
|
|
|
| 120 |
For Python, prefer standard library or common packages. Do not use network calls, subprocesses, shell commands, or long-running loops in demo code (except Gradio apps which are server-based).
|
| 121 |
|
| 122 |
If web search results are provided in the context, use them to inform your code generation. Incorporate relevant information from the search results into the generated code.
|
| 123 |
+
|
| 124 |
+
If the user provides an image, analyze it and generate code based on what you see in the image. For example: replicate a UI from a screenshot, generate code from a wireframe, or build an app described in a document.
|
| 125 |
"""
|
| 126 |
|
| 127 |
# βββ Example Prompts ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
code/model/inference.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
"""Model inference β streaming and synchronous generation.
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
from __future__ import annotations
|
|
@@ -10,8 +12,15 @@ import threading
|
|
| 10 |
from collections.abc import Iterator
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
-
from code.config.constants import DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS
|
| 14 |
-
from code.model.loader import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
logger = logging.getLogger(__name__)
|
| 17 |
|
|
@@ -19,19 +28,32 @@ logger = logging.getLogger(__name__)
|
|
| 19 |
def call_model(
|
| 20 |
messages: list[dict[str, Any]],
|
| 21 |
max_new_tokens: int = DEFAULT_MAX_TOKENS,
|
|
|
|
| 22 |
) -> Iterator[str]:
|
| 23 |
-
"""Stream model text
|
| 24 |
|
| 25 |
-
|
| 26 |
"""
|
| 27 |
-
|
| 28 |
if not is_model_loaded():
|
| 29 |
status = get_model_status()
|
| 30 |
yield status["message"]
|
| 31 |
return
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
model = get_model()
|
| 34 |
-
tokenizer =
|
| 35 |
|
| 36 |
try:
|
| 37 |
from transformers import TextIteratorStreamer
|
|
@@ -82,16 +104,171 @@ def call_model(
|
|
| 82 |
thread.join()
|
| 83 |
|
| 84 |
except Exception as exc:
|
| 85 |
-
logger.exception("Error during model inference")
|
| 86 |
yield f"_Error during generation: {exc}_"
|
| 87 |
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
def call_model_sync(
|
| 90 |
messages: list[dict[str, Any]],
|
| 91 |
max_new_tokens: int = DEFAULT_MAX_TOKENS,
|
|
|
|
| 92 |
) -> str:
|
| 93 |
"""Non-streaming model call β returns complete response."""
|
| 94 |
result = ""
|
| 95 |
-
for chunk in call_model(messages, max_new_tokens):
|
| 96 |
result = chunk
|
| 97 |
return result
|
|
|
|
| 1 |
"""Model inference β streaming and synchronous generation.
|
| 2 |
|
| 3 |
+
Supports two inference paths:
|
| 4 |
+
- Text-only (MiniCPM5-1B): uses TextIteratorStreamer for real-time streaming
|
| 5 |
+
- VLM (MiniCPM-V-4.6): uses processor.apply_chat_template() with image support
|
| 6 |
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
|
|
|
| 12 |
from collections.abc import Iterator
|
| 13 |
from typing import Any
|
| 14 |
|
| 15 |
+
from code.config.constants import DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS, MODEL_CONFIGS
|
| 16 |
+
from code.model.loader import (
|
| 17 |
+
get_model,
|
| 18 |
+
get_tokenizer_or_processor,
|
| 19 |
+
get_model_status,
|
| 20 |
+
is_model_loaded,
|
| 21 |
+
get_current_model_key,
|
| 22 |
+
get_current_model_type,
|
| 23 |
+
)
|
| 24 |
|
| 25 |
logger = logging.getLogger(__name__)
|
| 26 |
|
|
|
|
| 28 |
def call_model(
|
| 29 |
messages: list[dict[str, Any]],
|
| 30 |
max_new_tokens: int = DEFAULT_MAX_TOKENS,
|
| 31 |
+
image_url: str | None = None,
|
| 32 |
) -> Iterator[str]:
|
| 33 |
+
"""Stream model text. Yields progressively longer strings (full text so far).
|
| 34 |
|
| 35 |
+
For VLM models, if image_url is provided, it's included in the last user message.
|
| 36 |
"""
|
|
|
|
| 37 |
if not is_model_loaded():
|
| 38 |
status = get_model_status()
|
| 39 |
yield status["message"]
|
| 40 |
return
|
| 41 |
|
| 42 |
+
model_type = get_current_model_type()
|
| 43 |
+
|
| 44 |
+
if model_type == "vlm":
|
| 45 |
+
yield from _call_vlm_model(messages, max_new_tokens, image_url)
|
| 46 |
+
else:
|
| 47 |
+
yield from _call_text_model(messages, max_new_tokens)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _call_text_model(
|
| 51 |
+
messages: list[dict[str, Any]],
|
| 52 |
+
max_new_tokens: int,
|
| 53 |
+
) -> Iterator[str]:
|
| 54 |
+
"""Stream text from a text-only model using TextIteratorStreamer."""
|
| 55 |
model = get_model()
|
| 56 |
+
tokenizer = get_tokenizer_or_processor()
|
| 57 |
|
| 58 |
try:
|
| 59 |
from transformers import TextIteratorStreamer
|
|
|
|
| 104 |
thread.join()
|
| 105 |
|
| 106 |
except Exception as exc:
|
| 107 |
+
logger.exception("Error during text model inference")
|
| 108 |
yield f"_Error during generation: {exc}_"
|
| 109 |
|
| 110 |
|
| 111 |
+
def _call_vlm_model(
|
| 112 |
+
messages: list[dict[str, Any]],
|
| 113 |
+
max_new_tokens: int,
|
| 114 |
+
image_url: str | None = None,
|
| 115 |
+
) -> Iterator[str]:
|
| 116 |
+
"""Stream text from a VLM model with optional image input.
|
| 117 |
+
|
| 118 |
+
Uses processor.apply_chat_template() for proper image+text processing,
|
| 119 |
+
then generates with streaming via a thread.
|
| 120 |
+
"""
|
| 121 |
+
model = get_model()
|
| 122 |
+
processor = get_tokenizer_or_processor()
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
import torch
|
| 126 |
+
|
| 127 |
+
# Build VLM-style messages with image support
|
| 128 |
+
vlm_messages = _build_vlm_messages(messages, image_url)
|
| 129 |
+
|
| 130 |
+
# Apply chat template
|
| 131 |
+
try:
|
| 132 |
+
inputs = processor.apply_chat_template(
|
| 133 |
+
vlm_messages,
|
| 134 |
+
tokenize=True,
|
| 135 |
+
add_generation_prompt=True,
|
| 136 |
+
return_dict=True,
|
| 137 |
+
return_tensors="pt",
|
| 138 |
+
downsample_mode="16x",
|
| 139 |
+
max_slice_nums=9,
|
| 140 |
+
)
|
| 141 |
+
except TypeError:
|
| 142 |
+
# Fallback for older transformers without downsample_mode
|
| 143 |
+
inputs = processor.apply_chat_template(
|
| 144 |
+
vlm_messages,
|
| 145 |
+
tokenize=True,
|
| 146 |
+
add_generation_prompt=True,
|
| 147 |
+
return_dict=True,
|
| 148 |
+
return_tensors="pt",
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
if torch.cuda.is_available():
|
| 152 |
+
inputs = inputs.to("cuda")
|
| 153 |
+
else:
|
| 154 |
+
# Move to CPU explicitly
|
| 155 |
+
inputs = inputs.to("cpu")
|
| 156 |
+
|
| 157 |
+
# Generate with streaming
|
| 158 |
+
try:
|
| 159 |
+
from transformers import TextIteratorStreamer
|
| 160 |
+
streamer = TextIteratorStreamer(
|
| 161 |
+
processor.tokenizer if hasattr(processor, 'tokenizer') else processor,
|
| 162 |
+
skip_prompt=True,
|
| 163 |
+
skip_special_tokens=True,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
gen_kwargs = {
|
| 167 |
+
**inputs,
|
| 168 |
+
"streamer": streamer,
|
| 169 |
+
"max_new_tokens": max_new_tokens,
|
| 170 |
+
"temperature": DEFAULT_TEMPERATURE,
|
| 171 |
+
"do_sample": True,
|
| 172 |
+
"top_p": 0.9,
|
| 173 |
+
"repetition_penalty": 1.1,
|
| 174 |
+
}
|
| 175 |
+
# Add downsample_mode if supported
|
| 176 |
+
try:
|
| 177 |
+
gen_kwargs["downsample_mode"] = "16x"
|
| 178 |
+
except Exception:
|
| 179 |
+
pass
|
| 180 |
+
|
| 181 |
+
# Ensure pad_token_id
|
| 182 |
+
if hasattr(processor, 'tokenizer') and hasattr(processor.tokenizer, 'eos_token_id'):
|
| 183 |
+
gen_kwargs["pad_token_id"] = processor.tokenizer.eos_token_id
|
| 184 |
+
elif hasattr(processor, 'eos_token_id'):
|
| 185 |
+
gen_kwargs["pad_token_id"] = processor.eos_token_id
|
| 186 |
+
|
| 187 |
+
thread = threading.Thread(target=model.generate, kwargs=gen_kwargs)
|
| 188 |
+
thread.start()
|
| 189 |
+
|
| 190 |
+
output = ""
|
| 191 |
+
for new_text in streamer:
|
| 192 |
+
output += new_text
|
| 193 |
+
yield output
|
| 194 |
+
|
| 195 |
+
thread.join()
|
| 196 |
+
|
| 197 |
+
except Exception as stream_err:
|
| 198 |
+
# Fallback: non-streaming generation
|
| 199 |
+
logger.warning("Streaming failed for VLM, falling back to sync: %s", stream_err)
|
| 200 |
+
gen_kwargs = {
|
| 201 |
+
**inputs,
|
| 202 |
+
"max_new_tokens": max_new_tokens,
|
| 203 |
+
"temperature": DEFAULT_TEMPERATURE,
|
| 204 |
+
"do_sample": True,
|
| 205 |
+
"top_p": 0.9,
|
| 206 |
+
}
|
| 207 |
+
try:
|
| 208 |
+
gen_kwargs["downsample_mode"] = "16x"
|
| 209 |
+
except Exception:
|
| 210 |
+
pass
|
| 211 |
+
|
| 212 |
+
generated_ids = model.generate(**gen_kwargs)
|
| 213 |
+
# Trim input tokens from output
|
| 214 |
+
input_len = inputs["input_ids"].shape[1] if hasattr(inputs, "shape") else len(inputs["input_ids"])
|
| 215 |
+
generated_ids_trimmed = [
|
| 216 |
+
out_ids[len(in_ids):]
|
| 217 |
+
for in_ids, out_ids in zip(inputs["input_ids"], generated_ids)
|
| 218 |
+
]
|
| 219 |
+
tok = processor.tokenizer if hasattr(processor, 'tokenizer') else processor
|
| 220 |
+
output_text = tok.batch_decode(
|
| 221 |
+
generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
|
| 222 |
+
)
|
| 223 |
+
yield output_text[0] if output_text else ""
|
| 224 |
+
|
| 225 |
+
except Exception as exc:
|
| 226 |
+
logger.exception("Error during VLM model inference")
|
| 227 |
+
yield f"_Error during generation: {exc}_"
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def _build_vlm_messages(
|
| 231 |
+
messages: list[dict[str, Any]],
|
| 232 |
+
image_url: str | None = None,
|
| 233 |
+
) -> list[dict[str, Any]]:
|
| 234 |
+
"""Build VLM-style messages with image content blocks.
|
| 235 |
+
|
| 236 |
+
If an image_url is provided, it's injected into the last user message
|
| 237 |
+
as a content block with type "image".
|
| 238 |
+
"""
|
| 239 |
+
vlm_messages = []
|
| 240 |
+
|
| 241 |
+
for i, msg in enumerate(messages):
|
| 242 |
+
role = msg.get("role", "user")
|
| 243 |
+
content = msg.get("content", "")
|
| 244 |
+
|
| 245 |
+
if role == "system":
|
| 246 |
+
# System messages stay as-is for VLM
|
| 247 |
+
vlm_messages.append({"role": "system", "content": content})
|
| 248 |
+
continue
|
| 249 |
+
|
| 250 |
+
# For the last user message with an image, use structured content
|
| 251 |
+
is_last_user = (i == len(messages) - 1) and role == "user"
|
| 252 |
+
|
| 253 |
+
if is_last_user and image_url:
|
| 254 |
+
# Build content list with image + text
|
| 255 |
+
content_list = [{"type": "image", "url": image_url}]
|
| 256 |
+
if content.strip():
|
| 257 |
+
content_list.append({"type": "text", "text": content})
|
| 258 |
+
vlm_messages.append({"role": "user", "content": content_list})
|
| 259 |
+
else:
|
| 260 |
+
vlm_messages.append({"role": role, "content": content})
|
| 261 |
+
|
| 262 |
+
return vlm_messages
|
| 263 |
+
|
| 264 |
+
|
| 265 |
def call_model_sync(
|
| 266 |
messages: list[dict[str, Any]],
|
| 267 |
max_new_tokens: int = DEFAULT_MAX_TOKENS,
|
| 268 |
+
image_url: str | None = None,
|
| 269 |
) -> str:
|
| 270 |
"""Non-streaming model call β returns complete response."""
|
| 271 |
result = ""
|
| 272 |
+
for chunk in call_model(messages, max_new_tokens, image_url):
|
| 273 |
result = chunk
|
| 274 |
return result
|
code/model/loader.py
CHANGED
|
@@ -1,89 +1,232 @@
|
|
| 1 |
"""Model loading and status management.
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
The model is loaded in a background thread on startup.
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
|
|
|
| 9 |
import logging
|
| 10 |
import threading
|
| 11 |
from typing import Any
|
| 12 |
|
| 13 |
-
from code.config.constants import
|
| 14 |
|
| 15 |
logger = logging.getLogger(__name__)
|
| 16 |
|
| 17 |
# βββ Module-level state βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
|
|
|
|
| 19 |
_model = None
|
| 20 |
-
|
| 21 |
_model_loaded = False
|
| 22 |
_model_loading = False
|
| 23 |
_load_error: str | None = None
|
| 24 |
|
| 25 |
|
| 26 |
-
def
|
| 27 |
-
"""
|
| 28 |
-
global _model,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
return
|
| 32 |
|
| 33 |
_model_loading = True
|
| 34 |
_load_error = None
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
try:
|
| 37 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 38 |
import torch
|
| 39 |
|
| 40 |
-
logger.info("Loading MiniCPM5-1B model...")
|
| 41 |
-
|
| 42 |
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
|
| 43 |
device_map = "auto" if torch.cuda.is_available() else None
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
MODEL_ID,
|
| 51 |
-
torch_dtype=dtype,
|
| 52 |
-
device_map=device_map,
|
| 53 |
-
trust_remote_code=True,
|
| 54 |
-
low_cpu_mem_usage=True,
|
| 55 |
-
)
|
| 56 |
-
|
| 57 |
-
if device_map is None:
|
| 58 |
-
_model = _model.to("cpu")
|
| 59 |
-
|
| 60 |
-
_model.eval()
|
| 61 |
_model_loaded = True
|
| 62 |
-
logger.info("
|
| 63 |
|
| 64 |
except Exception as exc:
|
| 65 |
_load_error = str(exc)
|
| 66 |
-
logger.exception("Failed to load model: %s", exc)
|
| 67 |
finally:
|
| 68 |
_model_loading = False
|
| 69 |
|
| 70 |
|
| 71 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
"""Start loading the model in a background daemon thread."""
|
| 73 |
-
thread = threading.Thread(target=load_model, daemon=True)
|
| 74 |
thread.start()
|
| 75 |
return thread
|
| 76 |
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
def get_model_status() -> dict[str, Any]:
|
| 79 |
"""Return current model loading status."""
|
|
|
|
| 80 |
if _model_loaded:
|
| 81 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
if _model_loading:
|
| 83 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
if _load_error:
|
| 85 |
-
return {
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
def get_model():
|
|
@@ -91,11 +234,21 @@ def get_model():
|
|
| 91 |
return _model
|
| 92 |
|
| 93 |
|
| 94 |
-
def
|
| 95 |
-
"""Return the loaded tokenizer
|
| 96 |
-
return
|
| 97 |
|
| 98 |
|
| 99 |
def is_model_loaded() -> bool:
|
| 100 |
"""Return True if the model has been loaded successfully."""
|
| 101 |
return _model_loaded
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Model loading and status management.
|
| 2 |
|
| 3 |
+
Supports two models:
|
| 4 |
+
- MiniCPM5-1B (text-only, fast)
|
| 5 |
+
- MiniCPM-V-4.6 (vision + text, image understanding)
|
| 6 |
+
|
| 7 |
+
Only one model is loaded at a time to conserve memory.
|
| 8 |
The model is loaded in a background thread on startup.
|
| 9 |
"""
|
| 10 |
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
+
import gc
|
| 14 |
import logging
|
| 15 |
import threading
|
| 16 |
from typing import Any
|
| 17 |
|
| 18 |
+
from code.config.constants import DEFAULT_MODEL_KEY, MODEL_CONFIGS
|
| 19 |
|
| 20 |
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
# βββ Module-level state βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
|
| 24 |
+
_current_model_key: str = DEFAULT_MODEL_KEY
|
| 25 |
_model = None
|
| 26 |
+
_tokenizer_or_processor = None
|
| 27 |
_model_loaded = False
|
| 28 |
_model_loading = False
|
| 29 |
_load_error: str | None = None
|
| 30 |
|
| 31 |
|
| 32 |
+
def _unload_model() -> None:
|
| 33 |
+
"""Unload current model and free memory."""
|
| 34 |
+
global _model, _tokenizer_or_processor, _model_loaded
|
| 35 |
+
|
| 36 |
+
if _model is not None:
|
| 37 |
+
del _model
|
| 38 |
+
_model = None
|
| 39 |
+
if _tokenizer_or_processor is not None:
|
| 40 |
+
del _tokenizer_or_processor
|
| 41 |
+
_tokenizer_or_processor = None
|
| 42 |
+
|
| 43 |
+
_model_loaded = False
|
| 44 |
+
gc.collect()
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
import torch
|
| 48 |
+
if torch.cuda.is_available():
|
| 49 |
+
torch.cuda.empty_cache()
|
| 50 |
+
except ImportError:
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def load_model(model_key: str | None = None) -> None:
|
| 55 |
+
"""Load a model by key. Unloads the previous model first."""
|
| 56 |
+
global _model, _tokenizer_or_processor, _model_loaded, _model_loading
|
| 57 |
+
global _load_error, _current_model_key
|
| 58 |
|
| 59 |
+
if model_key is None:
|
| 60 |
+
model_key = _current_model_key
|
| 61 |
+
|
| 62 |
+
if model_key not in MODEL_CONFIGS:
|
| 63 |
+
_load_error = f"Unknown model: {model_key}"
|
| 64 |
+
logger.error(_load_error)
|
| 65 |
+
return
|
| 66 |
+
|
| 67 |
+
# Skip if already loading or already loaded with same key
|
| 68 |
+
if _model_loading:
|
| 69 |
+
return
|
| 70 |
+
if _model_loaded and _current_model_key == model_key:
|
| 71 |
return
|
| 72 |
|
| 73 |
_model_loading = True
|
| 74 |
_load_error = None
|
| 75 |
|
| 76 |
+
# Unload previous model if switching
|
| 77 |
+
if _model_loaded and _current_model_key != model_key:
|
| 78 |
+
logger.info("Switching model from %s to %s", _current_model_key, model_key)
|
| 79 |
+
_unload_model()
|
| 80 |
+
|
| 81 |
+
_current_model_key = model_key
|
| 82 |
+
config = MODEL_CONFIGS[model_key]
|
| 83 |
+
model_id = config["id"]
|
| 84 |
+
|
| 85 |
try:
|
|
|
|
| 86 |
import torch
|
| 87 |
|
|
|
|
|
|
|
| 88 |
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
|
| 89 |
device_map = "auto" if torch.cuda.is_available() else None
|
| 90 |
|
| 91 |
+
if config["type"] == "vlm":
|
| 92 |
+
_load_vlm_model(model_id, dtype, device_map)
|
| 93 |
+
else:
|
| 94 |
+
_load_text_model(model_id, dtype, device_map)
|
| 95 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
_model_loaded = True
|
| 97 |
+
logger.info("%s model loaded successfully.", config["name"])
|
| 98 |
|
| 99 |
except Exception as exc:
|
| 100 |
_load_error = str(exc)
|
| 101 |
+
logger.exception("Failed to load model %s: %s", model_id, exc)
|
| 102 |
finally:
|
| 103 |
_model_loading = False
|
| 104 |
|
| 105 |
|
| 106 |
+
def _load_text_model(model_id: str, dtype, device_map) -> None:
|
| 107 |
+
"""Load a text-only model (AutoModelForCausalLM + AutoTokenizer)."""
|
| 108 |
+
global _model, _tokenizer_or_processor
|
| 109 |
+
|
| 110 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 111 |
+
|
| 112 |
+
logger.info("Loading %s (text model)...", model_id)
|
| 113 |
+
|
| 114 |
+
_tokenizer_or_processor = AutoTokenizer.from_pretrained(
|
| 115 |
+
model_id,
|
| 116 |
+
trust_remote_code=True,
|
| 117 |
+
)
|
| 118 |
+
_model = AutoModelForCausalLM.from_pretrained(
|
| 119 |
+
model_id,
|
| 120 |
+
torch_dtype=dtype,
|
| 121 |
+
device_map=device_map,
|
| 122 |
+
trust_remote_code=True,
|
| 123 |
+
low_cpu_mem_usage=True,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
if device_map is None:
|
| 127 |
+
_model = _model.to("cpu")
|
| 128 |
+
|
| 129 |
+
_model.eval()
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _load_vlm_model(model_id: str, dtype, device_map) -> None:
|
| 133 |
+
"""Load a vision-language model (AutoModelForImageTextToText + AutoProcessor)."""
|
| 134 |
+
global _model, _tokenizer_or_processor
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
from transformers import AutoModelForImageTextToText, AutoProcessor
|
| 138 |
+
except ImportError:
|
| 139 |
+
# Fallback for older transformers
|
| 140 |
+
logger.warning("AutoModelForImageTextToText not found, trying AutoModel...")
|
| 141 |
+
from transformers import AutoModel as AutoModelForImageTextToText
|
| 142 |
+
from transformers import AutoProcessor
|
| 143 |
+
|
| 144 |
+
logger.info("Loading %s (VLM)...", model_id)
|
| 145 |
+
|
| 146 |
+
_tokenizer_or_processor = AutoProcessor.from_pretrained(
|
| 147 |
+
model_id,
|
| 148 |
+
trust_remote_code=True,
|
| 149 |
+
)
|
| 150 |
+
_model = AutoModelForImageTextToText.from_pretrained(
|
| 151 |
+
model_id,
|
| 152 |
+
torch_dtype=dtype,
|
| 153 |
+
device_map=device_map,
|
| 154 |
+
trust_remote_code=True,
|
| 155 |
+
low_cpu_mem_usage=True,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
if device_map is None:
|
| 159 |
+
_model = _model.to("cpu")
|
| 160 |
+
|
| 161 |
+
_model.eval()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def start_background_load(model_key: str | None = None) -> threading.Thread:
|
| 165 |
"""Start loading the model in a background daemon thread."""
|
| 166 |
+
thread = threading.Thread(target=load_model, args=(model_key,), daemon=True)
|
| 167 |
thread.start()
|
| 168 |
return thread
|
| 169 |
|
| 170 |
|
| 171 |
+
def switch_model(model_key: str) -> dict[str, Any]:
|
| 172 |
+
"""Switch to a different model. Returns status immediately, loads in background."""
|
| 173 |
+
global _current_model_key
|
| 174 |
+
|
| 175 |
+
if model_key not in MODEL_CONFIGS:
|
| 176 |
+
return {"success": False, "message": f"Unknown model: {model_key}"}
|
| 177 |
+
|
| 178 |
+
if _current_model_key == model_key and _model_loaded:
|
| 179 |
+
return {"success": True, "message": f"Already using {MODEL_CONFIGS[model_key]['name']}"}
|
| 180 |
+
|
| 181 |
+
_current_model_key = model_key
|
| 182 |
+
_model_loaded = False
|
| 183 |
+
|
| 184 |
+
# Start loading in background
|
| 185 |
+
start_background_load(model_key)
|
| 186 |
+
|
| 187 |
+
config = MODEL_CONFIGS[model_key]
|
| 188 |
+
return {
|
| 189 |
+
"success": True,
|
| 190 |
+
"message": f"Switching to {config['name']}...",
|
| 191 |
+
"model_key": model_key,
|
| 192 |
+
"model_name": config["name"],
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
|
| 196 |
def get_model_status() -> dict[str, Any]:
|
| 197 |
"""Return current model loading status."""
|
| 198 |
+
config = MODEL_CONFIGS.get(_current_model_key, {})
|
| 199 |
if _model_loaded:
|
| 200 |
+
return {
|
| 201 |
+
"status": "ready",
|
| 202 |
+
"message": f"{config.get('name', 'Model')} loaded and ready",
|
| 203 |
+
"model_key": _current_model_key,
|
| 204 |
+
"model_name": config.get("name", ""),
|
| 205 |
+
"model_type": config.get("type", "text"),
|
| 206 |
+
}
|
| 207 |
if _model_loading:
|
| 208 |
+
return {
|
| 209 |
+
"status": "loading",
|
| 210 |
+
"message": f"Loading {config.get('name', 'model')}... (this may take a few minutes)",
|
| 211 |
+
"model_key": _current_model_key,
|
| 212 |
+
"model_name": config.get("name", ""),
|
| 213 |
+
"model_type": config.get("type", "text"),
|
| 214 |
+
}
|
| 215 |
if _load_error:
|
| 216 |
+
return {
|
| 217 |
+
"status": "error",
|
| 218 |
+
"message": f"Model load error: {_load_error}",
|
| 219 |
+
"model_key": _current_model_key,
|
| 220 |
+
"model_name": config.get("name", ""),
|
| 221 |
+
"model_type": config.get("type", "text"),
|
| 222 |
+
}
|
| 223 |
+
return {
|
| 224 |
+
"status": "unknown",
|
| 225 |
+
"message": "Model not initialized",
|
| 226 |
+
"model_key": _current_model_key,
|
| 227 |
+
"model_name": config.get("name", ""),
|
| 228 |
+
"model_type": config.get("type", "text"),
|
| 229 |
+
}
|
| 230 |
|
| 231 |
|
| 232 |
def get_model():
|
|
|
|
| 234 |
return _model
|
| 235 |
|
| 236 |
|
| 237 |
+
def get_tokenizer_or_processor():
|
| 238 |
+
"""Return the loaded tokenizer or processor (or None)."""
|
| 239 |
+
return _tokenizer_or_processor
|
| 240 |
|
| 241 |
|
| 242 |
def is_model_loaded() -> bool:
|
| 243 |
"""Return True if the model has been loaded successfully."""
|
| 244 |
return _model_loaded
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def get_current_model_key() -> str:
|
| 248 |
+
"""Return the key of the currently selected model."""
|
| 249 |
+
return _current_model_key
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def get_current_model_type() -> str:
|
| 253 |
+
"""Return 'text' or 'vlm' for the current model."""
|
| 254 |
+
return MODEL_CONFIGS.get(_current_model_key, {}).get("type", "text")
|
code/server/routes.py
CHANGED
|
@@ -8,10 +8,13 @@ Defines all HTTP and API endpoints:
|
|
| 8 |
- API web_search β Google search scraping
|
| 9 |
- API chat β streaming chat with code execution
|
| 10 |
- API push_hf β push to HuggingFace Hub
|
|
|
|
|
|
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
| 14 |
|
|
|
|
| 15 |
import json
|
| 16 |
import logging
|
| 17 |
import os
|
|
@@ -26,7 +29,7 @@ from code.config.constants import (
|
|
| 26 |
APP_TITLE,
|
| 27 |
EXAMPLE_PROMPTS,
|
| 28 |
LANGUAGE_OPTIONS,
|
| 29 |
-
|
| 30 |
MODEL_URL,
|
| 31 |
PY_TIMEOUT_S,
|
| 32 |
)
|
|
@@ -41,7 +44,13 @@ from code.execution.code_extractor import (
|
|
| 41 |
from code.execution.gradio_runner import run_gradio_app, stop_gradio_app
|
| 42 |
from code.execution.python_runner import run_python
|
| 43 |
from code.huggingface.push import create_project_zip, push_to_huggingface
|
| 44 |
-
from code.model.loader import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
from code.model.inference import call_model
|
| 46 |
from code.server.chat_helpers import chat_history_to_messages, targeted_prompt
|
| 47 |
from code.websearch.google_scraper import web_search_google, format_search_results
|
|
@@ -52,6 +61,10 @@ logger = logging.getLogger(__name__)
|
|
| 52 |
|
| 53 |
_served_files: dict[str, str] = {}
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
# βββ Server Instance ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 56 |
|
| 57 |
app = Server()
|
|
@@ -69,13 +82,14 @@ async def homepage():
|
|
| 69 |
|
| 70 |
config = json.dumps({
|
| 71 |
"app_title": APP_TITLE,
|
| 72 |
-
"model_id":
|
| 73 |
"model_url": MODEL_URL,
|
| 74 |
"languages": LANGUAGE_OPTIONS,
|
| 75 |
"examples": [
|
| 76 |
{"label": label, "prompt": prompt, "language": lang, "framework": fw}
|
| 77 |
for label, prompt, lang, fw in EXAMPLE_PROMPTS
|
| 78 |
],
|
|
|
|
| 79 |
})
|
| 80 |
content = content.replace("__RUNTIME_CONFIG__", config)
|
| 81 |
return content
|
|
@@ -105,9 +119,77 @@ async def serve_download(filename: str):
|
|
| 105 |
return HTMLResponse("Not found", status_code=404)
|
| 106 |
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
# βββ Gradio API Endpoints ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
@app.api(name="web_search", concurrency_limit=4)
|
| 112 |
def handle_web_search(query: str) -> str:
|
| 113 |
"""Search the web using Google scraping. No API key needed."""
|
|
@@ -143,6 +225,7 @@ def handle_chat(
|
|
| 143 |
history_json: str,
|
| 144 |
exec_context_json: str,
|
| 145 |
search_enabled: str = "false",
|
|
|
|
| 146 |
) -> str:
|
| 147 |
"""Stream chat responses with code execution. Yields JSON strings."""
|
| 148 |
history = json.loads(history_json) if history_json else []
|
|
@@ -225,8 +308,11 @@ def handle_chat(
|
|
| 225 |
}
|
| 226 |
messages = chat_history_to_messages(model_history)
|
| 227 |
|
|
|
|
|
|
|
|
|
|
| 228 |
final_response = ""
|
| 229 |
-
for partial in call_model(messages):
|
| 230 |
final_response = partial
|
| 231 |
# Strip thinking blocks so chat only shows clean output
|
| 232 |
clean_partial = strip_thinking_blocks(partial)
|
|
@@ -250,8 +336,7 @@ def handle_chat(
|
|
| 250 |
})
|
| 251 |
return
|
| 252 |
|
| 253 |
-
# Extract code from response (use
|
| 254 |
-
# but chat history already has clean version)
|
| 255 |
clean_response = strip_thinking_blocks(final_response)
|
| 256 |
code, fence_lang = extract_code(clean_response)
|
| 257 |
target = normalize_language(target_language, fence_lang)
|
|
@@ -311,11 +396,11 @@ def handle_chat(
|
|
| 311 |
status_state = "success"
|
| 312 |
|
| 313 |
# Register image for serving
|
| 314 |
-
|
| 315 |
if image_path:
|
| 316 |
filename = os.path.basename(image_path)
|
| 317 |
_served_files[f"img:{filename}"] = image_path
|
| 318 |
-
|
| 319 |
|
| 320 |
# Register code for download
|
| 321 |
download_url = None
|
|
@@ -346,7 +431,7 @@ def handle_chat(
|
|
| 346 |
"fence_lang": fence_lang or target,
|
| 347 |
"stdout": stdout,
|
| 348 |
"stderr": stderr,
|
| 349 |
-
"image_url":
|
| 350 |
"image_path": image_path,
|
| 351 |
"status": status_text,
|
| 352 |
"language": fence_lang or target,
|
|
|
|
| 8 |
- API web_search β Google search scraping
|
| 9 |
- API chat β streaming chat with code execution
|
| 10 |
- API push_hf β push to HuggingFace Hub
|
| 11 |
+
- API switch_model β switch between loaded models
|
| 12 |
+
- API upload_image β upload image for VLM inference
|
| 13 |
"""
|
| 14 |
|
| 15 |
from __future__ import annotations
|
| 16 |
|
| 17 |
+
import base64
|
| 18 |
import json
|
| 19 |
import logging
|
| 20 |
import os
|
|
|
|
| 29 |
APP_TITLE,
|
| 30 |
EXAMPLE_PROMPTS,
|
| 31 |
LANGUAGE_OPTIONS,
|
| 32 |
+
MODEL_CONFIGS,
|
| 33 |
MODEL_URL,
|
| 34 |
PY_TIMEOUT_S,
|
| 35 |
)
|
|
|
|
| 44 |
from code.execution.gradio_runner import run_gradio_app, stop_gradio_app
|
| 45 |
from code.execution.python_runner import run_python
|
| 46 |
from code.huggingface.push import create_project_zip, push_to_huggingface
|
| 47 |
+
from code.model.loader import (
|
| 48 |
+
get_model_status,
|
| 49 |
+
is_model_loaded,
|
| 50 |
+
get_current_model_key,
|
| 51 |
+
get_current_model_type,
|
| 52 |
+
switch_model,
|
| 53 |
+
)
|
| 54 |
from code.model.inference import call_model
|
| 55 |
from code.server.chat_helpers import chat_history_to_messages, targeted_prompt
|
| 56 |
from code.websearch.google_scraper import web_search_google, format_search_results
|
|
|
|
| 61 |
|
| 62 |
_served_files: dict[str, str] = {}
|
| 63 |
|
| 64 |
+
# βββ Uploaded Images Registry βββββββββββββββββββββββββββββββββββββββββββ
|
| 65 |
+
|
| 66 |
+
_uploaded_images: dict[str, str] = {}
|
| 67 |
+
|
| 68 |
# βββ Server Instance ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
|
| 70 |
app = Server()
|
|
|
|
| 82 |
|
| 83 |
config = json.dumps({
|
| 84 |
"app_title": APP_TITLE,
|
| 85 |
+
"model_id": MODEL_CONFIGS,
|
| 86 |
"model_url": MODEL_URL,
|
| 87 |
"languages": LANGUAGE_OPTIONS,
|
| 88 |
"examples": [
|
| 89 |
{"label": label, "prompt": prompt, "language": lang, "framework": fw}
|
| 90 |
for label, prompt, lang, fw in EXAMPLE_PROMPTS
|
| 91 |
],
|
| 92 |
+
"default_model": "minicpm5-1b",
|
| 93 |
})
|
| 94 |
content = content.replace("__RUNTIME_CONFIG__", config)
|
| 95 |
return content
|
|
|
|
| 119 |
return HTMLResponse("Not found", status_code=404)
|
| 120 |
|
| 121 |
|
| 122 |
+
@app.get("/uploaded-images/{image_id}")
|
| 123 |
+
async def serve_uploaded_image(image_id: str):
|
| 124 |
+
"""Serve an uploaded image by its ID."""
|
| 125 |
+
path = _uploaded_images.get(image_id)
|
| 126 |
+
if path and os.path.exists(path):
|
| 127 |
+
return FileResponse(path, media_type="image/png")
|
| 128 |
+
return HTMLResponse("Not found", status_code=404)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
# βββ Gradio API Endpoints ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 132 |
|
| 133 |
|
| 134 |
+
@app.api(name="switch_model", concurrency_limit=1)
|
| 135 |
+
def handle_switch_model(model_key: str) -> str:
|
| 136 |
+
"""Switch to a different model."""
|
| 137 |
+
result = switch_model(model_key)
|
| 138 |
+
yield json.dumps(result)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
@app.api(name="upload_image", concurrency_limit=4)
|
| 142 |
+
def handle_upload_image(image_data: str) -> str:
|
| 143 |
+
"""Upload a base64-encoded image for VLM inference.
|
| 144 |
+
|
| 145 |
+
Returns an image ID that can be referenced in chat.
|
| 146 |
+
"""
|
| 147 |
+
try:
|
| 148 |
+
if not image_data:
|
| 149 |
+
yield json.dumps({"success": False, "message": "No image data provided"})
|
| 150 |
+
return
|
| 151 |
+
|
| 152 |
+
# Handle data URI format: data:image/png;base64,...
|
| 153 |
+
if image_data.startswith("data:"):
|
| 154 |
+
# Extract the base64 part
|
| 155 |
+
parts = image_data.split(",", 1)
|
| 156 |
+
if len(parts) == 2:
|
| 157 |
+
image_data = parts[1]
|
| 158 |
+
|
| 159 |
+
# Decode base64
|
| 160 |
+
image_bytes = base64.b64decode(image_data)
|
| 161 |
+
|
| 162 |
+
# Save to temp file
|
| 163 |
+
img_dir = tempfile.mkdtemp(prefix="uploaded_img_")
|
| 164 |
+
image_id = f"img_{os.getpid()}_{int(os.urandom(4).hex(), 16)}"
|
| 165 |
+
img_path = os.path.join(img_dir, f"{image_id}.png")
|
| 166 |
+
Path(img_path).write_bytes(image_bytes)
|
| 167 |
+
|
| 168 |
+
# Register for serving
|
| 169 |
+
_uploaded_images[image_id] = img_path
|
| 170 |
+
|
| 171 |
+
# Create a URL for the image that the VLM can access
|
| 172 |
+
image_url = f"/uploaded-images/{image_id}"
|
| 173 |
+
|
| 174 |
+
# Also save as a file:// URL for local VLM access
|
| 175 |
+
file_url = f"file://{img_path}"
|
| 176 |
+
|
| 177 |
+
yield json.dumps({
|
| 178 |
+
"success": True,
|
| 179 |
+
"image_id": image_id,
|
| 180 |
+
"image_url": image_url,
|
| 181 |
+
"file_url": file_url,
|
| 182 |
+
"message": "Image uploaded successfully",
|
| 183 |
+
})
|
| 184 |
+
|
| 185 |
+
except Exception as exc:
|
| 186 |
+
logger.exception("Image upload failed")
|
| 187 |
+
yield json.dumps({
|
| 188 |
+
"success": False,
|
| 189 |
+
"message": f"Upload failed: {str(exc)}",
|
| 190 |
+
})
|
| 191 |
+
|
| 192 |
+
|
| 193 |
@app.api(name="web_search", concurrency_limit=4)
|
| 194 |
def handle_web_search(query: str) -> str:
|
| 195 |
"""Search the web using Google scraping. No API key needed."""
|
|
|
|
| 225 |
history_json: str,
|
| 226 |
exec_context_json: str,
|
| 227 |
search_enabled: str = "false",
|
| 228 |
+
image_url: str = "",
|
| 229 |
) -> str:
|
| 230 |
"""Stream chat responses with code execution. Yields JSON strings."""
|
| 231 |
history = json.loads(history_json) if history_json else []
|
|
|
|
| 308 |
}
|
| 309 |
messages = chat_history_to_messages(model_history)
|
| 310 |
|
| 311 |
+
# Determine image URL for VLM
|
| 312 |
+
vlm_image_url = image_url.strip() if image_url else None
|
| 313 |
+
|
| 314 |
final_response = ""
|
| 315 |
+
for partial in call_model(messages, image_url=vlm_image_url):
|
| 316 |
final_response = partial
|
| 317 |
# Strip thinking blocks so chat only shows clean output
|
| 318 |
clean_partial = strip_thinking_blocks(partial)
|
|
|
|
| 336 |
})
|
| 337 |
return
|
| 338 |
|
| 339 |
+
# Extract code from response (use cleaned version)
|
|
|
|
| 340 |
clean_response = strip_thinking_blocks(final_response)
|
| 341 |
code, fence_lang = extract_code(clean_response)
|
| 342 |
target = normalize_language(target_language, fence_lang)
|
|
|
|
| 396 |
status_state = "success"
|
| 397 |
|
| 398 |
# Register image for serving
|
| 399 |
+
image_url_out = None
|
| 400 |
if image_path:
|
| 401 |
filename = os.path.basename(image_path)
|
| 402 |
_served_files[f"img:{filename}"] = image_path
|
| 403 |
+
image_url_out = f"/images/{filename}"
|
| 404 |
|
| 405 |
# Register code for download
|
| 406 |
download_url = None
|
|
|
|
| 431 |
"fence_lang": fence_lang or target,
|
| 432 |
"stdout": stdout,
|
| 433 |
"stderr": stderr,
|
| 434 |
+
"image_url": image_url_out,
|
| 435 |
"image_path": image_path,
|
| 436 |
"status": status_text,
|
| 437 |
"language": fence_lang or target,
|
index.html
CHANGED
|
@@ -212,6 +212,30 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
|
|
| 212 |
text-shadow: var(--glow-purple);
|
| 213 |
}
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 216 |
BANNER
|
| 217 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
@@ -937,13 +961,17 @@ body.hide-thinking .think-block { display: none; }
|
|
| 937 |
<header id="header">
|
| 938 |
<div class="header-title">
|
| 939 |
<div class="header-ascii">╔═══ FULLSTACK CODE BUILDER ═══╚</div>
|
| 940 |
-
<div class="header-subtitle">Local AI App Generator | MiniCPM5-1B</div>
|
| 941 |
</div>
|
| 942 |
<div class="header-actions">
|
| 943 |
<a class="pill" id="model-pill" href="#" target="_blank" rel="noopener">
|
| 944 |
<span class="dot loading" id="model-dot"></span>
|
| 945 |
<span id="model-pill-text">MiniCPM5-1B</span>
|
| 946 |
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 947 |
<button id="btn-thinking" class="btn-thinking active" onclick="toggleThinking()" title="Show/hide thinking blocks">π§ Think</button>
|
| 948 |
<button id="btn-new-chat" onclick="newChat()" title="Start a new chat session">[NEW]</button>
|
| 949 |
</div>
|
|
@@ -969,6 +997,12 @@ body.hide-thinking .think-block { display: none; }
|
|
| 969 |
<span class="selector-label">Framework:</span>
|
| 970 |
<select id="framework-select"></select>
|
| 971 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
</div>
|
| 973 |
<div id="input-row">
|
| 974 |
<span class="input-prompt-symbol">❯</span>
|
|
@@ -1123,6 +1157,10 @@ const state = {
|
|
| 1123 |
searchEnabled: false,
|
| 1124 |
lastSearchResults: [],
|
| 1125 |
showThinking: true,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1126 |
};
|
| 1127 |
|
| 1128 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1197,6 +1235,23 @@ async function pollModelStatus() {
|
|
| 1197 |
statusText.textContent = 'MODEL READY';
|
| 1198 |
indicator.className = 'status-indicator status-success';
|
| 1199 |
document.getElementById('btn-push-hf').disabled = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
setTimeout(() => {
|
| 1201 |
if (!state.isGenerating) {
|
| 1202 |
indicator.className = 'status-indicator status-idle';
|
|
@@ -1793,7 +1848,7 @@ async function sendMessage(prompt) {
|
|
| 1793 |
method: 'POST',
|
| 1794 |
headers: { 'Content-Type': 'application/json' },
|
| 1795 |
body: JSON.stringify({
|
| 1796 |
-
data: [prompt, state.targetLanguage, framework, historyJSON, execContextJSON, state.searchEnabled ? 'true' : 'false']
|
| 1797 |
})
|
| 1798 |
});
|
| 1799 |
|
|
@@ -1951,6 +2006,143 @@ function toggleThinking() {
|
|
| 1951 |
}
|
| 1952 |
}
|
| 1953 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1954 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1955 |
// WEB SEARCH
|
| 1956 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 212 |
text-shadow: var(--glow-purple);
|
| 213 |
}
|
| 214 |
|
| 215 |
+
#model-select {
|
| 216 |
+
background: var(--bg-deep);
|
| 217 |
+
border: 1px solid var(--border);
|
| 218 |
+
color: var(--cyan);
|
| 219 |
+
font-family: var(--font-mono);
|
| 220 |
+
font-size: 11px;
|
| 221 |
+
padding: 5px 8px;
|
| 222 |
+
border-radius: var(--radius);
|
| 223 |
+
outline: none;
|
| 224 |
+
cursor: pointer;
|
| 225 |
+
transition: border-color var(--transition);
|
| 226 |
+
}
|
| 227 |
+
#model-select:focus { border-color: var(--border-focus); }
|
| 228 |
+
#model-select option { background: var(--bg-deep); color: var(--gray-light); }
|
| 229 |
+
|
| 230 |
+
#btn-attach-image {
|
| 231 |
+
background: transparent; border: 1px solid var(--border); color: var(--amber);
|
| 232 |
+
font-family: var(--font-mono); font-size: 14px; padding: 3px 8px;
|
| 233 |
+
border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
|
| 234 |
+
}
|
| 235 |
+
#btn-attach-image:hover {
|
| 236 |
+
border-color: var(--amber); background: rgba(255,179,0,0.1);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 240 |
BANNER
|
| 241 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
|
|
| 961 |
<header id="header">
|
| 962 |
<div class="header-title">
|
| 963 |
<div class="header-ascii">╔═══ FULLSTACK CODE BUILDER ═══╚</div>
|
| 964 |
+
<div class="header-subtitle">Local AI App Generator | <span id="header-model-name">MiniCPM5-1B</span></div>
|
| 965 |
</div>
|
| 966 |
<div class="header-actions">
|
| 967 |
<a class="pill" id="model-pill" href="#" target="_blank" rel="noopener">
|
| 968 |
<span class="dot loading" id="model-dot"></span>
|
| 969 |
<span id="model-pill-text">MiniCPM5-1B</span>
|
| 970 |
</a>
|
| 971 |
+
<select id="model-select" onchange="onModelChange()" title="Switch AI model">
|
| 972 |
+
<option value="minicpm5-1b">MiniCPM5-1B (text)</option>
|
| 973 |
+
<option value="minicpm-v-4.6">MiniCPM-V-4.6 (vision)</option>
|
| 974 |
+
</select>
|
| 975 |
<button id="btn-thinking" class="btn-thinking active" onclick="toggleThinking()" title="Show/hide thinking blocks">π§ Think</button>
|
| 976 |
<button id="btn-new-chat" onclick="newChat()" title="Start a new chat session">[NEW]</button>
|
| 977 |
</div>
|
|
|
|
| 997 |
<span class="selector-label">Framework:</span>
|
| 998 |
<select id="framework-select"></select>
|
| 999 |
</div>
|
| 1000 |
+
<div class="selector-group" id="image-attach-group" style="display:none;">
|
| 1001 |
+
<input type="file" id="image-upload" accept="image/*" style="display:none" onchange="onImageUpload(event)">
|
| 1002 |
+
<button id="btn-attach-image" onclick="document.getElementById('image-upload').click()" title="Attach image (VLM only)">π·</button>
|
| 1003 |
+
<span id="image-attach-name" style="font-size:10px;color:var(--gray-dim);max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
|
| 1004 |
+
<button id="btn-remove-image" onclick="removeImage()" title="Remove image" style="display:none;font-size:10px;color:var(--red);background:none;border:none;cursor:pointer;">β</button>
|
| 1005 |
+
</div>
|
| 1006 |
</div>
|
| 1007 |
<div id="input-row">
|
| 1008 |
<span class="input-prompt-symbol">❯</span>
|
|
|
|
| 1157 |
searchEnabled: false,
|
| 1158 |
lastSearchResults: [],
|
| 1159 |
showThinking: true,
|
| 1160 |
+
currentModelKey: 'minicpm5-1b',
|
| 1161 |
+
currentModelType: 'text',
|
| 1162 |
+
uploadedImageFileUrl: '',
|
| 1163 |
+
uploadedImageName: '',
|
| 1164 |
};
|
| 1165 |
|
| 1166 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1235 |
statusText.textContent = 'MODEL READY';
|
| 1236 |
indicator.className = 'status-indicator status-success';
|
| 1237 |
document.getElementById('btn-push-hf').disabled = false;
|
| 1238 |
+
// Update model info from server response
|
| 1239 |
+
if (data.model_key) state.currentModelKey = data.model_key;
|
| 1240 |
+
if (data.model_type) state.currentModelType = data.model_type;
|
| 1241 |
+
if (data.model_name) {
|
| 1242 |
+
document.getElementById('model-pill-text').textContent = data.model_name;
|
| 1243 |
+
document.getElementById('header-model-name').textContent = data.model_name;
|
| 1244 |
+
}
|
| 1245 |
+
// Show/hide image upload based on model type
|
| 1246 |
+
const imageGroup = document.getElementById('image-attach-group');
|
| 1247 |
+
if (state.currentModelType === 'vlm') {
|
| 1248 |
+
imageGroup.style.display = 'flex';
|
| 1249 |
+
} else {
|
| 1250 |
+
imageGroup.style.display = 'none';
|
| 1251 |
+
}
|
| 1252 |
+
// Sync model selector
|
| 1253 |
+
const modelSelect = document.getElementById('model-select');
|
| 1254 |
+
if (modelSelect && data.model_key) modelSelect.value = data.model_key;
|
| 1255 |
setTimeout(() => {
|
| 1256 |
if (!state.isGenerating) {
|
| 1257 |
indicator.className = 'status-indicator status-idle';
|
|
|
|
| 1848 |
method: 'POST',
|
| 1849 |
headers: { 'Content-Type': 'application/json' },
|
| 1850 |
body: JSON.stringify({
|
| 1851 |
+
data: [prompt, state.targetLanguage, framework, historyJSON, execContextJSON, state.searchEnabled ? 'true' : 'false', state.uploadedImageFileUrl || '']
|
| 1852 |
})
|
| 1853 |
});
|
| 1854 |
|
|
|
|
| 2006 |
}
|
| 2007 |
}
|
| 2008 |
|
| 2009 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2010 |
+
// MODEL SWITCHING
|
| 2011 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2012 |
+
async function onModelChange() {
|
| 2013 |
+
const select = document.getElementById('model-select');
|
| 2014 |
+
const modelKey = select.value;
|
| 2015 |
+
if (modelKey === state.currentModelKey) return;
|
| 2016 |
+
|
| 2017 |
+
const isVLM = modelKey === 'minicpm-v-4.6';
|
| 2018 |
+
const imageGroup = document.getElementById('image-attach-group');
|
| 2019 |
+
if (isVLM) {
|
| 2020 |
+
imageGroup.style.display = 'flex';
|
| 2021 |
+
} else {
|
| 2022 |
+
imageGroup.style.display = 'none';
|
| 2023 |
+
removeImage();
|
| 2024 |
+
}
|
| 2025 |
+
|
| 2026 |
+
addSystemMessage(`Switching model to ${select.options[select.selectedIndex].text}...`);
|
| 2027 |
+
renderStatus('Switching model...', 'working');
|
| 2028 |
+
|
| 2029 |
+
try {
|
| 2030 |
+
const resp = await fetch('/gradio_api/call/switch_model', {
|
| 2031 |
+
method: 'POST',
|
| 2032 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2033 |
+
body: JSON.stringify({ data: [modelKey] })
|
| 2034 |
+
});
|
| 2035 |
+
const { event_id } = await resp.json();
|
| 2036 |
+
const eventSource = new EventSource(`/gradio_api/call/switch_model/${event_id}`);
|
| 2037 |
+
|
| 2038 |
+
eventSource.addEventListener('complete', (e) => {
|
| 2039 |
+
const dataArray = JSON.parse(e.data);
|
| 2040 |
+
const result = JSON.parse(dataArray[0]);
|
| 2041 |
+
if (result.success) {
|
| 2042 |
+
state.currentModelKey = modelKey;
|
| 2043 |
+
state.currentModelType = isVLM ? 'vlm' : 'text';
|
| 2044 |
+
const name = isVLM ? 'MiniCPM-V-4.6' : 'MiniCPM5-1B';
|
| 2045 |
+
document.getElementById('model-pill-text').textContent = name;
|
| 2046 |
+
document.getElementById('header-model-name').textContent = name;
|
| 2047 |
+
addSystemMessage(`Switched to ${name}. Model is loading in background...`);
|
| 2048 |
+
// Poll for model ready
|
| 2049 |
+
pollModelStatus();
|
| 2050 |
+
} else {
|
| 2051 |
+
addSystemMessage(`Failed to switch: ${result.message}`);
|
| 2052 |
+
select.value = state.currentModelKey;
|
| 2053 |
+
}
|
| 2054 |
+
eventSource.close();
|
| 2055 |
+
});
|
| 2056 |
+
eventSource.addEventListener('error', () => {
|
| 2057 |
+
addSystemMessage('Model switch failed');
|
| 2058 |
+
select.value = state.currentModelKey;
|
| 2059 |
+
eventSource.close();
|
| 2060 |
+
});
|
| 2061 |
+
} catch (err) {
|
| 2062 |
+
addSystemMessage(`Switch error: ${err.message}`);
|
| 2063 |
+
select.value = state.currentModelKey;
|
| 2064 |
+
}
|
| 2065 |
+
}
|
| 2066 |
+
|
| 2067 |
+
function pollModelStatus() {
|
| 2068 |
+
const interval = setInterval(async () => {
|
| 2069 |
+
try {
|
| 2070 |
+
const resp = await fetch('/api/model-status');
|
| 2071 |
+
const status = await resp.json();
|
| 2072 |
+
if (status.status === 'ready') {
|
| 2073 |
+
state.modelReady = true;
|
| 2074 |
+
const dot = document.getElementById('model-dot');
|
| 2075 |
+
dot.className = 'dot';
|
| 2076 |
+
dot.style.background = 'var(--success)';
|
| 2077 |
+
dot.style.boxShadow = '0 0 6px var(--success)';
|
| 2078 |
+
renderStatus('Ready', 'success');
|
| 2079 |
+
setTimeout(() => renderStatus('Idle', 'idle'), 2000);
|
| 2080 |
+
clearInterval(interval);
|
| 2081 |
+
} else if (status.status === 'error') {
|
| 2082 |
+
state.modelReady = false;
|
| 2083 |
+
renderStatus('Model error', 'error');
|
| 2084 |
+
clearInterval(interval);
|
| 2085 |
+
}
|
| 2086 |
+
} catch { clearInterval(interval); }
|
| 2087 |
+
}, 2000);
|
| 2088 |
+
}
|
| 2089 |
+
|
| 2090 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2091 |
+
// IMAGE UPLOAD (VLM)
|
| 2092 |
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2093 |
+
async function onImageUpload(event) {
|
| 2094 |
+
const file = event.target.files[0];
|
| 2095 |
+
if (!file) return;
|
| 2096 |
+
|
| 2097 |
+
state.uploadedImageName = file.name;
|
| 2098 |
+
document.getElementById('image-attach-name').textContent = file.name;
|
| 2099 |
+
document.getElementById('btn-remove-image').style.display = 'inline';
|
| 2100 |
+
|
| 2101 |
+
// Convert to base64
|
| 2102 |
+
const reader = new FileReader();
|
| 2103 |
+
reader.onload = async function(e) {
|
| 2104 |
+
const base64Data = e.target.result;
|
| 2105 |
+
|
| 2106 |
+
// Upload to server
|
| 2107 |
+
try {
|
| 2108 |
+
const resp = await fetch('/gradio_api/call/upload_image', {
|
| 2109 |
+
method: 'POST',
|
| 2110 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2111 |
+
body: JSON.stringify({ data: [base64Data] })
|
| 2112 |
+
});
|
| 2113 |
+
const { event_id } = await resp.json();
|
| 2114 |
+
const eventSource = new EventSource(`/gradio_api/call/upload_image/${event_id}`);
|
| 2115 |
+
|
| 2116 |
+
eventSource.addEventListener('complete', (ev) => {
|
| 2117 |
+
const dataArray = JSON.parse(ev.data);
|
| 2118 |
+
const result = JSON.parse(dataArray[0]);
|
| 2119 |
+
if (result.success) {
|
| 2120 |
+
state.uploadedImageFileUrl = result.file_url;
|
| 2121 |
+
document.getElementById('image-attach-name').textContent = 'β ' + file.name;
|
| 2122 |
+
} else {
|
| 2123 |
+
document.getElementById('image-attach-name').textContent = 'β Upload failed';
|
| 2124 |
+
}
|
| 2125 |
+
eventSource.close();
|
| 2126 |
+
});
|
| 2127 |
+
eventSource.addEventListener('error', () => {
|
| 2128 |
+
document.getElementById('image-attach-name').textContent = 'β Upload error';
|
| 2129 |
+
eventSource.close();
|
| 2130 |
+
});
|
| 2131 |
+
} catch (err) {
|
| 2132 |
+
document.getElementById('image-attach-name').textContent = 'β Error';
|
| 2133 |
+
}
|
| 2134 |
+
};
|
| 2135 |
+
reader.readAsDataURL(file);
|
| 2136 |
+
}
|
| 2137 |
+
|
| 2138 |
+
function removeImage() {
|
| 2139 |
+
state.uploadedImageFileUrl = '';
|
| 2140 |
+
state.uploadedImageName = '';
|
| 2141 |
+
document.getElementById('image-upload').value = '';
|
| 2142 |
+
document.getElementById('image-attach-name').textContent = '';
|
| 2143 |
+
document.getElementById('btn-remove-image').style.display = 'none';
|
| 2144 |
+
}
|
| 2145 |
+
|
| 2146 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2147 |
// WEB SEARCH
|
| 2148 |
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
requirements.txt
CHANGED
|
@@ -7,3 +7,4 @@ matplotlib>=3.8
|
|
| 7 |
requests>=2.31.0
|
| 8 |
beautifulsoup4>=4.12.0
|
| 9 |
Pillow>=10.0
|
|
|
|
|
|
| 7 |
requests>=2.31.0
|
| 8 |
beautifulsoup4>=4.12.0
|
| 9 |
Pillow>=10.0
|
| 10 |
+
torchvision>=0.16.0
|