bored10 commited on
Commit
035ca90
·
verified ·
1 Parent(s): 49e74e9

Add Space application

Browse files
Files changed (1) hide show
  1. app.py +178 -0
app.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A secure Gradio demo for an OpenAI-compatible Qwen3.8-Flash-Next endpoint."""
2
+
3
+ import spaces # ZeroGPU requires one decorated function, even for an API proxy.
4
+
5
+ import base64
6
+ import mimetypes
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import gradio as gr
12
+ from openai import OpenAI
13
+
14
+
15
+ MODEL_ID = os.getenv("MODEL_ID", "Qwen/Qwen3.8-Flash-Next")
16
+ API_KEY = os.getenv("OPENAI_API_KEY")
17
+ BASE_URL = os.getenv("OPENAI_BASE_URL")
18
+ MAX_IMAGE_BYTES = 10 * 1024 * 1024
19
+
20
+
21
+ @spaces.GPU(duration=1)
22
+ def _zerogpu_marker() -> None:
23
+ """Declare ZeroGPU support without allocating a GPU for API-proxy requests."""
24
+ return None
25
+
26
+
27
+ def image_part(image_path: str | None) -> dict[str, Any] | None:
28
+ """Return a local image as a bounded data URL for the vision-capable model."""
29
+ if not image_path:
30
+ return None
31
+
32
+ path = Path(image_path)
33
+ if not path.is_file():
34
+ raise ValueError("The uploaded image could not be read. Please try again.")
35
+ if path.stat().st_size > MAX_IMAGE_BYTES:
36
+ raise ValueError("Images must be 10 MB or smaller.")
37
+
38
+ media_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
39
+ if not media_type.startswith("image/"):
40
+ raise ValueError("Please upload an image file.")
41
+
42
+ encoded = base64.b64encode(path.read_bytes()).decode("ascii")
43
+ return {"type": "image_url", "image_url": {"url": f"data:{media_type};base64,{encoded}"}}
44
+
45
+
46
+ def api_messages(history: list[dict[str, Any]]) -> list[dict[str, Any]]:
47
+ """Convert Gradio's text-only history into Chat Completions messages."""
48
+ messages: list[dict[str, Any]] = []
49
+ for item in history:
50
+ role = item.get("role")
51
+ content = item.get("content")
52
+ if role in {"user", "assistant"} and isinstance(content, str):
53
+ messages.append({"role": role, "content": content})
54
+ return messages
55
+
56
+
57
+ def respond(
58
+ message: str,
59
+ image_path: str | None,
60
+ history: list[dict[str, Any]],
61
+ enable_thinking: bool,
62
+ reasoning_effort: str,
63
+ ):
64
+ """Answer a question with Qwen3.8-Flash-Next, optionally using an uploaded image."""
65
+ text = (message or "").strip()
66
+ if not text and not image_path:
67
+ yield history, "Add a question or an image before sending."
68
+ return
69
+ if not API_KEY or not BASE_URL:
70
+ yield history, (
71
+ "This Space needs the OPENAI_API_KEY and OPENAI_BASE_URL secrets. "
72
+ "See the README setup instructions."
73
+ )
74
+ return
75
+
76
+ try:
77
+ content: list[dict[str, Any]] = []
78
+ part = image_part(image_path)
79
+ if part:
80
+ content.append(part)
81
+ if text:
82
+ content.append({"type": "text", "text": text})
83
+
84
+ user_message: dict[str, Any] = {"role": "user", "content": content}
85
+ conversation = api_messages(history)
86
+ conversation.append(user_message)
87
+ display_text = text or "Please analyse this image."
88
+ updated_history = [*history, {"role": "user", "content": display_text}]
89
+
90
+ client = OpenAI(api_key=API_KEY, base_url=BASE_URL, timeout=180)
91
+ stream = client.chat.completions.create(
92
+ model=MODEL_ID,
93
+ messages=conversation,
94
+ stream=True,
95
+ reasoning_effort=reasoning_effort,
96
+ extra_body={
97
+ "chat_template_kwargs": {
98
+ "enable_thinking": enable_thinking,
99
+ "preserve_thinking": False,
100
+ }
101
+ },
102
+ )
103
+
104
+ answer = ""
105
+ for chunk in stream:
106
+ if not chunk.choices:
107
+ continue
108
+ delta = chunk.choices[0].delta
109
+ # Deliberately do not display internal reasoning traces. Only final output is shown.
110
+ if delta.content:
111
+ answer += delta.content
112
+ yield [*updated_history, {"role": "assistant", "content": answer}], ""
113
+
114
+ if not answer:
115
+ answer = "The endpoint returned no final response. Try again or check its logs."
116
+ yield [*updated_history, {"role": "assistant", "content": answer}], ""
117
+ except ValueError as error:
118
+ yield history, str(error)
119
+ except Exception:
120
+ yield history, "The model endpoint could not complete this request. Please try again shortly."
121
+
122
+
123
+ with gr.Blocks(title="Qwen3.8-Flash-Next Demo") as demo:
124
+ gr.Markdown(
125
+ "# Qwen3.8-Flash-Next\n"
126
+ "A private-key, OpenAI-compatible chat demo for the Qwen vision-language checkpoint. "
127
+ "Your API credentials remain in Space secrets."
128
+ )
129
+ with gr.Row():
130
+ with gr.Column(scale=3):
131
+ chatbot = gr.Chatbot(label="Conversation", type="messages", height=520)
132
+ with gr.Row():
133
+ message = gr.Textbox(
134
+ label="Your message",
135
+ placeholder="Ask a question, request code, or describe the attached image…",
136
+ lines=2,
137
+ autofocus=True,
138
+ )
139
+ image = gr.Image(label="Optional image", type="filepath", sources=["upload"])
140
+ with gr.Row():
141
+ send = gr.Button("Send", variant="primary")
142
+ clear = gr.Button("Clear conversation")
143
+ with gr.Column(scale=1):
144
+ gr.Markdown("### Generation settings")
145
+ enable_thinking = gr.Checkbox(
146
+ label="Enable model thinking",
147
+ value=True,
148
+ info="The model reasons internally; only its final answer is shown.",
149
+ )
150
+ reasoning_effort = gr.Radio(
151
+ choices=["low", "medium", "xhigh"],
152
+ value="xhigh",
153
+ label="Reasoning effort",
154
+ info="Higher effort can improve complex answers but takes longer.",
155
+ )
156
+ status = gr.Textbox(label="Status", interactive=False, lines=2)
157
+ gr.Markdown(
158
+ "**Model:** `Qwen/Qwen3.8-Flash-Next` \n"
159
+ "**Inputs:** text and one image (up to 10 MB)"
160
+ )
161
+ gr.Examples(
162
+ examples=[
163
+ ["Write a concise Python function that merges two sorted linked lists."],
164
+ ["Explain the practical difference between sparse attention and full attention."],
165
+ ["Summarise this image and list details that could be important to a researcher."],
166
+ ],
167
+ inputs=message,
168
+ label="Try an example",
169
+ )
170
+
171
+ inputs = [message, image, chatbot, enable_thinking, reasoning_effort]
172
+ send.click(respond, inputs=inputs, outputs=[chatbot, status])
173
+ message.submit(respond, inputs=inputs, outputs=[chatbot, status])
174
+ clear.click(lambda: ([], ""), outputs=[chatbot, status])
175
+
176
+
177
+ if __name__ == "__main__":
178
+ demo.queue(default_concurrency_limit=8, max_size=32).launch(mcp_server=True)